mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 20:17:32 +08:00
feat: enhance plan validation, traffic system and email verification
- feat: add plan price validation - feat: make traffic packages stackable - feat: add commission and invite info to admin order details - feat: apply email whitelist to verification code API - fix: subscription link copy compatibility for non-HTTPS - fix: resolve route editing 500 error in certain cases - refactor: restructure traffic reset logic
This commit is contained in:
@@ -88,6 +88,14 @@ class Order extends Model
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请人
|
||||
*/
|
||||
public function invite_user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invite_user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与订单关联的套餐
|
||||
*/
|
||||
|
||||
@@ -115,72 +115,6 @@ class Plan extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一次流量重置时间
|
||||
*
|
||||
* @param Carbon|null $from 计算起始时间,默认为当前时间
|
||||
* @return Carbon|null 下次重置时间,如果不重置则返回null
|
||||
*/
|
||||
public function getNextResetTime(?Carbon $from = null): ?Carbon
|
||||
{
|
||||
$from = $from ?? Carbon::now();
|
||||
|
||||
switch ($this->reset_traffic_method) {
|
||||
case self::RESET_TRAFFIC_FIRST_DAY_MONTH:
|
||||
return $from->copy()->addMonth()->startOfMonth();
|
||||
|
||||
case self::RESET_TRAFFIC_MONTHLY:
|
||||
return $from->copy()->addMonth()->startOfDay();
|
||||
|
||||
case self::RESET_TRAFFIC_FIRST_DAY_YEAR:
|
||||
return $from->copy()->addYear()->startOfYear();
|
||||
|
||||
case self::RESET_TRAFFIC_YEARLY:
|
||||
return $from->copy()->addYear()->startOfDay();
|
||||
|
||||
case self::RESET_TRAFFIC_NEVER:
|
||||
return null;
|
||||
|
||||
case self::RESET_TRAFFIC_FOLLOW_SYSTEM:
|
||||
default:
|
||||
// 这里需要实现获取系统设置的逻辑
|
||||
// 可以通过系统配置或其他方式获取
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要重置流量
|
||||
*
|
||||
* @param Carbon|null $checkTime 检查时间点,默认为当前时间
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldResetTraffic(?Carbon $checkTime = null): bool
|
||||
{
|
||||
if ($this->reset_traffic_method === self::RESET_TRAFFIC_NEVER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$checkTime = $checkTime ?? Carbon::now();
|
||||
$nextResetTime = $this->getNextResetTime($checkTime);
|
||||
|
||||
if ($nextResetTime === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $checkTime->greaterThanOrEqualTo($nextResetTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量重置方式的描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResetTrafficMethodName(): string
|
||||
{
|
||||
return self::getResetTrafficMethods()[$this->reset_traffic_method] ?? '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的订阅周期
|
||||
*
|
||||
|
||||
@@ -12,5 +12,6 @@ class ServerRoute extends Model
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'match' => 'array'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 流量重置记录模型
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id 用户ID
|
||||
* @property string $reset_type 重置类型
|
||||
* @property \Carbon\Carbon $reset_time 重置时间
|
||||
* @property int $old_upload 重置前上传流量
|
||||
* @property int $old_download 重置前下载流量
|
||||
* @property int $old_total 重置前总流量
|
||||
* @property int $new_upload 重置后上传流量
|
||||
* @property int $new_download 重置后下载流量
|
||||
* @property int $new_total 重置后总流量
|
||||
* @property string $trigger_source 触发来源
|
||||
* @property array|null $metadata 额外元数据
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*
|
||||
* @property-read User $user 关联用户
|
||||
*/
|
||||
class TrafficResetLog extends Model
|
||||
{
|
||||
protected $table = 'v2_traffic_reset_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'reset_type',
|
||||
'reset_time',
|
||||
'old_upload',
|
||||
'old_download',
|
||||
'old_total',
|
||||
'new_upload',
|
||||
'new_download',
|
||||
'new_total',
|
||||
'trigger_source',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'reset_time' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// 重置类型常量
|
||||
public const TYPE_MONTHLY = 'monthly';
|
||||
public const TYPE_FIRST_DAY_MONTH = 'first_day_month';
|
||||
public const TYPE_YEARLY = 'yearly';
|
||||
public const TYPE_FIRST_DAY_YEAR = 'first_day_year';
|
||||
public const TYPE_MANUAL = 'manual';
|
||||
public const TYPE_PURCHASE = 'purchase';
|
||||
|
||||
// 触发来源常量
|
||||
public const SOURCE_AUTO = 'auto';
|
||||
public const SOURCE_MANUAL = 'manual';
|
||||
public const SOURCE_API = 'api';
|
||||
public const SOURCE_CRON = 'cron';
|
||||
public const SOURCE_USER_ACCESS = 'user_access';
|
||||
|
||||
/**
|
||||
* 获取重置类型的多语言名称
|
||||
*/
|
||||
public static function getResetTypeNames(): array
|
||||
{
|
||||
return [
|
||||
self::TYPE_MONTHLY => __('traffic_reset.reset_type.monthly'),
|
||||
self::TYPE_FIRST_DAY_MONTH => __('traffic_reset.reset_type.first_day_month'),
|
||||
self::TYPE_YEARLY => __('traffic_reset.reset_type.yearly'),
|
||||
self::TYPE_FIRST_DAY_YEAR => __('traffic_reset.reset_type.first_day_year'),
|
||||
self::TYPE_MANUAL => __('traffic_reset.reset_type.manual'),
|
||||
self::TYPE_PURCHASE => __('traffic_reset.reset_type.purchase'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取触发来源的多语言名称
|
||||
*/
|
||||
public static function getSourceNames(): array
|
||||
{
|
||||
return [
|
||||
self::SOURCE_AUTO => __('traffic_reset.source.auto'),
|
||||
self::SOURCE_MANUAL => __('traffic_reset.source.manual'),
|
||||
self::SOURCE_API => __('traffic_reset.source.api'),
|
||||
self::SOURCE_CRON => __('traffic_reset.source.cron'),
|
||||
self::SOURCE_USER_ACCESS => __('traffic_reset.source.user_access'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重置类型名称
|
||||
*/
|
||||
public function getResetTypeName(): string
|
||||
{
|
||||
return self::getResetTypeNames()[$this->reset_type] ?? $this->reset_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取触发来源名称
|
||||
*/
|
||||
public function getSourceName(): string
|
||||
{
|
||||
return self::getSourceNames()[$this->trigger_source] ?? $this->trigger_source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重置的流量差值
|
||||
*/
|
||||
public function getTrafficDiff(): array
|
||||
{
|
||||
return [
|
||||
'upload_diff' => $this->new_upload - $this->old_upload,
|
||||
'download_diff' => $this->new_download - $this->old_download,
|
||||
'total_diff' => $this->new_total - $this->old_total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化流量大小
|
||||
*/
|
||||
public function formatTraffic(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
+67
-2
@@ -37,6 +37,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property int|null $last_login_at 最后登录时间
|
||||
* @property int|null $parent_id 父账户ID
|
||||
* @property int|null $is_admin 是否管理员
|
||||
* @property \Carbon\Carbon|null $next_reset_at 下次流量重置时间
|
||||
* @property \Carbon\Carbon|null $last_reset_at 上次流量重置时间
|
||||
* @property int $reset_count 流量重置次数
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property bool $commission_auto_check 是否自动计算佣金
|
||||
@@ -48,6 +51,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, Order> $orders 订单列表
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, StatUser> $stat 统计信息
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $tickets 工单列表
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, TrafficResetLog> $trafficResetLogs 流量重置记录
|
||||
* @property-read User|null $parent 父账户
|
||||
* @property-read string $subscribe_url 订阅链接(动态生成)
|
||||
*/
|
||||
@@ -64,7 +68,9 @@ class User extends Authenticatable
|
||||
'remind_expire' => 'boolean',
|
||||
'remind_traffic' => 'boolean',
|
||||
'commission_auto_check' => 'boolean',
|
||||
'commission_rate' => 'float'
|
||||
'commission_rate' => 'float',
|
||||
'next_reset_at' => 'timestamp',
|
||||
'last_reset_at' => 'timestamp',
|
||||
];
|
||||
protected $hidden = ['password'];
|
||||
|
||||
@@ -72,7 +78,6 @@ class User extends Authenticatable
|
||||
public const COMMISSION_TYPE_PERIOD = 1;
|
||||
public const COMMISSION_TYPE_ONETIME = 2;
|
||||
|
||||
|
||||
// 获取邀请人信息
|
||||
public function invite_user(): BelongsTo
|
||||
{
|
||||
@@ -120,6 +125,14 @@ class User extends Authenticatable
|
||||
return $this->belongsTo(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联流量重置记录
|
||||
*/
|
||||
public function trafficResetLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(TrafficResetLog::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订阅链接属性
|
||||
*/
|
||||
@@ -127,4 +140,56 @@ class User extends Authenticatable
|
||||
{
|
||||
return Helper::getSubscribeUrl($this->token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否处于活跃状态
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return !$this->banned &&
|
||||
($this->expired_at === null || $this->expired_at > time()) &&
|
||||
$this->plan_id !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要重置流量
|
||||
*/
|
||||
public function shouldResetTraffic(): bool
|
||||
{
|
||||
return $this->isActive() &&
|
||||
$this->next_reset_at !== null &&
|
||||
$this->next_reset_at <= time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总使用流量
|
||||
*/
|
||||
public function getTotalUsedTraffic(): int
|
||||
{
|
||||
return ($this->u ?? 0) + ($this->d ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余流量
|
||||
*/
|
||||
public function getRemainingTraffic(): int
|
||||
{
|
||||
$used = $this->getTotalUsedTraffic();
|
||||
$total = $this->transfer_enable ?? 0;
|
||||
return max(0, $total - $used);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量使用百分比
|
||||
*/
|
||||
public function getTrafficUsagePercentage(): float
|
||||
{
|
||||
$total = $this->transfer_enable ?? 0;
|
||||
if ($total <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$used = $this->getTotalUsedTraffic();
|
||||
return min(100, ($used / $total) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user