diff --git a/app/Http/Controllers/V2/Admin/ConfigController.php b/app/Http/Controllers/V2/Admin/ConfigController.php index 9de1b8a..4c6ba43 100644 --- a/app/Http/Controllers/V2/Admin/ConfigController.php +++ b/app/Http/Controllers/V2/Admin/ConfigController.php @@ -72,12 +72,14 @@ class ConfigController extends Controller public function fetch(Request $request) { $key = $request->input('key'); - $configMappings = $this->getConfigMappings(); - if ($key && isset($configMappings[$key])) { - return $this->success([$key => $configMappings[$key]]); + if ($key) { + $configMapping = $this->getConfigMapping($key); + if ($configMapping !== null) { + return $this->success([$key => $configMapping]); + } } - return $this->success($configMappings); + return $this->success($this->getConfigMappings()); } /** @@ -87,7 +89,36 @@ class ConfigController extends Controller */ private function getConfigMappings(): array { - return [ + $keys = [ + 'invite', + 'site', + 'subscribe', + 'frontend', + 'server', + 'email', + 'telegram', + 'app', + 'safe', + 'subscribe_template', + ]; + + $configMappings = []; + foreach ($keys as $key) { + $configMappings[$key] = $this->getConfigMapping($key); + } + + return $configMappings; + } + + /** + * 获取单个配置分组 + * + * @param string $key 配置分组键名 + * @return array|null 配置分组内容 + */ + private function getConfigMapping(string $key): ?array + { + return match ($key) { 'invite' => [ 'invite_force' => (bool) admin_setting('invite_force', 0), 'invite_commission' => admin_setting('invite_commission', 10), @@ -205,8 +236,9 @@ class ConfigController extends Controller 'subscribe_template_stash' => subscribe_template('stash') ?? '', 'subscribe_template_surge' => subscribe_template('surge') ?? '', 'subscribe_template_surfboard' => subscribe_template('surfboard') ?? '' - ] - ]; + ], + default => null, + }; } public function save(ConfigSave $request) diff --git a/app/Models/SubscribeTemplate.php b/app/Models/SubscribeTemplate.php index fec76e0..bf31a3f 100644 --- a/app/Models/SubscribeTemplate.php +++ b/app/Models/SubscribeTemplate.php @@ -2,8 +2,16 @@ namespace App\Models; +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 Illuminate\Database\Eloquent\Model; +use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\File; class SubscribeTemplate extends Model { @@ -15,32 +23,131 @@ class SubscribeTemplate extends Model ]; private static string $cachePrefix = 'subscribe_template:'; + private static array $legacyTemplateFiles = [ + '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], + ]; 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'); + return self::resolveContent($name); }); } public static function setContent(string $name, ?string $content): void { - self::updateOrCreate( - ['name' => $name], - ['content' => $content] - ); + try { + self::updateOrCreate( + ['name' => $name], + ['content' => $content] + ); + } catch (QueryException) { + admin_setting([self::legacySettingKey($name) => $content]); + } Cache::store('redis')->forget(self::$cachePrefix . $name); } public static function getAllContents(): array { - return self::pluck('content', 'name')->toArray(); + try { + $contents = self::pluck('content', 'name')->toArray(); + return $contents ?: self::getLegacyContents(); + } catch (QueryException) { + return self::getLegacyContents(); + } } public static function flushCache(string $name): void { Cache::store('redis')->forget(self::$cachePrefix . $name); } + + /** + * Resolve template content with backwards-compatible fallbacks. + * + * @param string $name + * @return string|null + */ + private static function resolveContent(string $name): ?string + { + try { + $template = self::query() + ->select('content') + ->where('name', $name) + ->first(); + + if ($template !== null) { + return $template->content; + } + } catch (QueryException) { + // Fall back to legacy sources when the new table is unavailable. + } + + return self::getLegacyContent($name); + } + + /** + * Collect all legacy template contents by protocol name. + * + * @return array + */ + private static function getLegacyContents(): array + { + $contents = []; + + foreach (array_keys(self::$legacyTemplateFiles) as $name) { + $contents[$name] = self::getLegacyContent($name) ?? ''; + } + + return $contents; + } + + /** + * Resolve template content from legacy settings and bundled files. + * + * @param string $name + * @return string|null + */ + private static function getLegacyContent(string $name): ?string + { + $settingValue = admin_setting(self::legacySettingKey($name)); + if ($settingValue !== null && $settingValue !== '') { + return $settingValue; + } + + foreach (self::$legacyTemplateFiles[$name] ?? [] as $file) { + $path = base_path($file); + if (File::exists($path)) { + return File::get($path); + } + } + + return null; + } + + /** + * Build the legacy settings key for a template name. + * + * @param string $name + * @return string + */ + private static function legacySettingKey(string $name): string + { + return "subscribe_template_{$name}"; + } } diff --git a/plugins/.gitignore b/plugins/.gitignore index 34eb213..dd3a306 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -5,4 +5,6 @@ !Coinbase !Epay !Mgate -!Telegram \ No newline at end of file +!Telegram +!TokenPay/ +!TokenPay/** diff --git a/plugins/TokenPay/Plugin.php b/plugins/TokenPay/Plugin.php new file mode 100644 index 0000000..2a155e3 --- /dev/null +++ b/plugins/TokenPay/Plugin.php @@ -0,0 +1,123 @@ +filter('available_payment_methods', function ($methods) { + if ($this->getConfig('enabled', true)) { + $methods['TokenPay'] = [ + 'name' => $this->getConfig('display_name', 'TokenPay'), + 'icon' => $this->getConfig('icon', '🪙'), + 'plugin_code' => $this->getPluginCode(), + 'type' => 'plugin' + ]; + } + + return $methods; + }); + } + + public function form(): array + { + return [ + 'token_pay_url' => [ + 'label' => 'API 地址', + 'type' => 'string', + 'required' => true, + 'description' => '您的 TokenPay API 接口地址,例如:https://token-pay.xxx.com' + ], + 'token_pay_apitoken' => [ + 'label' => 'API Token', + 'type' => 'string', + 'required' => true, + 'description' => '您的 TokenPay API Token' + ], + 'token_pay_currency' => [ + 'label' => '币种', + 'type' => 'string', + 'required' => true, + 'description' => '您的 TokenPay 币种,如 USDT_TRC20、TRX' + ] + ]; + } + + public function pay($order): array + { + $params = [ + 'ActualAmount' => $order['total_amount'] / 100, + 'OutOrderId' => $order['trade_no'], + 'OrderUserKey' => (string) $order['user_id'], + 'Currency' => $this->getConfig('token_pay_currency'), + 'RedirectUrl' => self::FIXED_RETURN_URL, + 'NotifyUrl' => $order['notify_url'], + ]; + + ksort($params); + $str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('token_pay_apitoken'); + $params['Signature'] = md5($str); + + $curl = new Curl(); + $curl->setUserAgent('TokenPay'); + $curl->setOpt(CURLOPT_SSL_VERIFYPEER, 0); + $curl->setOpt(CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + $curl->post(rtrim((string) $this->getConfig('token_pay_url'), '/') . '/CreateOrder', json_encode($params)); + + $result = $curl->response; + $error = $curl->error ? ($curl->errorMessage ?: 'TokenPay request failed') : null; + $curl->close(); + + if ($error) { + throw new ApiException($error); + } + + if (!isset($result->success) || !$result->success) { + $message = isset($result->message) ? (string) $result->message : 'Failed to create TokenPay order'; + throw new ApiException($message); + } + + if (!isset($result->data) || !is_string($result->data) || $result->data === '') { + throw new ApiException('TokenPay payment url is invalid'); + } + + return [ + 'type' => 1, + 'data' => $result->data + ]; + } + + public function notify($params): array|bool + { + if (!isset($params['Signature'])) { + return false; + } + + $sign = $params['Signature']; + unset($params['Signature']); + ksort($params); + $str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('token_pay_apitoken'); + + if ($sign !== md5($str)) { + return false; + } + + if (!isset($params['Status']) || (int) $params['Status'] !== 1) { + return false; + } + + return [ + 'trade_no' => $params['OutOrderId'] ?? '', + 'callback_no' => $params['Id'] ?? '', + 'custom_result' => 'ok' + ]; + } +} diff --git a/plugins/TokenPay/config.json b/plugins/TokenPay/config.json new file mode 100644 index 0000000..122df30 --- /dev/null +++ b/plugins/TokenPay/config.json @@ -0,0 +1,8 @@ +{ + "name": "TokenPay", + "code": "token_pay", + "type": "payment", + "version": "1.0.0", + "description": "TokenPay payment plugin", + "author": "Micah123321" +}