feat: plugin controller config system with guest_comm_config hook integration

- Add HasPluginConfig trait and PluginController base class
- Integrate guest_comm_config hook in CommController for plugin frontend config injection
- Add user creation functionality to UserService and fix null value handling
- Enhance AbstractPlugin.getConfig() with key parameter support
- Multiple service layer optimizations and architecture improvements
This commit is contained in:
xboard
2025-06-29 01:42:48 +08:00
parent b96700ab30
commit 5b295dbec3
16 changed files with 804 additions and 574 deletions
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers;
use App\Traits\HasPluginConfig;
/**
* 插件控制器基类
*
* 为所有插件控制器提供通用功能
*/
abstract class PluginController extends Controller
{
use HasPluginConfig;
/**
* 执行插件操作前的检查
*/
protected function beforePluginAction(): ?array
{
return null;
}
}
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\V1\Guest; namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\Plugin\HookManager;
use App\Utils\Dict; use App\Utils\Dict;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -30,6 +31,9 @@ class CommController extends Controller
// 保持向后兼容 // 保持向后兼容
'is_recaptcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0, 'is_recaptcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0,
]; ];
$data = HookManager::filter('guest_comm_config', $data);
return $this->success($data); return $this->success($data);
} }
} }
@@ -10,6 +10,7 @@ use App\Jobs\SendEmailJob;
use App\Models\Plan; use App\Models\Plan;
use App\Models\User; use App\Models\User;
use App\Services\AuthService; use App\Services\AuthService;
use App\Services\UserService;
use App\Traits\QueryOperators; use App\Traits\QueryOperators;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -342,30 +343,26 @@ class UserController extends Controller
public function generate(UserGenerate $request) public function generate(UserGenerate $request)
{ {
if ($request->input('email_prefix')) { if ($request->input('email_prefix')) {
if ($request->input('plan_id')) { $email = $request->input('email_prefix') . '@' . $request->input('email_suffix');
$plan = Plan::find($request->input('plan_id'));
if (!$plan) { if (User::where('email', $email)->exists()) {
return $this->fail([400202, '订阅计划不存在']);
}
}
$user = [
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
return $this->fail([400201, '邮箱已存在于系统中']); return $this->fail([400201, '邮箱已存在于系统中']);
} }
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) { $userService = app(UserService::class);
$user = $userService->createUser([
'email' => $email,
'password' => $request->input('password') ?? $email,
'plan_id' => $request->input('plan_id'),
'expired_at' => $request->input('expired_at'),
]);
if (!$user->save()) {
return $this->fail([500, '生成失败']); return $this->fail([500, '生成失败']);
} }
return $this->success(true); return $this->success(true);
} }
if ($request->input('generate_count')) { if ($request->input('generate_count')) {
return $this->multiGenerate($request); return $this->multiGenerate($request);
} }
@@ -373,37 +370,32 @@ class UserController extends Controller
private function multiGenerate(Request $request) private function multiGenerate(Request $request)
{ {
if ($request->input('plan_id')) { $userService = app(UserService::class);
$plan = Plan::find($request->input('plan_id')); $usersData = [];
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
}
$users = [];
for ($i = 0; $i < $request->input('generate_count'); $i++) { for ($i = 0; $i < $request->input('generate_count'); $i++) {
$user = [ $email = Helper::randomChar(6) . '@' . $request->input('email_suffix');
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'), $usersData[] = [
'plan_id' => isset($plan->id) ? $plan->id : NULL, 'email' => $email,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL, 'password' => $request->input('password') ?? $email,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0, 'plan_id' => $request->input('plan_id'),
'expired_at' => $request->input('expired_at') ?? NULL, 'expired_at' => $request->input('expired_at'),
'uuid' => Helper::guid(true),
'token' => Helper::guid(),
'created_at' => time(),
'updated_at' => time()
]; ];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
} }
try { try {
DB::beginTransaction(); DB::beginTransaction();
if (!User::insert($users)) { $users = [];
throw new \Exception(); foreach ($usersData as $userData) {
$user = $userService->createUser($userData);
$user->save();
$users[] = $user;
} }
DB::commit(); DB::commit();
} catch (\Exception $e) { } catch (\Exception $e) {
DB::rollBack(); DB::rollBack();
Log::error($e);
return $this->fail([500, '生成失败']); return $this->fail([500, '生成失败']);
} }
@@ -417,6 +409,7 @@ class UserController extends Controller
$handle = fopen('php://output', 'w'); $handle = fopen('php://output', 'w');
fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']); fputcsv($handle, ['账号', '密码', '过期时间', 'UUID', '创建时间', '订阅地址']);
foreach ($users as $user) { foreach ($users as $user) {
$user = $user->refresh();
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']); $createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email']; $password = $request->input('password') ?? $user['email'];
+23 -46
View File
@@ -6,6 +6,7 @@ use App\Models\InviteCode;
use App\Models\Plan; use App\Models\Plan;
use App\Models\User; use App\Models\User;
use App\Services\CaptchaService; use App\Services\CaptchaService;
use App\Services\UserService;
use App\Utils\CacheKey; use App\Utils\CacheKey;
use App\Utils\Dict; use App\Utils\Dict;
use App\Utils\Helper; use App\Utils\Helper;
@@ -100,16 +101,11 @@ class RegisterService
/** /**
* 处理邀请码 * 处理邀请码
* *
* @param User $user 用户对象 * @param string $inviteCode 邀请码
* @param string|null $inviteCode 邀请码 * @return array [邀请人ID或成功状态, 错误消息]
* @return array [是否成功, 错误消息]
*/ */
public function handleInviteCode(User $user, ?string $inviteCode): array public function handleInviteCode(string $inviteCode): array
{ {
if (!$inviteCode) {
return [true, null];
}
$inviteCodeModel = InviteCode::where('code', $inviteCode) $inviteCodeModel = InviteCode::where('code', $inviteCode)
->where('status', 0) ->where('status', 0)
->first(); ->first();
@@ -118,38 +114,18 @@ class RegisterService
if ((int) admin_setting('invite_force', 0)) { if ((int) admin_setting('invite_force', 0)) {
return [false, [400, __('Invalid invitation code')]]; return [false, [400, __('Invalid invitation code')]];
} }
return [true, null]; return [null, null];
} }
$user->invite_user_id = $inviteCodeModel->user_id ? $inviteCodeModel->user_id : null;
if (!(int) admin_setting('invite_never_expire', 0)) { if (!(int) admin_setting('invite_never_expire', 0)) {
$inviteCodeModel->status = true; $inviteCodeModel->status = true;
$inviteCodeModel->save(); $inviteCodeModel->save();
} }
return [true, null]; return [$inviteCodeModel->user_id, null];
} }
/**
* 处理试用计划
*
* @param User $user 用户对象
* @return void
*/
public function handleTryOut(User $user): void
{
if ((int) admin_setting('try_out_plan_id', 0)) {
$plan = Plan::find(admin_setting('try_out_plan_id'));
if ($plan) {
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
}
}
/** /**
* 注册用户 * 注册用户
@@ -167,24 +143,25 @@ class RegisterService
$email = $request->input('email'); $email = $request->input('email');
$password = $request->input('password'); $password = $request->input('password');
$inviteCode = $request->input('invite_code');
// 创建用户 // 处理邀请码获取邀请人ID
$user = new User(); $inviteUserId = null;
$user->email = $email; if ($inviteCode) {
$user->password = password_hash($password, PASSWORD_DEFAULT); [$inviteSuccess, $inviteError] = $this->handleInviteCode($inviteCode);
$user->uuid = Helper::guid(true); if (!$inviteSuccess) {
$user->token = Helper::guid(); return [false, $inviteError];
$user->remind_expire = admin_setting('default_remind_expire', 1); }
$user->remind_traffic = admin_setting('default_remind_traffic', 1); $inviteUserId = $inviteSuccess;
// 处理邀请码
[$inviteSuccess, $inviteError] = $this->handleInviteCode($user, $request->input('invite_code'));
if (!$inviteSuccess) {
return [false, $inviteError];
} }
// 处理试用计划 // 创建用户
$this->handleTryOut($user); $userService = app(UserService::class);
$user = $userService->createUser([
'email' => $email,
'password' => $password,
'invite_user_id' => $inviteUserId,
]);
// 保存用户 // 保存用户
if (!$user->save()) { if (!$user->save()) {
+1 -1
View File
@@ -80,7 +80,7 @@ class CaptchaService
// 检查分数阈值(如果有的话) // 检查分数阈值(如果有的话)
$score = $recaptchaResp->getScore(); $score = $recaptchaResp->getScore();
$threshold = admin_setting('recaptcha_v3_score_threshold', 0.5); $threshold = admin_setting('recaptcha_v3_score_threshold', 0.5);
if ($score !== null && $score < $threshold) { if ($score < $threshold) {
return [false, [400, __('Invalid code is incorrect')]]; return [false, [400, __('Invalid code is incorrect')]];
} }
+1 -1
View File
@@ -30,7 +30,7 @@ class MailService
/** /**
* 分块处理用户提醒邮件 * 分块处理用户提醒邮件
*/ */
public function processUsersInChunks(int $chunkSize, callable $progressCallback = null): array public function processUsersInChunks(int $chunkSize, ?callable $progressCallback = null): array
{ {
$statistics = [ $statistics = [
'processed_users' => 0, 'processed_users' => 0,
+6 -2
View File
@@ -58,9 +58,13 @@ abstract class AbstractPlugin
/** /**
* 获取配置 * 获取配置
*/ */
public function getConfig(): array public function getConfig(?string $key = null, $default = null): mixed
{ {
return $this->config; $config = $this->config;
if ($key) {
$config = $config[$key] ?? $default;
}
return $config;
} }
/** /**
+16 -11
View File
@@ -34,7 +34,7 @@ class PluginManager
*/ */
public function getPluginPath(string $pluginCode): string public function getPluginPath(string $pluginCode): string
{ {
return $this->pluginPath . '/' . Str::studly($pluginCode); return $this->pluginPath . '/' . Str::studly($pluginCode);
} }
/** /**
@@ -85,16 +85,21 @@ class PluginManager
{ {
$routesPath = $this->getPluginPath($pluginCode) . '/routes'; $routesPath = $this->getPluginPath($pluginCode) . '/routes';
if (File::exists($routesPath)) { if (File::exists($routesPath)) {
$files = ['web.php', 'api.php']; $webRouteFile = $routesPath . '/web.php';
foreach ($files as $file) { $apiRouteFile = $routesPath . '/api.php';
$routeFile = $routesPath . '/' . $file; if (File::exists($webRouteFile)) {
if (File::exists($routeFile)) { Route::middleware('web')
Route::middleware('web') ->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers')
->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers') ->group(function () use ($webRouteFile) {
->group(function () use ($routeFile) { require $webRouteFile;
require $routeFile; });
}); }
} if (File::exists($apiRouteFile)) {
Route::middleware('api')
->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers')
->group(function () use ($apiRouteFile) {
require $apiRouteFile;
});
} }
} }
} }
+1 -1
View File
@@ -274,7 +274,7 @@ class TrafficResetService
/** /**
* Batch check and reset users. Processes all eligible users in batches. * Batch check and reset users. Processes all eligible users in batches.
*/ */
public function batchCheckReset(int $batchSize = 100, callable $progressCallback = null): array public function batchCheckReset(int $batchSize = 100, ?callable $progressCallback = null): array
{ {
$startTime = microtime(true); $startTime = microtime(true);
$totalResetCount = 0; $totalResetCount = 0;
+95
View File
@@ -11,6 +11,8 @@ use App\Models\User;
use App\Services\Plugin\HookManager; use App\Services\Plugin\HookManager;
use App\Services\TrafficResetService; use App\Services\TrafficResetService;
use App\Models\TrafficResetLog; use App\Models\TrafficResetLog;
use App\Utils\Helper;
use Illuminate\Support\Facades\Hash;
class UserService class UserService
{ {
@@ -150,4 +152,97 @@ class UserService
'reset_count' => $user->reset_count, 'reset_count' => $user->reset_count,
]; ];
} }
/**
* 创建用户
*/
public function createUser(array $data): User
{
$user = new User();
// 基本信息
$user->email = $data['email'];
$user->password = isset($data['password'])
? Hash::make($data['password'])
: Hash::make($data['email']);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
// 默认设置
$user->remind_expire = admin_setting('default_remind_expire', 1);
$user->remind_traffic = admin_setting('default_remind_traffic', 1);
// 可选字段
$this->setOptionalFields($user, $data);
$user->expired_at = null;
// 处理计划
if (isset($data['plan_id'])) {
$this->setPlanForUser($user, $data['plan_id'], $data['expired_at'] ?? null);
} else {
$this->setTryOutPlan(user: $user);
}
return $user;
}
/**
* 设置可选字段
*/
private function setOptionalFields(User $user, array $data): void
{
$optionalFields = [
'invite_user_id',
'telegram_id',
'group_id',
'speed_limit',
'expired_at',
'transfer_enable'
];
foreach ($optionalFields as $field) {
if (array_key_exists($field, $data)) {
$user->{$field} = $data[$field];
}
}
}
/**
* 为用户设置计划
*/
private function setPlanForUser(User $user, int $planId, ?int $expiredAt = null): void
{
$plan = Plan::find($planId);
if (!$plan)
return;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->speed_limit = $plan->speed_limit;
if ($expiredAt) {
$user->expired_at = $expiredAt;
}
}
/**
* 设置试用计划
*/
private function setTryOutPlan(User $user): void
{
if (!(int) admin_setting('try_out_plan_id', 0))
return;
$plan = Plan::find(admin_setting('try_out_plan_id'));
if (!$plan)
return;
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
} }
+1 -1
View File
@@ -72,7 +72,7 @@ class Setting
* @param mixed $value * @param mixed $value
* @return bool 设置是否成功 * @return bool 设置是否成功
*/ */
public function set(string $key, $value = null): bool public function set(string $key, mixed $value = null): bool
{ {
$key = strtolower($key); $key = strtolower($key);
SettingModel::createOrUpdate($key, $value); SettingModel::createOrUpdate($key, $value);
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace App\Traits;
use App\Models\Plugin;
use Illuminate\Support\Facades\Cache;
trait HasPluginConfig
{
/**
* 缓存的插件配置
*/
protected ?array $pluginConfig = null;
/**
* 插件代码
*/
protected ?string $pluginCode = null;
/**
* 获取插件配置
*/
public function getConfig(?string $key = null, $default = null): mixed
{
$config = $this->getPluginConfig();
if ($key) {
return $config[$key] ?? $default;
}
return $config;
}
/**
* 获取完整的插件配置
*/
protected function getPluginConfig(): array
{
if ($this->pluginConfig === null) {
$pluginCode = $this->getPluginCode();
\Log::channel('daily')->info('Telegram Login: 获取插件配置', [
'plugin_code' => $pluginCode
]);
$this->pluginConfig = Cache::remember(
"plugin_config_{$pluginCode}",
3600,
function () use ($pluginCode) {
$plugin = Plugin::where('code', $pluginCode)
->where('is_enabled', true)
->first();
if (!$plugin || !$plugin->config) {
return [];
}
return json_decode($plugin->config, true) ?? [];
}
);
}
return $this->pluginConfig;
}
/**
* 获取插件代码
*/
public function getPluginCode(): string
{
if ($this->pluginCode === null) {
$this->pluginCode = $this->autoDetectPluginCode();
}
return $this->pluginCode;
}
/**
* 设置插件代码(如果自动检测不准确可以手动设置)
*/
public function setPluginCode(string $pluginCode): void
{
$this->pluginCode = $pluginCode;
$this->pluginConfig = null; // 重置配置缓存
}
/**
* 自动检测插件代码
*/
protected function autoDetectPluginCode(): string
{
$reflection = new \ReflectionClass($this);
$namespace = $reflection->getNamespaceName();
// 从命名空间提取插件代码
// 例如: Plugin\TelegramLogin\Controllers => telegram_login
if (preg_match('/^Plugin\\\\(.+?)\\\\/', $namespace, $matches)) {
return $this->convertToKebabCase($matches[1]);
}
throw new \RuntimeException('Unable to detect plugin code from namespace: ' . $namespace);
}
/**
* 将 StudlyCase 转换为 kebab-case
*/
protected function convertToKebabCase(string $string): string
{
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $string));
}
/**
* 检查插件是否启用
*/
public function isPluginEnabled(): bool
{
return (bool) $this->getConfig('enable', false);
}
/**
* 清除插件配置缓存
*/
public function clearConfigCache(): void
{
$pluginCode = $this->getPluginCode();
Cache::forget("plugin_config_{$pluginCode}");
$this->pluginConfig = null;
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ class CacheKey
/** /**
* 生成缓存键 * 生成缓存键
*/ */
public static function get(string $key, $uniqueValue = null): string public static function get(string $key, mixed $uniqueValue = null): string
{ {
// 检查是否为核心键 // 检查是否为核心键
if (array_key_exists($key, self::CORE_KEYS)) { if (array_key_exists($key, self::CORE_KEYS)) {
+462 -462
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.