feat: new xboard

This commit is contained in:
xboard
2025-01-21 14:57:54 +08:00
parent de18cfe596
commit 0f43fff242
373 changed files with 17923 additions and 20264 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ class Knowledge extends Model
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'show' => 'boolean',
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
'updated_at' => 'timestamp',
];
}
+2 -1
View File
@@ -12,6 +12,7 @@ class Notice extends Model
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'tags' => 'array'
'tags' => 'array',
'show' => 'boolean',
];
}
+18
View File
@@ -40,4 +40,22 @@ class Order extends Model
self::TYPE_RESET_TRAFFIC => '流量重置',
];
public function payment()
{
return $this->belongsTo(Payment::class, 'payment_id', 'id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function plan()
{
return $this->belongsTo(Plan::class);
}
public function commission_log()
{
return $this->hasMany(CommissionLog::class, 'trade_no', 'trade_no');
}
}
+376 -5
View File
@@ -3,14 +3,385 @@
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;
class Plan extends Model
{
use HasFactory;
protected $table = 'v2_plan';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
// 定义流量重置方式
public const RESET_TRAFFIC_FOLLOW_SYSTEM = 0; // 跟随系统设置
public const RESET_TRAFFIC_FIRST_DAY_MONTH = 1; // 每月1号
public const RESET_TRAFFIC_MONTHLY = 2; // 按月重置
public const RESET_TRAFFIC_NEVER = 3; // 不重置
public const RESET_TRAFFIC_FIRST_DAY_YEAR = 4; // 每年1月1日
public const RESET_TRAFFIC_YEARLY = 5; // 按年重置
// 定义价格类型
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'
];
protected $casts = [
'show' => 'boolean',
'renew' => 'boolean',
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'integer',
'prices' => '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 => '按年重置',
];
}
/**
* 获取下一次流量重置时间
*
* @param Carbon|null $from 计算起始时间,默认为当前时间
* @return Carbon|null 下次重置时间,如果不重置则返回null
*/
public function getNextResetTime(?Carbon $from = null): ?Carbon
{
$from = $from ?? Carbon::now();
switch ($this->reset_traffic_method) {
case self::RESET_TRAFFIC_FIRST_DAY_MONTH:
return $from->copy()->addMonth()->startOfMonth();
case self::RESET_TRAFFIC_MONTHLY:
return $from->copy()->addMonth()->startOfDay();
case self::RESET_TRAFFIC_FIRST_DAY_YEAR:
return $from->copy()->addYear()->startOfYear();
case self::RESET_TRAFFIC_YEARLY:
return $from->copy()->addYear()->startOfDay();
case self::RESET_TRAFFIC_NEVER:
return null;
case self::RESET_TRAFFIC_FOLLOW_SYSTEM:
default:
// 这里需要实现获取系统设置的逻辑
// 可以通过系统配置或其他方式获取
return null;
}
}
/**
* 检查是否需要重置流量
*
* @param Carbon|null $checkTime 检查时间点,默认为当前时间
* @return bool
*/
public function shouldResetTraffic(?Carbon $checkTime = null): bool
{
if ($this->reset_traffic_method === self::RESET_TRAFFIC_NEVER) {
return false;
}
$checkTime = $checkTime ?? Carbon::now();
$nextResetTime = $this->getNextResetTime($checkTime);
if ($nextResetTime === null) {
return false;
}
return $checkTime->greaterThanOrEqualTo($nextResetTime);
}
/**
* 获取流量重置方式的描述
*
* @return string
*/
public function getResetTrafficMethodName(): string
{
return self::getResetTrafficMethods()[$this->reset_traffic_method] ?? '未知';
}
/**
* 获取所有可用的订阅周期
*
* @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()
{
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;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Plugin extends Model
{
protected $table = 'v2_plugins';
protected $guarded = [
'id',
'created_at',
'updated_at'
];
}
+293
View File
@@ -0,0 +1,293 @@
<?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;
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 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,
];
protected $table = 'v2_server';
protected $guarded = ['id'];
protected $casts = [
'group_ids' => 'array',
'route_ids' => 'array',
'tags' => 'array',
'protocol_settings' => 'array',
'last_check_at' => 'integer',
'last_push_at' => 'integer',
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
private const PROTOCOL_CONFIGURATIONS = [
self::TYPE_TROJAN => [
'allow_insecure' => ['type' => 'boolean', 'default' => false],
'server_name' => ['type' => 'string', 'default' => null],
'network' => ['type' => 'string', 'default' => null],
'network_settings' => ['type' => 'array', 'default' => null]
],
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::TYPE_VLESS => [
'tls' => ['type' => 'integer', 'default' => 0],
'tls_settings' => ['type' => 'array', 'default' => null],
'flow' => ['type' => 'string', 'default' => null],
'network' => ['type' => 'string', 'default' => null],
'network_settings' => ['type' => 'array', 'default' => null],
'reality_settings' => [
'type' => 'object',
'fields' => [
'allow_insecure' => ['type' => 'boolean', 'default' => false],
'server_port' => ['type' => 'string', 'default' => null],
'server_name' => ['type' => 'string', 'default' => null],
'public_key' => ['type' => 'string', 'default' => null],
'private_key' => ['type' => 'string', 'default' => null],
'short_id' => ['type' => 'string', 'default' => null]
]
]
],
self::TYPE_SHADOWSOCKS => [
'cipher' => ['type' => 'string', 'default' => null],
'obfs' => ['type' => 'string', 'default' => null],
'obfs_settings' => ['type' => 'array', '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]
]
]
],
self::TYPE_TUIC => [
'congestion_control' => ['type' => 'string', 'default' => 'cubic'],
'alpn' => ['type' => 'array', 'default' => ['h3']],
'udp_relay_mode' => ['type' => 'string', 'default' => 'native'],
'allow_insecure' => ['type' => 'boolean', 'default' => false],
'tls_settings' => ['type' => 'array', 'default' => null]
]
];
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;
}
private function getDefaultSettings(array $configs): array
{
$defaults = [];
foreach ($configs as $key => $config) {
if ($config['type'] === 'object') {
$defaults[$key] = $this->getDefaultSettings($config['fields']);
} else {
$defaults[$key] = $config['default'];
}
}
return $defaults;
}
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 loadParentCreatedAt(): void
{
if ($this->parent_id) {
$this->created_at = $this->parent()->value('created_at');
}
}
public function loadServerStatus(): void
{
$type = strtoupper($this->type);
$serverId = $this->parent_id ?: $this->id;
$this->last_check_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_CHECK_AT", $serverId));
$this->last_push_at = Cache::get(CacheKey::get("SERVER_{$type}_LAST_PUSH_AT", $serverId));
$this->online = Cache::get(CacheKey::get("SERVER_{$type}_ONLINE_USER", $serverId)) ?? 0;
$this->is_online = (time() - 300 > $this->last_check_at) ? 0 : 1;
$this->available_status = $this->getAvailableStatus();
$this->cache_key = "{$this->type}-{$this->id}-{$this->updated_at}-{$this->is_online}";
}
public function handlePortAllocation(): void
{
if (strpos($this->port, '-') !== false) {
$this->ports = $this->port;
$this->port = Helper::randomPort($this->port);
} else {
$this->port = (int) $this->port;
}
}
public function generateShadowsocksPassword(User $user): void
{
if ($this->type !== self::TYPE_SHADOWSOCKS) {
return;
}
$this->password = $user->uuid;
$cipher = data_get($this, 'protocol_settings.cipher');
if (!$cipher || !isset(self::CIPHER_CONFIGURATIONS[$cipher])) {
return;
}
$config = self::CIPHER_CONFIGURATIONS[$cipher];
$serverKey = Helper::getServerKey($this->created_at, $config['serverKeySize']);
$userKey = Helper::uuidToBase64($user->uuid, $config['userKeySize']);
$this->password = "{$serverKey}:{$userKey}";
}
public static function normalizeType(string $type): string
{
return strtolower(self::TYPE_ALIASES[$type] ?? $type);
}
public static function isValidType(string $type): bool
{
return in_array(self::normalizeType($type), self::VALID_TYPES, true);
}
public function getAvailableStatus(): 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();
}
}
+11
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class ServerGroup extends Model
{
@@ -12,4 +13,14 @@ class ServerGroup extends Model
'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();
}
}
-35
View File
@@ -1,35 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServerHysteria extends Model
{
protected $table = 'v2_server_hysteria';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'route_id' => 'array',
'tags' => 'array',
'ips' => 'array',
'excludes' => 'array'
];
// ALPN映射表
public static $alpnMap = [
0 => 'hysteria',
1 => 'http/1.1',
2 => 'h2',
3 => 'h3'
];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServerShadowsocks extends Model
{
protected $table = 'v2_server_shadowsocks';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'route_id' => 'array',
'tags' => 'array',
'excludes' => 'array',
'obfs_settings' => 'array',
'ips' => 'array'
];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServerTrojan extends Model
{
protected $table = 'v2_server_trojan';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'route_id' => 'array',
'networkSettings' => 'array',
'tags' => 'array',
'excludes' => 'array',
'ips' => 'array'
];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServerVless extends Model
{
protected $table = 'v2_server_vless';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'route_id' => 'array',
'tls_settings' => 'array',
'network_settings' => 'array',
'tags' => 'array',
'excludes' => 'array',
'ips' => 'array'
];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}
-31
View File
@@ -1,31 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServerVmess extends Model
{
protected $table = 'v2_server_vmess';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'route_id' => 'array',
'tlsSettings' => 'array',
'networkSettings' => 'array',
'dnsSettings' => 'array',
'ruleSettings' => 'array',
'tags' => 'array',
'excludes' => 'array',
'ips' => 'array'
];
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}
+5
View File
@@ -13,4 +13,9 @@ class StatServer extends Model
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
public function server()
{
return $this->belongsTo(Server::class, 'server_id');
}
}
+9
View File
@@ -21,6 +21,15 @@ class Ticket extends Model
self::STATUS_CLOSED => '关闭'
];
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id');
}
public function messages()
{
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
}
// 即将删除
public function message()
{
return $this->hasMany(TicketMessage::class, 'ticket_id', 'id');
+14 -1
View File
@@ -3,10 +3,12 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class User extends Model
class User extends Authenticatable
{
use HasApiTokens;
protected $table = 'v2_user';
protected $dateFormat = 'U';
protected $guarded = ['id'];
@@ -14,6 +16,7 @@ class User extends Model
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
protected $hidden = ['password'];
// 获取邀请人信息
@@ -28,6 +31,11 @@ class User extends Model
return $this->belongsTo(Plan::class, 'plan_id', 'id');
}
public function group()
{
return $this->belongsTo(ServerGroup::class, 'group_id', 'id');
}
// 获取用户邀请码列表
public function codes()
{
@@ -39,4 +47,9 @@ class User extends Model
{
return $this->hasMany(Ticket::class, 'user_id', 'id');
}
public function parent()
{
return $this->belongsTo(self::class, 'parent_id', 'id');
}
}