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:
xboard
2025-06-22 01:18:38 +08:00
parent 7bab761db6
commit 4fe2f35183
34 changed files with 2176 additions and 539 deletions
+8
View File
@@ -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');
}
/**
* 获取与订单关联的套餐
*/
-66
View File
@@ -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] ?? '未知';
}
/**
* 获取所有可用的订阅周期
*
+1
View File
@@ -12,5 +12,6 @@ class ServerRoute extends Model
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'match' => 'array'
];
}
+147
View File
@@ -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
View File
@@ -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);
}
}