first commit
This commit is contained in:
21
Xboard/app/Models/AdminAuditLog.php
Normal file
21
Xboard/app/Models/AdminAuditLog.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminAuditLog extends Model
|
||||
{
|
||||
protected $table = 'v2_admin_audit_log';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
];
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
16
Xboard/app/Models/CommissionLog.php
Normal file
16
Xboard/app/Models/CommissionLog.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CommissionLog extends Model
|
||||
{
|
||||
protected $table = 'v2_commission_log';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
28
Xboard/app/Models/Coupon.php
Normal file
28
Xboard/app/Models/Coupon.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\PlanService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Coupon extends Model
|
||||
{
|
||||
protected $table = 'v2_coupon';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'limit_plan_ids' => 'array',
|
||||
'limit_period' => 'array',
|
||||
'show' => 'boolean',
|
||||
];
|
||||
|
||||
public function getLimitPeriodAttribute($value)
|
||||
{
|
||||
return collect(json_decode((string) $value, true))->map(function ($item) {
|
||||
return PlanService::getPeriodKey($item);
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
}
|
||||
260
Xboard/app/Models/GiftCardCode.php
Normal file
260
Xboard/app/Models/GiftCardCode.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardCode
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $template_id 模板ID
|
||||
* @property GiftCardTemplate $template 关联模板
|
||||
* @property string $code 兑换码
|
||||
* @property string|null $batch_id 批次ID
|
||||
* @property int $status 状态
|
||||
* @property int|null $user_id 使用用户ID
|
||||
* @property int|null $used_at 使用时间
|
||||
* @property int|null $expires_at 过期时间
|
||||
* @property array|null $actual_rewards 实际奖励
|
||||
* @property int $usage_count 使用次数
|
||||
* @property int $max_usage 最大使用次数
|
||||
* @property array|null $metadata 额外数据
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*/
|
||||
class GiftCardCode extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_code';
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 状态常量
|
||||
const STATUS_UNUSED = 0; // 未使用
|
||||
const STATUS_USED = 1; // 已使用
|
||||
const STATUS_EXPIRED = 2; // 已过期
|
||||
const STATUS_DISABLED = 3; // 已禁用
|
||||
|
||||
protected $fillable = [
|
||||
'template_id',
|
||||
'code',
|
||||
'batch_id',
|
||||
'status',
|
||||
'user_id',
|
||||
'used_at',
|
||||
'expires_at',
|
||||
'actual_rewards',
|
||||
'usage_count',
|
||||
'max_usage',
|
||||
'metadata'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'used_at' => 'timestamp',
|
||||
'expires_at' => 'timestamp',
|
||||
'actual_rewards' => 'array',
|
||||
'metadata' => 'array'
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取状态映射
|
||||
*/
|
||||
public static function getStatusMap(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_UNUSED => '未使用',
|
||||
self::STATUS_USED => '已使用',
|
||||
self::STATUS_EXPIRED => '已过期',
|
||||
self::STATUS_DISABLED => '已禁用',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态名称
|
||||
*/
|
||||
public function getStatusNameAttribute(): string
|
||||
{
|
||||
return self::getStatusMap()[$this->status] ?? '未知状态';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联礼品卡模板
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用记录
|
||||
*/
|
||||
public function usages(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'code_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可用
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
// 检查状态
|
||||
if (in_array($this->status, [self::STATUS_EXPIRED, self::STATUS_DISABLED])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if ($this->expires_at && $this->expires_at < time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查使用次数
|
||||
if ($this->usage_count >= $this->max_usage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已过期
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已使用
|
||||
*/
|
||||
public function markAsUsed(User $user): bool
|
||||
{
|
||||
$this->status = self::STATUS_USED;
|
||||
$this->user_id = $user->id;
|
||||
$this->used_at = time();
|
||||
$this->usage_count += 1;
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已过期
|
||||
*/
|
||||
public function markAsExpired(): bool
|
||||
{
|
||||
$this->status = self::STATUS_EXPIRED;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已禁用
|
||||
*/
|
||||
public function markAsDisabled(): bool
|
||||
{
|
||||
$this->status = self::STATUS_DISABLED;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成兑换码
|
||||
*/
|
||||
public static function generateCode(string $prefix = 'GC'): string
|
||||
{
|
||||
do {
|
||||
$safePrefix = (string) $prefix;
|
||||
$code = $safePrefix . strtoupper(substr(md5(uniqid($safePrefix . mt_rand(), true)), 0, 12));
|
||||
} while (self::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量生成兑换码
|
||||
*/
|
||||
public static function batchGenerate(int $templateId, int $count, array $options = []): string
|
||||
{
|
||||
$batchId = uniqid('batch_');
|
||||
$prefix = $options['prefix'] ?? 'GC';
|
||||
$expiresAt = $options['expires_at'] ?? null;
|
||||
$maxUsage = $options['max_usage'] ?? 1;
|
||||
|
||||
$codes = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$codes[] = [
|
||||
'template_id' => $templateId,
|
||||
'code' => self::generateCode($prefix),
|
||||
'batch_id' => $batchId,
|
||||
'status' => self::STATUS_UNUSED,
|
||||
'expires_at' => $expiresAt,
|
||||
'max_usage' => $maxUsage,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
self::insert($codes);
|
||||
|
||||
return $batchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实际奖励(用于盲盒等)
|
||||
*/
|
||||
public function setActualRewards(array $rewards): bool
|
||||
{
|
||||
$this->actual_rewards = $rewards;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实际奖励
|
||||
*/
|
||||
public function getActualRewards(): array
|
||||
{
|
||||
return $this->actual_rewards ?? $this->template->rewards ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查兑换码格式
|
||||
*/
|
||||
public static function validateCodeFormat(string $code): bool
|
||||
{
|
||||
// 基本格式验证:字母数字组合,长度8-32
|
||||
return preg_match('/^[A-Z0-9]{8,32}$/', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据批次ID获取兑换码
|
||||
*/
|
||||
public static function getByBatchId(string $batchId)
|
||||
{
|
||||
return self::where('batch_id', $batchId)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期兑换码
|
||||
*/
|
||||
public static function cleanupExpired(): int
|
||||
{
|
||||
$count = self::where('status', self::STATUS_UNUSED)
|
||||
->where('expires_at', '<', time())
|
||||
->count();
|
||||
|
||||
self::where('status', self::STATUS_UNUSED)
|
||||
->where('expires_at', '<', time())
|
||||
->update(['status' => self::STATUS_EXPIRED]);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
254
Xboard/app/Models/GiftCardTemplate.php
Normal file
254
Xboard/app/Models/GiftCardTemplate.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Dflydev\DotAccessData\Data;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardTemplate
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 礼品卡名称
|
||||
* @property string|null $description 礼品卡描述
|
||||
* @property int $type 卡片类型
|
||||
* @property boolean $status 状态
|
||||
* @property array|null $conditions 使用条件配置
|
||||
* @property array $rewards 奖励配置
|
||||
* @property array|null $limits 限制条件
|
||||
* @property array|null $special_config 特殊配置
|
||||
* @property string|null $icon 卡片图标
|
||||
* @property string $theme_color 主题色
|
||||
* @property int $sort 排序
|
||||
* @property int $admin_id 创建管理员ID
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*/
|
||||
class GiftCardTemplate extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_template';
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 卡片类型常量
|
||||
const TYPE_GENERAL = 1; // 通用礼品卡
|
||||
const TYPE_PLAN = 2; // 套餐礼品卡
|
||||
const TYPE_MYSTERY = 3; // 盲盒礼品卡
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'status',
|
||||
'conditions',
|
||||
'rewards',
|
||||
'limits',
|
||||
'special_config',
|
||||
'icon',
|
||||
'background_image',
|
||||
'theme_color',
|
||||
'sort',
|
||||
'admin_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'conditions' => 'array',
|
||||
'rewards' => 'array',
|
||||
'limits' => 'array',
|
||||
'special_config' => 'array',
|
||||
'status' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取卡片类型映射
|
||||
*/
|
||||
public static function getTypeMap(): array
|
||||
{
|
||||
return [
|
||||
self::TYPE_GENERAL => '通用礼品卡',
|
||||
self::TYPE_PLAN => '套餐礼品卡',
|
||||
self::TYPE_MYSTERY => '盲盒礼品卡',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型名称
|
||||
*/
|
||||
public function getTypeNameAttribute(): string
|
||||
{
|
||||
return self::getTypeMap()[$this->type] ?? '未知类型';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联兑换码
|
||||
*/
|
||||
public function codes(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardCode::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用记录
|
||||
*/
|
||||
public function usages(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联统计数据
|
||||
*/
|
||||
public function stats(): HasMany
|
||||
{
|
||||
return $this->hasMany(GiftCardUsage::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可用
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否满足使用条件
|
||||
*/
|
||||
public function checkUserConditions(User $user): bool
|
||||
{
|
||||
switch ($this->type) {
|
||||
case self::TYPE_GENERAL:
|
||||
$rewards = $this->rewards ?? [];
|
||||
if (isset($rewards['transfer_enable']) || isset($rewards['expire_days']) || isset($rewards['reset_package'])) {
|
||||
if (!$user->plan_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_PLAN:
|
||||
if ($user->isActive()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$conditions = $this->conditions ?? [];
|
||||
|
||||
// 检查新用户条件
|
||||
if (isset($conditions['new_user_only']) && $conditions['new_user_only']) {
|
||||
$maxDays = $conditions['new_user_max_days'] ?? 7;
|
||||
if ($user->created_at < (time() - ($maxDays * 86400))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查付费用户条件
|
||||
if (isset($conditions['paid_user_only']) && $conditions['paid_user_only']) {
|
||||
$paidOrderExists = $user->orders()->where('status', Order::STATUS_COMPLETED)->exists();
|
||||
if (!$paidOrderExists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查允许的套餐
|
||||
if (isset($conditions['allowed_plans']) && $user->plan_id) {
|
||||
if (!in_array($user->plan_id, $conditions['allowed_plans'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要邀请人
|
||||
if (isset($conditions['require_invite']) && $conditions['require_invite']) {
|
||||
if (!$user->invite_user_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算实际奖励
|
||||
*/
|
||||
public function calculateActualRewards(User $user): array
|
||||
{
|
||||
$baseRewards = $this->rewards;
|
||||
$actualRewards = $baseRewards;
|
||||
|
||||
// 处理盲盒随机奖励
|
||||
if ($this->type === self::TYPE_MYSTERY && isset($this->rewards['random_rewards'])) {
|
||||
$randomRewards = $this->rewards['random_rewards'];
|
||||
$totalWeight = array_sum(array_column($randomRewards, 'weight'));
|
||||
$random = mt_rand(1, $totalWeight);
|
||||
$currentWeight = 0;
|
||||
|
||||
foreach ($randomRewards as $reward) {
|
||||
$currentWeight += $reward['weight'];
|
||||
if ($random <= $currentWeight) {
|
||||
$actualRewards = array_merge($actualRewards, $reward);
|
||||
unset($actualRewards['weight']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节日等特殊奖励(通用逻辑)
|
||||
if (isset($this->special_config['festival_bonus'])) {
|
||||
$now = time();
|
||||
$festivalConfig = $this->special_config;
|
||||
|
||||
if (isset($festivalConfig['start_time']) && isset($festivalConfig['end_time'])) {
|
||||
if ($now >= $festivalConfig['start_time'] && $now <= $festivalConfig['end_time']) {
|
||||
$bonus = data_get($festivalConfig, 'festival_bonus', 1.0);
|
||||
if ($bonus > 1.0) {
|
||||
foreach ($actualRewards as $key => &$value) {
|
||||
if (is_numeric($value)) {
|
||||
$value = intval($value * $bonus);
|
||||
}
|
||||
}
|
||||
unset($value); // 解除引用
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $actualRewards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查使用频率限制
|
||||
*/
|
||||
public function checkUsageLimit(User $user): bool
|
||||
{
|
||||
$limits = $this->limits ?? [];
|
||||
|
||||
// 检查每用户最大使用次数
|
||||
if (isset($limits['max_use_per_user'])) {
|
||||
$usedCount = $this->usages()
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
if ($usedCount >= $limits['max_use_per_user']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查冷却时间
|
||||
if (isset($limits['cooldown_hours'])) {
|
||||
$lastUsage = $this->usages()
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastUsage && isset($lastUsage->created_at)) {
|
||||
$cooldownTime = $lastUsage->created_at + ($limits['cooldown_hours'] * 3600);
|
||||
if (time() < $cooldownTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
112
Xboard/app/Models/GiftCardUsage.php
Normal file
112
Xboard/app/Models/GiftCardUsage.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\GiftCardUsage
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $code_id 兑换码ID
|
||||
* @property int $template_id 模板ID
|
||||
* @property int $user_id 使用用户ID
|
||||
* @property int|null $invite_user_id 邀请人ID
|
||||
* @property array $rewards_given 实际发放的奖励
|
||||
* @property array|null $invite_rewards 邀请人获得的奖励
|
||||
* @property int|null $user_level_at_use 使用时用户等级
|
||||
* @property int|null $plan_id_at_use 使用时用户套餐ID
|
||||
* @property float $multiplier_applied 应用的倍率
|
||||
* @property string|null $ip_address 使用IP地址
|
||||
* @property string|null $user_agent 用户代理
|
||||
* @property string|null $notes 备注
|
||||
* @property int $created_at
|
||||
*/
|
||||
class GiftCardUsage extends Model
|
||||
{
|
||||
protected $table = 'v2_gift_card_usage';
|
||||
protected $dateFormat = 'U';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'code_id',
|
||||
'template_id',
|
||||
'user_id',
|
||||
'invite_user_id',
|
||||
'rewards_given',
|
||||
'invite_rewards',
|
||||
'user_level_at_use',
|
||||
'plan_id_at_use',
|
||||
'multiplier_applied',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'notes',
|
||||
'created_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'rewards_given' => 'array',
|
||||
'invite_rewards' => 'array',
|
||||
'multiplier_applied' => 'float'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联兑换码
|
||||
*/
|
||||
public function code(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardCode::class, 'code_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联模板
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GiftCardTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联使用用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联邀请人
|
||||
*/
|
||||
public function inviteUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invite_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用记录
|
||||
*/
|
||||
public static function createRecord(
|
||||
GiftCardCode $code,
|
||||
User $user,
|
||||
array $rewards,
|
||||
array $options = []
|
||||
): self {
|
||||
return self::create([
|
||||
'code_id' => $code->id,
|
||||
'template_id' => $code->template_id,
|
||||
'user_id' => $user->id,
|
||||
'invite_user_id' => $user->invite_user_id,
|
||||
'rewards_given' => $rewards,
|
||||
'invite_rewards' => $options['invite_rewards'] ?? null,
|
||||
'user_level_at_use' => $user->plan ? $user->plan->sort : null,
|
||||
'plan_id_at_use' => $user->plan_id,
|
||||
'multiplier_applied' => $options['multiplier'] ?? 1.0,
|
||||
// 'ip_address' => $options['ip_address'] ?? null,
|
||||
'user_agent' => $options['user_agent'] ?? null,
|
||||
'notes' => $options['notes'] ?? null,
|
||||
'created_at' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
Xboard/app/Models/InviteCode.php
Normal file
19
Xboard/app/Models/InviteCode.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InviteCode extends Model
|
||||
{
|
||||
protected $table = 'v2_invite_code';
|
||||
protected $dateFormat = 'U';
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'status' => 'boolean',
|
||||
];
|
||||
|
||||
const STATUS_UNUSED = 0;
|
||||
const STATUS_USED = 1;
|
||||
}
|
||||
17
Xboard/app/Models/Knowledge.php
Normal file
17
Xboard/app/Models/Knowledge.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Knowledge extends Model
|
||||
{
|
||||
protected $table = 'v2_knowledge';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'show' => 'boolean',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
];
|
||||
}
|
||||
16
Xboard/app/Models/MailLog.php
Normal file
16
Xboard/app/Models/MailLog.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MailLog extends Model
|
||||
{
|
||||
protected $table = 'v2_mail_log';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
18
Xboard/app/Models/Notice.php
Normal file
18
Xboard/app/Models/Notice.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Notice extends Model
|
||||
{
|
||||
protected $table = 'v2_notice';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'tags' => 'array',
|
||||
'show' => 'boolean',
|
||||
];
|
||||
}
|
||||
120
Xboard/app/Models/Order.php
Normal file
120
Xboard/app/Models/Order.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\Order
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property int $plan_id
|
||||
* @property int|null $payment_id
|
||||
* @property string $period
|
||||
* @property string $trade_no
|
||||
* @property int $total_amount
|
||||
* @property int|null $handling_amount
|
||||
* @property int|null $balance_amount
|
||||
* @property int|null $refund_amount
|
||||
* @property int|null $surplus_amount
|
||||
* @property int $type
|
||||
* @property int $status
|
||||
* @property array|null $surplus_order_ids
|
||||
* @property int|null $coupon_id
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property int|null $commission_status
|
||||
* @property int|null $invite_user_id
|
||||
* @property int|null $actual_commission_balance
|
||||
* @property int|null $commission_rate
|
||||
* @property int|null $commission_auto_check
|
||||
* @property int|null $commission_balance
|
||||
* @property int|null $discount_amount
|
||||
* @property int|null $paid_at
|
||||
* @property string|null $callback_no
|
||||
*
|
||||
* @property-read Plan $plan
|
||||
* @property-read Payment|null $payment
|
||||
* @property-read User $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, CommissionLog> $commission_log
|
||||
*/
|
||||
class Order extends Model
|
||||
{
|
||||
protected $table = 'v2_order';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'surplus_order_ids' => 'array',
|
||||
'handling_amount' => 'integer'
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 0; // 待支付
|
||||
const STATUS_PROCESSING = 1; // 开通中
|
||||
const STATUS_CANCELLED = 2; // 已取消
|
||||
const STATUS_COMPLETED = 3; // 已完成
|
||||
const STATUS_DISCOUNTED = 4; // 已折抵
|
||||
|
||||
public static $statusMap = [
|
||||
self::STATUS_PENDING => '待支付',
|
||||
self::STATUS_PROCESSING => '开通中',
|
||||
self::STATUS_CANCELLED => '已取消',
|
||||
self::STATUS_COMPLETED => '已完成',
|
||||
self::STATUS_DISCOUNTED => '已折抵',
|
||||
];
|
||||
|
||||
const TYPE_NEW_PURCHASE = 1; // 新购
|
||||
const TYPE_RENEWAL = 2; // 续费
|
||||
const TYPE_UPGRADE = 3; // 升级
|
||||
const TYPE_RESET_TRAFFIC = 4; //流量重置包
|
||||
public static $typeMap = [
|
||||
self::TYPE_NEW_PURCHASE => '新购',
|
||||
self::TYPE_RENEWAL => '续费',
|
||||
self::TYPE_UPGRADE => '升级',
|
||||
self::TYPE_RESET_TRAFFIC => '流量重置',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取与订单关联的支付方式
|
||||
*/
|
||||
public function payment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Payment::class, 'payment_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与订单关联的用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请人
|
||||
*/
|
||||
public function invite_user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invite_user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与订单关联的套餐
|
||||
*/
|
||||
public function plan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Plan::class, 'plan_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与订单关联的佣金记录
|
||||
*/
|
||||
public function commission_log(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no');
|
||||
}
|
||||
}
|
||||
18
Xboard/app/Models/Payment.php
Normal file
18
Xboard/app/Models/Payment.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
protected $table = 'v2_payment';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'config' => 'array',
|
||||
'enable' => 'boolean'
|
||||
];
|
||||
}
|
||||
353
Xboard/app/Models/Plan.php
Normal file
353
Xboard/app/Models/Plan.php
Normal file
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use InvalidArgumentException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* App\Models\Plan
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 套餐名称
|
||||
* @property int|null $group_id 权限组ID
|
||||
* @property int $transfer_enable 流量(KB)
|
||||
* @property int|null $speed_limit 速度限制Mbps
|
||||
* @property bool $show 是否显示
|
||||
* @property bool $renew 是否允许续费
|
||||
* @property bool $sell 是否允许购买
|
||||
* @property array|null $prices 价格配置
|
||||
* @property array|null $tags 标签
|
||||
* @property int $sort 排序
|
||||
* @property string|null $content 套餐描述
|
||||
* @property int|null $reset_traffic_method 流量重置方式
|
||||
* @property int|null $capacity_limit 订阅人数限制
|
||||
* @property int|null $device_limit 设备数量限制
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*
|
||||
* @property-read ServerGroup|null $group 关联的权限组
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, Order> $order 关联的订单
|
||||
*/
|
||||
class Plan extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'v2_plan';
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 定义流量重置方式
|
||||
public const RESET_TRAFFIC_FOLLOW_SYSTEM = null; // 跟随系统设置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_MONTH = 0; // 每月1号
|
||||
public const RESET_TRAFFIC_MONTHLY = 1; // 按月重置
|
||||
public const RESET_TRAFFIC_NEVER = 2; // 不重置
|
||||
public const RESET_TRAFFIC_FIRST_DAY_YEAR = 3; // 每年1月1日
|
||||
public const RESET_TRAFFIC_YEARLY = 4; // 按年重置
|
||||
|
||||
// 定义价格类型
|
||||
public const PRICE_TYPE_RESET_TRAFFIC = 'reset_traffic'; // 重置流量价格
|
||||
|
||||
// 定义可用的订阅周期
|
||||
public const PERIOD_MONTHLY = 'monthly';
|
||||
public const PERIOD_QUARTERLY = 'quarterly';
|
||||
public const PERIOD_HALF_YEARLY = 'half_yearly';
|
||||
public const PERIOD_YEARLY = 'yearly';
|
||||
public const PERIOD_TWO_YEARLY = 'two_yearly';
|
||||
public const PERIOD_THREE_YEARLY = 'three_yearly';
|
||||
public const PERIOD_ONETIME = 'onetime';
|
||||
public const PERIOD_RESET_TRAFFIC = 'reset_traffic';
|
||||
|
||||
// 定义旧版周期映射
|
||||
public const LEGACY_PERIOD_MAPPING = [
|
||||
'month_price' => self::PERIOD_MONTHLY,
|
||||
'quarter_price' => self::PERIOD_QUARTERLY,
|
||||
'half_year_price' => self::PERIOD_HALF_YEARLY,
|
||||
'year_price' => self::PERIOD_YEARLY,
|
||||
'two_year_price' => self::PERIOD_TWO_YEARLY,
|
||||
'three_year_price' => self::PERIOD_THREE_YEARLY,
|
||||
'onetime_price' => self::PERIOD_ONETIME,
|
||||
'reset_price' => self::PERIOD_RESET_TRAFFIC
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'transfer_enable',
|
||||
'name',
|
||||
'speed_limit',
|
||||
'show',
|
||||
'sort',
|
||||
'renew',
|
||||
'content',
|
||||
'prices',
|
||||
'reset_traffic_method',
|
||||
'capacity_limit',
|
||||
'sell',
|
||||
'device_limit',
|
||||
'tags'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'show' => 'boolean',
|
||||
'renew' => 'boolean',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'group_id' => 'integer',
|
||||
'prices' => 'array',
|
||||
'tags' => 'array',
|
||||
'reset_traffic_method' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取所有可用的流量重置方式
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getResetTrafficMethods(): array
|
||||
{
|
||||
return [
|
||||
self::RESET_TRAFFIC_FOLLOW_SYSTEM => '跟随系统设置',
|
||||
self::RESET_TRAFFIC_FIRST_DAY_MONTH => '每月1号',
|
||||
self::RESET_TRAFFIC_MONTHLY => '按月重置',
|
||||
self::RESET_TRAFFIC_NEVER => '不重置',
|
||||
self::RESET_TRAFFIC_FIRST_DAY_YEAR => '每年1月1日',
|
||||
self::RESET_TRAFFIC_YEARLY => '按年重置',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的订阅周期
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAvailablePeriods(): array
|
||||
{
|
||||
return [
|
||||
self::PERIOD_MONTHLY => [
|
||||
'name' => '月付',
|
||||
'days' => 30,
|
||||
'value' => 1
|
||||
],
|
||||
self::PERIOD_QUARTERLY => [
|
||||
'name' => '季付',
|
||||
'days' => 90,
|
||||
'value' => 3
|
||||
],
|
||||
self::PERIOD_HALF_YEARLY => [
|
||||
'name' => '半年付',
|
||||
'days' => 180,
|
||||
'value' => 6
|
||||
],
|
||||
self::PERIOD_YEARLY => [
|
||||
'name' => '年付',
|
||||
'days' => 365,
|
||||
'value' => 12
|
||||
],
|
||||
self::PERIOD_TWO_YEARLY => [
|
||||
'name' => '两年付',
|
||||
'days' => 730,
|
||||
'value' => 24
|
||||
],
|
||||
self::PERIOD_THREE_YEARLY => [
|
||||
'name' => '三年付',
|
||||
'days' => 1095,
|
||||
'value' => 36
|
||||
],
|
||||
self::PERIOD_ONETIME => [
|
||||
'name' => '一次性',
|
||||
'days' => -1,
|
||||
'value' => -1
|
||||
],
|
||||
self::PERIOD_RESET_TRAFFIC => [
|
||||
'name' => '重置流量',
|
||||
'days' => -1,
|
||||
'value' => -1
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定周期的价格
|
||||
*
|
||||
* @param string $period
|
||||
* @return int|null
|
||||
*/
|
||||
public function getPriceByPeriod(string $period): ?int
|
||||
{
|
||||
return $this->prices[$period] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已设置价格的周期
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getActivePeriods(): array
|
||||
{
|
||||
return array_filter(
|
||||
self::getAvailablePeriods(),
|
||||
fn($period) => isset($this->prices[$period])
|
||||
&& $this->prices[$period] > 0,
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定周期的价格
|
||||
*
|
||||
* @param string $period
|
||||
* @param int $price
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setPeriodPrice(string $period, int $price): void
|
||||
{
|
||||
if (!array_key_exists($period, self::getAvailablePeriods())) {
|
||||
throw new InvalidArgumentException("Invalid period: {$period}");
|
||||
}
|
||||
|
||||
$prices = $this->prices ?? [];
|
||||
$prices[$period] = $price;
|
||||
$this->prices = $prices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定周期的价格
|
||||
*
|
||||
* @param string $period
|
||||
* @return void
|
||||
*/
|
||||
public function removePeriodPrice(string $period): void
|
||||
{
|
||||
$prices = $this->prices ?? [];
|
||||
unset($prices[$period]);
|
||||
$this->prices = $prices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有价格及其对应的周期信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPriceList(): array
|
||||
{
|
||||
$prices = $this->prices ?? [];
|
||||
$periods = self::getAvailablePeriods();
|
||||
|
||||
$priceList = [];
|
||||
foreach ($prices as $period => $price) {
|
||||
if (isset($periods[$period]) && $price > 0) {
|
||||
$priceList[$period] = [
|
||||
'period' => $periods[$period],
|
||||
'price' => $price,
|
||||
'average_price' => $periods[$period]['value'] > 0
|
||||
? round($price / $periods[$period]['value'], 2)
|
||||
: $price
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $priceList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以重置流量
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canResetTraffic(): bool
|
||||
{
|
||||
return $this->reset_traffic_method !== self::RESET_TRAFFIC_NEVER
|
||||
&& $this->getResetTrafficPrice() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重置流量的价格
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getResetTrafficPrice(): int
|
||||
{
|
||||
return $this->prices[self::PRICE_TYPE_RESET_TRAFFIC] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算指定周期的有效天数
|
||||
*
|
||||
* @param string $period
|
||||
* @return int -1表示永久有效
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function getPeriodDays(string $period): int
|
||||
{
|
||||
$periods = self::getAvailablePeriods();
|
||||
if (!isset($periods[$period])) {
|
||||
throw new InvalidArgumentException("Invalid period: {$period}");
|
||||
}
|
||||
|
||||
return $periods[$period]['days'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查周期是否有效
|
||||
*
|
||||
* @param string $period
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidPeriod(string $period): bool
|
||||
{
|
||||
return array_key_exists($period, self::getAvailablePeriods());
|
||||
}
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function group(): HasOne
|
||||
{
|
||||
return $this->hasOne(ServerGroup::class, 'id', 'group_id');
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置流量重置方式
|
||||
*
|
||||
* @param int $method
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setResetTrafficMethod(int $method): void
|
||||
{
|
||||
if (!array_key_exists($method, self::getResetTrafficMethods())) {
|
||||
throw new InvalidArgumentException("Invalid reset traffic method: {$method}");
|
||||
}
|
||||
|
||||
$this->reset_traffic_method = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置重置流量价格
|
||||
*
|
||||
* @param int $price
|
||||
* @return void
|
||||
*/
|
||||
public function setResetTrafficPrice(int $price): void
|
||||
{
|
||||
$prices = $this->prices ?? [];
|
||||
$prices[self::PRICE_TYPE_RESET_TRAFFIC] = max(0, $price);
|
||||
$this->prices = $prices;
|
||||
}
|
||||
|
||||
public function order(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
}
|
||||
77
Xboard/app/Models/Plugin.php
Normal file
77
Xboard/app/Models/Plugin.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $code
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $version
|
||||
* @property string $author
|
||||
* @property string $url
|
||||
* @property string $email
|
||||
* @property string $license
|
||||
* @property string $requires
|
||||
* @property string $config
|
||||
* @property string $type
|
||||
* @property boolean $is_enabled
|
||||
*/
|
||||
class Plugin extends Model
|
||||
{
|
||||
protected $table = 'v2_plugins';
|
||||
|
||||
const TYPE_FEATURE = 'feature';
|
||||
const TYPE_PAYMENT = 'payment';
|
||||
|
||||
// 默认不可删除的插件列表
|
||||
const PROTECTED_PLUGINS = [
|
||||
'epay', // EPay
|
||||
'alipay_f2f', // Alipay F2F
|
||||
'btcpay', // BTCPay
|
||||
'coinbase', // Coinbase
|
||||
'coin_payments', // CoinPayments
|
||||
'mgate', // MGate
|
||||
'telegram', // Telegram
|
||||
];
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeByType(Builder $query, string $type): Builder
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
public function isFeaturePlugin(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_FEATURE;
|
||||
}
|
||||
|
||||
public function isPaymentPlugin(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_PAYMENT;
|
||||
}
|
||||
|
||||
public function isProtected(): bool
|
||||
{
|
||||
return in_array($this->code, self::PROTECTED_PLUGINS);
|
||||
}
|
||||
|
||||
public function canBeDeleted(): bool
|
||||
{
|
||||
return !$this->isProtected();
|
||||
}
|
||||
|
||||
}
|
||||
563
Xboard/app/Models/Server.php
Normal file
563
Xboard/app/Models/Server.php
Normal file
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
/**
|
||||
* App\Models\Server
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 节点名称
|
||||
* @property string $type 服务类型
|
||||
* @property string $host 主机地址
|
||||
* @property string|int $port 端口
|
||||
* @property int|null $server_port 服务器端口
|
||||
* @property array|null $group_ids 分组IDs
|
||||
* @property array|null $route_ids 路由IDs
|
||||
* @property array|null $tags 标签
|
||||
* @property boolean $show 是否显示
|
||||
* @property string|null $allow_insecure 是否允许不安全
|
||||
* @property string|null $network 网络类型
|
||||
* @property int|null $parent_id 父节点ID
|
||||
* @property float|null $rate 倍率
|
||||
* @property boolean $rate_time_enable 是否启用时间范围功能
|
||||
* @property array|null $rate_time_ranges 倍率时间范围
|
||||
* @property int|null $sort 排序
|
||||
* @property array|null $protocol_settings 协议设置
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*
|
||||
* @property-read Server|null $parent 父节点
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, StatServer> $stats 节点统计
|
||||
*
|
||||
* @property-read int|null $last_check_at 最后检查时间(Unix时间戳)
|
||||
* @property-read int|null $last_push_at 最后推送时间(Unix时间戳)
|
||||
* @property-read int $online 在线用户数
|
||||
* @property-read int $online_conn 在线连接数
|
||||
* @property-read array|null $metrics 节点指标指标
|
||||
* @property-read int $is_online 是否在线(1在线 0离线)
|
||||
* @property-read string $available_status 可用状态描述
|
||||
* @property-read string $cache_key 缓存键
|
||||
* @property string|null $ports 端口范围
|
||||
* @property string|null $password 密码
|
||||
* @property int|null $u 上行流量
|
||||
* @property int|null $d 下行流量
|
||||
* @property int|null $total 总流量
|
||||
* @property-read array|null $load_status 负载状态(包含CPU、内存、交换区、磁盘信息)
|
||||
*
|
||||
* @property int $transfer_enable 流量上限,0或者null表示不限制
|
||||
* @property int $u 当前上传流量
|
||||
* @property int $d 当前下载流量
|
||||
*/
|
||||
class Server extends Model
|
||||
{
|
||||
public const TYPE_HYSTERIA = 'hysteria';
|
||||
public const TYPE_VLESS = 'vless';
|
||||
public const TYPE_TROJAN = 'trojan';
|
||||
public const TYPE_VMESS = 'vmess';
|
||||
public const TYPE_TUIC = 'tuic';
|
||||
public const TYPE_SHADOWSOCKS = 'shadowsocks';
|
||||
public const TYPE_ANYTLS = 'anytls';
|
||||
public const TYPE_SOCKS = 'socks';
|
||||
public const TYPE_NAIVE = 'naive';
|
||||
public const TYPE_HTTP = 'http';
|
||||
public const TYPE_MIERU = 'mieru';
|
||||
public const STATUS_OFFLINE = 0;
|
||||
public const STATUS_ONLINE_NO_PUSH = 1;
|
||||
public const STATUS_ONLINE = 2;
|
||||
|
||||
public const CHECK_INTERVAL = 300; // 5 minutes in seconds
|
||||
|
||||
private const CIPHER_CONFIGURATIONS = [
|
||||
'2022-blake3-aes-128-gcm' => [
|
||||
'serverKeySize' => 16,
|
||||
'userKeySize' => 16,
|
||||
],
|
||||
'2022-blake3-aes-256-gcm' => [
|
||||
'serverKeySize' => 32,
|
||||
'userKeySize' => 32,
|
||||
],
|
||||
'2022-blake3-chacha20-poly1305' => [
|
||||
'serverKeySize' => 32,
|
||||
'userKeySize' => 32,
|
||||
]
|
||||
];
|
||||
|
||||
public const TYPE_ALIASES = [
|
||||
'v2ray' => self::TYPE_VMESS,
|
||||
'hysteria2' => self::TYPE_HYSTERIA,
|
||||
];
|
||||
|
||||
public const VALID_TYPES = [
|
||||
self::TYPE_HYSTERIA,
|
||||
self::TYPE_VLESS,
|
||||
self::TYPE_TROJAN,
|
||||
self::TYPE_VMESS,
|
||||
self::TYPE_TUIC,
|
||||
self::TYPE_SHADOWSOCKS,
|
||||
self::TYPE_ANYTLS,
|
||||
self::TYPE_SOCKS,
|
||||
self::TYPE_NAIVE,
|
||||
self::TYPE_HTTP,
|
||||
self::TYPE_MIERU,
|
||||
];
|
||||
|
||||
protected $table = 'v2_server';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'group_ids' => 'array',
|
||||
'route_ids' => 'array',
|
||||
'tags' => 'array',
|
||||
'protocol_settings' => 'array',
|
||||
'custom_outbounds' => 'array',
|
||||
'custom_routes' => 'array',
|
||||
'cert_config' => 'array',
|
||||
'last_check_at' => 'integer',
|
||||
'last_push_at' => 'integer',
|
||||
'show' => 'boolean',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'rate_time_ranges' => 'array',
|
||||
'rate_time_enable' => 'boolean',
|
||||
'transfer_enable' => 'integer',
|
||||
'u' => 'integer',
|
||||
'd' => 'integer',
|
||||
];
|
||||
|
||||
private const MULTIPLEX_CONFIGURATION = [
|
||||
'multiplex' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||
'protocol' => ['type' => 'string', 'default' => 'yamux'],
|
||||
'max_connections' => ['type' => 'integer', 'default' => null],
|
||||
// 'min_streams' => ['type' => 'integer', 'default' => null],
|
||||
// 'max_streams' => ['type' => 'integer', 'default' => null],
|
||||
'padding' => ['type' => 'boolean', 'default' => false],
|
||||
'brutal' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||
'up_mbps' => ['type' => 'integer', 'default' => null],
|
||||
'down_mbps' => ['type' => 'integer', 'default' => null],
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
private const REALITY_CONFIGURATION = [
|
||||
'reality_settings' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'server_name' => ['type' => 'string', 'default' => null],
|
||||
'server_port' => ['type' => 'string', 'default' => null],
|
||||
'public_key' => ['type' => 'string', 'default' => null],
|
||||
'private_key' => ['type' => 'string', 'default' => null],
|
||||
'short_id' => ['type' => 'string', 'default' => null],
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
private const UTLS_CONFIGURATION = [
|
||||
'utls' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||
'fingerprint' => ['type' => 'string', 'default' => 'chrome'],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
private const PROTOCOL_CONFIGURATIONS = [
|
||||
self::TYPE_TROJAN => [
|
||||
'tls' => ['type' => 'integer', 'default' => 1],
|
||||
'network' => ['type' => 'string', 'default' => null],
|
||||
'network_settings' => ['type' => 'array', 'default' => null],
|
||||
'server_name' => ['type' => 'string', 'default' => null],
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
||||
...self::REALITY_CONFIGURATION,
|
||||
...self::MULTIPLEX_CONFIGURATION,
|
||||
...self::UTLS_CONFIGURATION
|
||||
],
|
||||
self::TYPE_VMESS => [
|
||||
'tls' => ['type' => 'integer', 'default' => 0],
|
||||
'network' => ['type' => 'string', 'default' => null],
|
||||
'rules' => ['type' => 'array', 'default' => null],
|
||||
'network_settings' => ['type' => 'array', 'default' => null],
|
||||
'tls_settings' => ['type' => 'array', 'default' => null],
|
||||
...self::MULTIPLEX_CONFIGURATION,
|
||||
...self::UTLS_CONFIGURATION
|
||||
],
|
||||
self::TYPE_VLESS => [
|
||||
'tls' => ['type' => 'integer', 'default' => 0],
|
||||
'tls_settings' => ['type' => 'array', 'default' => null],
|
||||
'flow' => ['type' => 'string', 'default' => null],
|
||||
'encryption' => [
|
||||
'type' => 'object',
|
||||
'default' => null,
|
||||
'fields' => [
|
||||
'enabled' => ['type' => 'boolean', 'default' => false],
|
||||
'encryption' => ['type' => 'string', 'default' => null], // 客户端公钥
|
||||
'decryption' => ['type' => 'string', 'default' => null], // 服务端私钥
|
||||
]
|
||||
],
|
||||
'network' => ['type' => 'string', 'default' => null],
|
||||
'network_settings' => ['type' => 'array', 'default' => null],
|
||||
...self::REALITY_CONFIGURATION,
|
||||
...self::MULTIPLEX_CONFIGURATION,
|
||||
...self::UTLS_CONFIGURATION
|
||||
],
|
||||
self::TYPE_SHADOWSOCKS => [
|
||||
'cipher' => ['type' => 'string', 'default' => null],
|
||||
'obfs' => ['type' => 'string', 'default' => null],
|
||||
'obfs_settings' => ['type' => 'array', 'default' => null],
|
||||
'plugin' => ['type' => 'string', 'default' => null],
|
||||
'plugin_opts' => ['type' => 'string', 'default' => null]
|
||||
],
|
||||
self::TYPE_HYSTERIA => [
|
||||
'version' => ['type' => 'integer', 'default' => 2],
|
||||
'bandwidth' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'up' => ['type' => 'integer', 'default' => null],
|
||||
'down' => ['type' => 'integer', 'default' => null]
|
||||
]
|
||||
],
|
||||
'obfs' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'open' => ['type' => 'boolean', 'default' => false],
|
||||
'type' => ['type' => 'string', 'default' => 'salamander'],
|
||||
'password' => ['type' => 'string', 'default' => null]
|
||||
]
|
||||
],
|
||||
'tls' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'server_name' => ['type' => 'string', 'default' => null],
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false]
|
||||
]
|
||||
],
|
||||
'hop_interval' => ['type' => 'integer', 'default' => null]
|
||||
],
|
||||
self::TYPE_TUIC => [
|
||||
'version' => ['type' => 'integer', 'default' => 5],
|
||||
'congestion_control' => ['type' => 'string', 'default' => 'cubic'],
|
||||
'alpn' => ['type' => 'array', 'default' => ['h3']],
|
||||
'udp_relay_mode' => ['type' => 'string', 'default' => 'native'],
|
||||
'tls' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'server_name' => ['type' => 'string', 'default' => null],
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false]
|
||||
]
|
||||
]
|
||||
],
|
||||
self::TYPE_ANYTLS => [
|
||||
'padding_scheme' => [
|
||||
'type' => 'array',
|
||||
'default' => [
|
||||
"stop=8",
|
||||
"0=30-30",
|
||||
"1=100-400",
|
||||
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
|
||||
"3=9-9,500-1000",
|
||||
"4=500-1000",
|
||||
"5=500-1000",
|
||||
"6=500-1000",
|
||||
"7=500-1000"
|
||||
]
|
||||
],
|
||||
'tls' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'server_name' => ['type' => 'string', 'default' => null],
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false]
|
||||
]
|
||||
]
|
||||
],
|
||||
self::TYPE_SOCKS => [
|
||||
'tls' => ['type' => 'integer', 'default' => 0],
|
||||
'tls_settings' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false]
|
||||
]
|
||||
]
|
||||
],
|
||||
self::TYPE_NAIVE => [
|
||||
'tls' => ['type' => 'integer', 'default' => 0],
|
||||
'tls_settings' => ['type' => 'array', 'default' => null]
|
||||
],
|
||||
self::TYPE_HTTP => [
|
||||
'tls' => ['type' => 'integer', 'default' => 0],
|
||||
'tls_settings' => [
|
||||
'type' => 'object',
|
||||
'fields' => [
|
||||
'allow_insecure' => ['type' => 'boolean', 'default' => false],
|
||||
'server_name' => ['type' => 'string', 'default' => null]
|
||||
]
|
||||
]
|
||||
],
|
||||
self::TYPE_MIERU => [
|
||||
'transport' => ['type' => 'string', 'default' => 'TCP'],
|
||||
'traffic_pattern' => ['type' => 'string', 'default' => ''],
|
||||
...self::MULTIPLEX_CONFIGURATION,
|
||||
]
|
||||
];
|
||||
|
||||
private function castValueWithConfig($value, array $config)
|
||||
{
|
||||
if ($value === null && $config['type'] !== 'object') {
|
||||
return $config['default'] ?? null;
|
||||
}
|
||||
|
||||
return match ($config['type']) {
|
||||
'integer' => (int) $value,
|
||||
'boolean' => (bool) $value,
|
||||
'string' => (string) $value,
|
||||
'array' => (array) $value,
|
||||
'object' => is_array($value) ?
|
||||
$this->castSettingsWithConfig($value, $config['fields']) :
|
||||
$config['default'] ?? null,
|
||||
default => $value
|
||||
};
|
||||
}
|
||||
|
||||
private function castSettingsWithConfig(array $settings, array $configs): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($configs as $key => $config) {
|
||||
$value = $settings[$key] ?? null;
|
||||
$result[$key] = $this->castValueWithConfig($value, $config);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getProtocolSettingsAttribute($value)
|
||||
{
|
||||
$settings = json_decode($value, true) ?? [];
|
||||
$configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? [];
|
||||
return $this->castSettingsWithConfig($settings, $configs);
|
||||
}
|
||||
|
||||
public function setProtocolSettingsAttribute($value)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
$configs = self::PROTOCOL_CONFIGURATIONS[$this->type] ?? [];
|
||||
$castedSettings = $this->castSettingsWithConfig($value ?? [], $configs);
|
||||
|
||||
$this->attributes['protocol_settings'] = json_encode($castedSettings);
|
||||
}
|
||||
|
||||
public function generateServerPassword(User $user): string
|
||||
{
|
||||
if ($this->type !== self::TYPE_SHADOWSOCKS) {
|
||||
return $user->uuid;
|
||||
}
|
||||
|
||||
|
||||
$cipher = data_get($this, 'protocol_settings.cipher');
|
||||
if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) {
|
||||
return $user->uuid;
|
||||
}
|
||||
|
||||
$config = self::CIPHER_CONFIGURATIONS[$cipher];
|
||||
// Use parent's created_at if this is a child node
|
||||
$serverCreatedAt = $this->parent_id ? $this->parent->created_at : $this->created_at;
|
||||
$serverKey = Helper::getServerKey($serverCreatedAt, $config['serverKeySize']);
|
||||
$userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']);
|
||||
return "{$serverKey}:{$userKey}";
|
||||
}
|
||||
|
||||
public static function normalizeType(?string $type): string | null
|
||||
{
|
||||
return $type ? strtolower(self::TYPE_ALIASES[$type] ?? $type) : null;
|
||||
}
|
||||
|
||||
public static function isValidType(?string $type): bool
|
||||
{
|
||||
return $type ? in_array(self::normalizeType($type), self::VALID_TYPES, true) : true;
|
||||
}
|
||||
|
||||
public function getAvailableStatusAttribute(): int
|
||||
{
|
||||
$now = time();
|
||||
if (!$this->last_check_at || ($now - self::CHECK_INTERVAL) >= $this->last_check_at) {
|
||||
return self::STATUS_OFFLINE;
|
||||
}
|
||||
if (!$this->last_push_at || ($now - self::CHECK_INTERVAL) >= $this->last_push_at) {
|
||||
return self::STATUS_ONLINE_NO_PUSH;
|
||||
}
|
||||
return self::STATUS_ONLINE;
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
public function stats(): HasMany
|
||||
{
|
||||
return $this->hasMany(StatServer::class, 'server_id', 'id');
|
||||
}
|
||||
|
||||
public function groups()
|
||||
{
|
||||
return ServerGroup::whereIn('id', $this->group_ids)->get();
|
||||
}
|
||||
|
||||
public function routes()
|
||||
{
|
||||
return ServerRoute::whereIn('id', $this->route_ids)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后检查时间访问器
|
||||
*/
|
||||
protected function lastCheckAt(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$type = strtoupper($this->type);
|
||||
$serverId = $this->parent_id ?: $this->id;
|
||||
return Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后推送时间访问器
|
||||
*/
|
||||
protected function lastPushAt(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$type = strtoupper($this->type);
|
||||
$serverId = $this->parent_id ?: $this->id;
|
||||
return Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线用户数访问器
|
||||
*/
|
||||
protected function online(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$type = strtoupper($this->type);
|
||||
$serverId = $this->parent_id ?: $this->id;
|
||||
return Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在线访问器
|
||||
*/
|
||||
protected function isOnline(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return (time() - 300 > $this->last_check_at) ? 0 : 1;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存键访问器
|
||||
*/
|
||||
protected function cacheKey(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器密钥访问器
|
||||
*/
|
||||
protected function serverKey(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if ($this->type === self::TYPE_SHADOWSOCKS) {
|
||||
return Helper::getServerKey($this->created_at, 16);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指标指标访问器
|
||||
*/
|
||||
protected function metrics(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$type = strtoupper($this->type);
|
||||
$serverId = $this->parent_id ?: $this->id;
|
||||
return Cache::get(CacheKey::get("SERVER_{$type}_METRICS", $serverId));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线连接数访问器
|
||||
*/
|
||||
protected function onlineConn(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->metrics['active_connections'] ?? 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 负载状态访问器
|
||||
*/
|
||||
protected function loadStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$type = strtoupper($this->type);
|
||||
$serverId = $this->parent_id ?: $this->id;
|
||||
return Cache::get(CacheKey::get("SERVER_{$type}_LOAD_STATUS", $serverId));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getCurrentRate(): float
|
||||
{
|
||||
if (!$this->rate_time_enable) {
|
||||
return (float) $this->rate;
|
||||
}
|
||||
|
||||
$now = now()->format('H:i');
|
||||
$ranges = $this->rate_time_ranges ?? [];
|
||||
$matchedRange = collect($ranges)
|
||||
->first(fn($range) => $now >= $range['start'] && $now <= $range['end']);
|
||||
|
||||
return $matchedRange ? (float) $matchedRange['rate'] : (float) $this->rate;
|
||||
}
|
||||
}
|
||||
46
Xboard/app/Models/ServerGroup.php
Normal file
46
Xboard/app/Models/ServerGroup.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
/**
|
||||
* App\Models\ServerGroup
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name 分组名
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property-read int $server_count 服务器数量
|
||||
*/
|
||||
class ServerGroup extends Model
|
||||
{
|
||||
protected $table = 'v2_server_group';
|
||||
protected $dateFormat = 'U';
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function servers()
|
||||
{
|
||||
return Server::whereJsonContains('group_ids', (string) $this->id)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器数量
|
||||
*/
|
||||
protected function serverCount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => Server::whereJsonContains('group_ids', (string) $this->id)->count(),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
Xboard/app/Models/ServerLog.php
Normal file
16
Xboard/app/Models/ServerLog.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ServerLog extends Model
|
||||
{
|
||||
protected $table = 'v2_server_log';
|
||||
protected $dateFormat = 'U';
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
17
Xboard/app/Models/ServerRoute.php
Normal file
17
Xboard/app/Models/ServerRoute.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ServerRoute extends Model
|
||||
{
|
||||
protected $table = 'v2_server_route';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'match' => 'array'
|
||||
];
|
||||
}
|
||||
16
Xboard/app/Models/ServerStat.php
Normal file
16
Xboard/app/Models/ServerStat.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ServerStat extends Model
|
||||
{
|
||||
protected $table = 'v2_server_stat';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
68
Xboard/app/Models/Setting.php
Normal file
68
Xboard/app/Models/Setting.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $table = 'v2_settings';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'name' => 'string',
|
||||
'value' => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取实际内容值
|
||||
*/
|
||||
public function getContentValue()
|
||||
{
|
||||
$rawValue = $this->attributes['value'] ?? null;
|
||||
|
||||
if ($rawValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果已经是数组,直接返回
|
||||
if (is_array($rawValue)) {
|
||||
return $rawValue;
|
||||
}
|
||||
|
||||
// 如果是数字字符串,返回原值
|
||||
if (is_numeric($rawValue) && !preg_match('/[^\d.]/', $rawValue)) {
|
||||
return $rawValue;
|
||||
}
|
||||
|
||||
// 尝试解析 JSON
|
||||
if (is_string($rawValue)) {
|
||||
$decodedValue = json_decode($rawValue, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decodedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容性:保持原有的 value 访问器
|
||||
*/
|
||||
public function getValueAttribute($value)
|
||||
{
|
||||
return $this->getContentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新设置项
|
||||
*/
|
||||
public static function createOrUpdate(string $name, $value): self
|
||||
{
|
||||
$processedValue = is_array($value) ? json_encode($value) : $value;
|
||||
|
||||
return self::updateOrCreate(
|
||||
['name' => $name],
|
||||
['value' => $processedValue]
|
||||
);
|
||||
}
|
||||
}
|
||||
16
Xboard/app/Models/Stat.php
Normal file
16
Xboard/app/Models/Stat.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Stat extends Model
|
||||
{
|
||||
protected $table = 'v2_stat';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
33
Xboard/app/Models/StatServer.php
Normal file
33
Xboard/app/Models/StatServer.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\Models\StatServer
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $server_id 服务器ID
|
||||
* @property int $u 上行流量
|
||||
* @property int $d 下行流量
|
||||
* @property int $record_at 记录时间
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用
|
||||
*/
|
||||
class StatServer extends Model
|
||||
{
|
||||
protected $table = 'v2_stat_server';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class, 'server_id');
|
||||
}
|
||||
}
|
||||
28
Xboard/app/Models/StatUser.php
Normal file
28
Xboard/app/Models/StatUser.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* App\Models\StatUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id 用户ID
|
||||
* @property int $u 上行流量
|
||||
* @property int $d 下行流量
|
||||
* @property int $record_at 记录时间
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property-read int $value 通过SUM(u + d)计算的总流量值,仅在查询指定时可用
|
||||
*/
|
||||
class StatUser extends Model
|
||||
{
|
||||
protected $table = 'v2_stat_user';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
}
|
||||
46
Xboard/app/Models/SubscribeTemplate.php
Normal file
46
Xboard/app/Models/SubscribeTemplate.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SubscribeTemplate extends Model
|
||||
{
|
||||
protected $table = 'v2_subscribe_templates';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'name' => 'string',
|
||||
'content' => 'string',
|
||||
];
|
||||
|
||||
private static string $cachePrefix = 'subscribe_template:';
|
||||
|
||||
public static function getContent(string $name): ?string
|
||||
{
|
||||
$cacheKey = self::$cachePrefix . $name;
|
||||
|
||||
return Cache::store('redis')->remember($cacheKey, 3600, function () use ($name) {
|
||||
return self::where('name', $name)->value('content');
|
||||
});
|
||||
}
|
||||
|
||||
public static function setContent(string $name, ?string $content): void
|
||||
{
|
||||
self::updateOrCreate(
|
||||
['name' => $name],
|
||||
['content' => $content]
|
||||
);
|
||||
Cache::store('redis')->forget(self::$cachePrefix . $name);
|
||||
}
|
||||
|
||||
public static function getAllContents(): array
|
||||
{
|
||||
return self::pluck('content', 'name')->toArray();
|
||||
}
|
||||
|
||||
public static function flushCache(string $name): void
|
||||
{
|
||||
Cache::store('redis')->forget(self::$cachePrefix . $name);
|
||||
}
|
||||
}
|
||||
60
Xboard/app/Models/Ticket.php
Normal file
60
Xboard/app/Models/Ticket.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\Ticket
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id 用户ID
|
||||
* @property string $subject 工单主题
|
||||
* @property string|null $level 工单等级
|
||||
* @property int $status 工单状态
|
||||
* @property int|null $reply_status 回复状态
|
||||
* @property int|null $last_reply_user_id 最后回复人
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
*
|
||||
* @property-read User $user 关联的用户
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, TicketMessage> $messages 关联的工单消息
|
||||
*/
|
||||
class Ticket extends Model
|
||||
{
|
||||
protected $table = 'v2_ticket';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
|
||||
const STATUS_OPENING = 0;
|
||||
const STATUS_CLOSED = 1;
|
||||
public static $statusMap = [
|
||||
self::STATUS_OPENING => '开启',
|
||||
self::STATUS_CLOSED => '关闭'
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联的工单消息
|
||||
*/
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
|
||||
}
|
||||
|
||||
// 即将删除
|
||||
public function message(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
|
||||
}
|
||||
}
|
||||
56
Xboard/app/Models/TicketMessage.php
Normal file
56
Xboard/app/Models/TicketMessage.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\TicketMessage
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $ticket_id
|
||||
* @property int $user_id
|
||||
* @property string $message
|
||||
* @property \Illuminate\Support\Carbon $created_at
|
||||
* @property \Illuminate\Support\Carbon $updated_at
|
||||
* @property-read \App\Models\Ticket $ticket 关联的工单
|
||||
* @property-read bool $is_from_user 消息是否由工单发起人发送
|
||||
* @property-read bool $is_from_admin 消息是否由管理员发送
|
||||
*/
|
||||
class TicketMessage extends Model
|
||||
{
|
||||
protected $table = 'v2_ticket_message';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp'
|
||||
];
|
||||
|
||||
protected $appends = ['is_from_user', 'is_from_admin'];
|
||||
|
||||
/**
|
||||
* 关联的工单
|
||||
*/
|
||||
public function ticket(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ticket::class, 'ticket_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息是否由工单发起人发送
|
||||
*/
|
||||
public function getIsFromUserAttribute(): bool
|
||||
{
|
||||
return $this->ticket->user_id === $this->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断消息是否由管理员发送
|
||||
*/
|
||||
public function getIsFromAdminAttribute(): bool
|
||||
{
|
||||
return $this->ticket->user_id !== $this->user_id;
|
||||
}
|
||||
}
|
||||
149
Xboard/app/Models/TrafficResetLog.php
Normal file
149
Xboard/app/Models/TrafficResetLog.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?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 const SOURCE_ORDER = 'order';
|
||||
public const SOURCE_GIFT_CARD = 'gift_card';
|
||||
|
||||
/**
|
||||
* 获取重置类型的多语言名称
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
||||
215
Xboard/app/Models/User.php
Normal file
215
Xboard/app/Models/User.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* App\Models\User
|
||||
*
|
||||
* @property int $id 用户ID
|
||||
* @property string $email 邮箱
|
||||
* @property string $password 密码
|
||||
* @property string|null $password_algo 加密方式
|
||||
* @property string|null $password_salt 加密盐
|
||||
* @property string $token 邀请码
|
||||
* @property string $uuid
|
||||
* @property int|null $invite_user_id 邀请人
|
||||
* @property int|null $plan_id 订阅ID
|
||||
* @property int|null $group_id 权限组ID
|
||||
* @property int|null $transfer_enable 流量(KB)
|
||||
* @property int|null $speed_limit 限速Mbps
|
||||
* @property int|null $u 上行流量
|
||||
* @property int|null $d 下行流量
|
||||
* @property int|null $banned 是否封禁
|
||||
* @property int|null $remind_expire 到期提醒
|
||||
* @property int|null $remind_traffic 流量提醒
|
||||
* @property int|null $expired_at 过期时间
|
||||
* @property int|null $balance 余额
|
||||
* @property int|null $commission_balance 佣金余额
|
||||
* @property float $commission_rate 返佣比例
|
||||
* @property int|null $commission_type 返佣类型
|
||||
* @property int|null $device_limit 设备限制数量
|
||||
* @property int|null $discount 折扣
|
||||
* @property int|null $last_login_at 最后登录时间
|
||||
* @property int|null $parent_id 父账户ID
|
||||
* @property int|null $is_admin 是否管理员
|
||||
* @property int|null $next_reset_at 下次流量重置时间
|
||||
* @property int|null $last_reset_at 上次流量重置时间
|
||||
* @property int|null $telegram_id Telegram ID
|
||||
* @property int $reset_count 流量重置次数
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
* @property bool $commission_auto_check 是否自动计算佣金
|
||||
*
|
||||
* @property-read User|null $invite_user 邀请人信息
|
||||
* @property-read \App\Models\Plan|null $plan 用户订阅计划
|
||||
* @property-read ServerGroup|null $group 权限组
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, InviteCode> $codes 邀请码列表
|
||||
* @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 订阅链接(动态生成)
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens;
|
||||
protected $table = 'v2_user';
|
||||
protected $dateFormat = 'U';
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
'banned' => 'boolean',
|
||||
'is_admin' => 'boolean',
|
||||
'is_staff' => 'boolean',
|
||||
'remind_expire' => 'boolean',
|
||||
'remind_traffic' => 'boolean',
|
||||
'commission_auto_check' => 'boolean',
|
||||
'commission_rate' => 'float',
|
||||
'next_reset_at' => 'timestamp',
|
||||
'last_reset_at' => 'timestamp',
|
||||
];
|
||||
protected $hidden = ['password'];
|
||||
|
||||
public const COMMISSION_TYPE_SYSTEM = 0;
|
||||
public const COMMISSION_TYPE_PERIOD = 1;
|
||||
public const COMMISSION_TYPE_ONETIME = 2;
|
||||
protected function email(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => strtolower(trim($value)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按邮箱查询(大小写不敏感,兼容所有数据库)
|
||||
*/
|
||||
public function scopeByEmail(Builder $query, string $email): Builder
|
||||
{
|
||||
return $query->where('email', strtolower(trim($email)));
|
||||
}
|
||||
|
||||
// 获取邀请人信息
|
||||
public function invite_user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'invite_user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订阅计划
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function plan(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Plan::class, 'plan_id', 'id');
|
||||
}
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServerGroup::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
// 获取用户邀请码列表
|
||||
public function codes(): HasMany
|
||||
{
|
||||
return $this->hasMany(InviteCode::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(Order::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function stat(): HasMany
|
||||
{
|
||||
return $this->hasMany(StatUser::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
// 关联工单列表
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联流量重置记录
|
||||
*/
|
||||
public function trafficResetLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(TrafficResetLog::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否处于活跃状态
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return !$this->banned &&
|
||||
($this->expired_at === null || $this->expired_at > time()) &&
|
||||
$this->plan_id !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可用节点流量且充足
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->isActive() && $this->getRemainingTraffic() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要重置流量
|
||||
*/
|
||||
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