first commit

This commit is contained in:
CN-JS-HuiBai
2026-04-07 16:54:24 +08:00
commit 2c6a38c80d
399 changed files with 42205 additions and 0 deletions

View 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');
}
}

View 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'
];
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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(),
]);
}
}

View 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;
}

View 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',
];
}

View 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'
];
}

View 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
View 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');
}
}

View 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
View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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(),
);
}
}

View 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'
];
}

View 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'
];
}

View 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'
];
}

View 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]
);
}
}

View 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'
];
}

View 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');
}
}

View 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'
];
}

View 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);
}
}

View 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');
}
}

View 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;
}
}

View 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
View 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);
}
}