refactor: move subscribe templates to dedicated database table

This commit is contained in:
xboard
2026-03-11 05:47:29 +08:00
parent 448ddecb56
commit 6efedcebd4
10 changed files with 175 additions and 102 deletions

View File

@@ -28,6 +28,16 @@ if (!function_exists('admin_setting')) {
}
}
if (!function_exists('subscribe_template')) {
/**
* Get subscribe template content by protocol name.
*/
function subscribe_template(string $name): ?string
{
return \App\Models\SubscribeTemplate::getContent($name);
}
}
if (!function_exists('admin_settings_batch')) {
/**
* 批量获取配置参数,性能优化版本

View File

@@ -4,20 +4,12 @@ namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave;
use App\Protocols\Clash;
use App\Protocols\ClashMeta;
use App\Protocols\SingBox;
use App\Protocols\Stash;
use App\Protocols\Surfboard;
use App\Protocols\Surge;
use App\Models\SubscribeTemplate;
use App\Services\MailService;
use App\Services\TelegramService;
use App\Services\ThemeService;
use App\Utils\Dict;
use Illuminate\Console\Command;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class ConfigController extends Controller
{
@@ -57,18 +49,6 @@ class ConfigController extends Controller
'data' => $mailLog,
]);
}
/**
* 获取规则模板内容
*
* @param string $file 文件路径
* @return string 文件内容
*/
private function getTemplateContent(string $file): string
{
$path = base_path($file);
return File::exists($path) ? File::get($path) : '';
}
public function setTelegramWebhook(Request $request)
{
$hookUrl = $this->resolveTelegramWebhookUrl();
@@ -214,14 +194,14 @@ class ConfigController extends Controller
],
'subscribe_template' => [
'subscribe_template_singbox' => $this->formatTemplateContent(
admin_setting('subscribe_template_singbox', $this->getDefaultTemplate('singbox')),
subscribe_template('singbox') ?? '',
'json'
),
'subscribe_template_clash' => admin_setting('subscribe_template_clash', $this->getDefaultTemplate('clash')),
'subscribe_template_clashmeta' => admin_setting('subscribe_template_clashmeta', $this->getDefaultTemplate('clashmeta')),
'subscribe_template_stash' => admin_setting('subscribe_template_stash', $this->getDefaultTemplate('stash')),
'subscribe_template_surge' => admin_setting('subscribe_template_surge', $this->getDefaultTemplate('surge')),
'subscribe_template_surfboard' => admin_setting('subscribe_template_surfboard', $this->getDefaultTemplate('surfboard'))
'subscribe_template_clash' => subscribe_template('clash') ?? '',
'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '',
'subscribe_template_stash' => subscribe_template('stash') ?? '',
'subscribe_template_surge' => subscribe_template('surge') ?? '',
'subscribe_template_surfboard' => subscribe_template('surfboard') ?? ''
]
];
}
@@ -230,7 +210,20 @@ class ConfigController extends Controller
{
$data = $request->validated();
$templateKeys = [
'subscribe_template_singbox' => 'singbox',
'subscribe_template_clash' => 'clash',
'subscribe_template_clashmeta' => 'clashmeta',
'subscribe_template_stash' => 'stash',
'subscribe_template_surge' => 'surge',
'subscribe_template_surfboard' => 'surfboard',
];
foreach ($data as $k => $v) {
if (isset($templateKeys[$k])) {
SubscribeTemplate::setContent($templateKeys[$k], $v);
continue;
}
if ($k == 'frontend_theme') {
$themeService = app(ThemeService::class);
$themeService->switch($v);
@@ -273,53 +266,6 @@ class ConfigController extends Controller
};
}
/**
* 获取默认模板内容
*
* @param string $type 模板类型
* @return string 默认模板内容
*/
private function getDefaultTemplate(string $type): string
{
$fileMap = [
'singbox' => [SingBox::CUSTOM_TEMPLATE_FILE, SingBox::DEFAULT_TEMPLATE_FILE],
'clash' => [Clash::CUSTOM_TEMPLATE_FILE, Clash::DEFAULT_TEMPLATE_FILE],
'clashmeta' => [
ClashMeta::CUSTOM_TEMPLATE_FILE,
ClashMeta::CUSTOM_CLASH_TEMPLATE_FILE,
ClashMeta::DEFAULT_TEMPLATE_FILE
],
'stash' => [
Stash::CUSTOM_TEMPLATE_FILE,
Stash::CUSTOM_CLASH_TEMPLATE_FILE,
Stash::DEFAULT_TEMPLATE_FILE
],
'surge' => [Surge::CUSTOM_TEMPLATE_FILE, Surge::DEFAULT_TEMPLATE_FILE],
'surfboard' => [Surfboard::CUSTOM_TEMPLATE_FILE, Surfboard::DEFAULT_TEMPLATE_FILE],
];
if (!isset($fileMap[$type])) {
return '';
}
// 按优先级查找可用的模板文件
foreach ($fileMap[$type] as $file) {
$content = $this->getTemplateContent($file);
if (!empty($content)) {
// 对于 SingBox需要格式化 JSON
if ($type === 'singbox') {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
return $content;
}
}
return '';
}
private function getTelegramWebhookBaseUrl(): ?string
{
$customUrl = trim((string) admin_setting('telegram_webhook_url', ''));

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

@@ -27,9 +27,7 @@ class Clash extends AbstractProtocol
$appName = admin_setting('app_name', 'XBoard');
// 优先从数据库配置中获取模板
$template = admin_setting('subscribe_template_clash', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
$template = subscribe_template('clash');
$config = Yaml::parse($template);
$proxy = [];

View File

@@ -65,13 +65,7 @@ class ClashMeta extends AbstractProtocol
$user = $this->user;
$appName = admin_setting('app_name', 'XBoard');
$template = admin_setting('subscribe_template_clashmeta', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: (
File::exists(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE))
));
$template = subscribe_template('clashmeta');
$config = Yaml::parse($template);
$proxy = [];

View File

@@ -83,9 +83,7 @@ class SingBox extends AbstractProtocol
protected function loadConfig()
{
$jsonData = admin_setting('subscribe_template_singbox', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
$jsonData = subscribe_template('singbox');
return is_array($jsonData) ? $jsonData : json_decode($jsonData, true);
}

View File

@@ -18,14 +18,14 @@ class Stash extends AbstractProtocol
Server::TYPE_HYSTERIA,
Server::TYPE_TROJAN,
Server::TYPE_TUIC,
// Server::TYPE_ANYTLS,
Server::TYPE_ANYTLS,
Server::TYPE_SOCKS,
Server::TYPE_HTTP,
];
protected $protocolRequirements = [
'stash' => [
'anytls' => [
'base_version' => '9.9.9'
'base_version' => '3.3.0' // AnyTLS 协议在3.3.0版本中添加
],
'vless' => [
'protocol_settings.tls' => [
@@ -79,13 +79,7 @@ class Stash extends AbstractProtocol
$user = $this->user;
$appName = admin_setting('app_name', 'XBoard');
$template = admin_setting('subscribe_template_stash', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: (
File::exists(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_CLASH_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE))
));
$template = subscribe_template('stash');
$config = Yaml::parse($template);
$proxy = [];

View File

@@ -58,9 +58,7 @@ class Surfboard extends AbstractProtocol
}
}
$config = admin_setting('subscribe_template_surfboard', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
$config = subscribe_template('surfboard');
// Subscription link
$subsURL = Helper::getSubscribeUrl($user['token']);
$subsDomain = request()->header('Host');

View File

@@ -78,9 +78,7 @@ class Surge extends AbstractProtocol
}
$config = admin_setting('subscribe_template_surge', File::exists(base_path(self::CUSTOM_TEMPLATE_FILE))
? File::get(base_path(self::CUSTOM_TEMPLATE_FILE))
: File::get(base_path(self::DEFAULT_TEMPLATE_FILE)));
$config = subscribe_template('surge');
// Subscription link
$subsDomain = request()->header('Host');

View File

@@ -0,0 +1,91 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
return new class extends Migration
{
public function up(): void
{
Schema::create('v2_subscribe_templates', function (Blueprint $table) {
$table->id();
$table->string('name')->unique()->comment('Template key, e.g. singbox, clash');
$table->mediumText('content')->nullable()->comment('Template content');
$table->timestamps();
});
$this->seedDefaults();
}
public function down(): void
{
Schema::dropIfExists('v2_subscribe_templates');
}
private function seedDefaults(): void
{
// Fallback order matches original protocol class behavior
$protocols = [
'singbox' => [
'resources/rules/custom.sing-box.json',
'resources/rules/default.sing-box.json',
],
'clash' => [
'resources/rules/custom.clash.yaml',
'resources/rules/default.clash.yaml',
],
'clashmeta' => [
'resources/rules/custom.clashmeta.yaml',
'resources/rules/custom.clash.yaml',
'resources/rules/default.clash.yaml',
],
'stash' => [
'resources/rules/custom.stash.yaml',
'resources/rules/custom.clash.yaml',
'resources/rules/default.clash.yaml',
],
'surge' => [
'resources/rules/custom.surge.conf',
'resources/rules/default.surge.conf',
],
'surfboard' => [
'resources/rules/custom.surfboard.conf',
'resources/rules/default.surfboard.conf',
],
];
foreach ($protocols as $name => $fileFallbacks) {
$existing = DB::table('v2_settings')
->where('name', "subscribe_template_{$name}")
->value('value');
if ($existing !== null && $existing !== '') {
$content = $existing;
} else {
$content = '';
foreach ($fileFallbacks as $file) {
$path = base_path($file);
if (File::exists($path)) {
$content = File::get($path);
break;
}
}
}
DB::table('v2_subscribe_templates')->insert([
'name' => $name,
'content' => $content,
'created_at' => now(),
'updated_at' => now(),
]);
}
// Clean up old entries from v2_settings
DB::table('v2_settings')
->where('name', 'like', 'subscribe_template_%')
->delete();
}
};