remove Traits

This commit is contained in:
Jethro Lin 2024-12-04 16:41:43 +08:00
parent 27cad57159
commit 1ab5da5576
14 changed files with 362 additions and 234 deletions

View file

@ -8,7 +8,6 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Admin; use App\Models\Admin;
use App\Services\LogService; use App\Services\LogService;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -18,26 +17,33 @@
class AuthController extends Controller class AuthController extends Controller
{ {
use ApiResponse; /**
* @var LogService
*/
private readonly LogService $logService;
public function __construct( public function __construct(LogService $logService)
private readonly LogService $logService, {
) {} $this->logService = $logService;
}
/** /**
* 管理员登录 * 管理员登录
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
* @throws ValidationException
*/ */
public function login(Request $request): JsonResponse public function login(Request $request): JsonResponse
{ {
try { try {
/** @var array{email: string, password: string} $validated */
$validated = $request->validate([ $validated = $request->validate([
'email' => 'required|email', 'email' => 'required|email',
'password' => 'required|string', 'password' => 'required|string',
]); ]);
/** @var Admin|null $admin */
$admin = Admin::where('email', $validated['email'])->first(); $admin = Admin::where('email', $validated['email'])->first();
if (!$admin || !Hash::check($validated['password'], $admin->password)) { if (!$admin || !Hash::check($validated['password'], $admin->password)) {
@ -47,6 +53,7 @@ public function login(Request $request): JsonResponse
); );
} }
/** @var string $token */
$token = $admin->createToken('admin-token')->plainTextToken; $token = $admin->createToken('admin-token')->plainTextToken;
$this->logService->logOperation( $this->logService->logOperation(
@ -59,7 +66,6 @@ public function login(Request $request): JsonResponse
'token' => $token, 'token' => $token,
'admin' => [ 'admin' => [
'id' => $admin->id, 'id' => $admin->id,
'name' => $admin->name,
'email' => $admin->email, 'email' => $admin->email,
], ],
]); ]);
@ -92,7 +98,16 @@ public function login(Request $request): JsonResponse
public function logout(Request $request): JsonResponse public function logout(Request $request): JsonResponse
{ {
try { try {
/** @var Admin|null $admin */
$admin = $request->user(); $admin = $request->user();
if (!$admin) {
return $this->error(
ErrorCode::UNAUTHORIZED,
'未登錄或會話已過期。'
);
}
$admin->currentAccessToken()->delete(); $admin->currentAccessToken()->delete();
$this->logService->logOperation( $this->logService->logOperation(
@ -122,17 +137,27 @@ public function logout(Request $request): JsonResponse
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
* @throws ValidationException
*/ */
public function changePassword(Request $request): JsonResponse public function changePassword(Request $request): JsonResponse
{ {
try { try {
/** @var Admin|null $admin */
$admin = $request->user();
if (!$admin) {
return $this->error(
ErrorCode::UNAUTHORIZED,
'未登錄或會話已過期。'
);
}
/** @var array{current_password: string, new_password: string} $validated */
$validated = $request->validate([ $validated = $request->validate([
'current_password' => 'required|string', 'current_password' => 'required|string',
'new_password' => 'required|string|min:8|confirmed', 'new_password' => 'required|string|min:8|confirmed',
]); ]);
$admin = $request->user();
if (!Hash::check($validated['current_password'], $admin->password)) { if (!Hash::check($validated['current_password'], $admin->password)) {
return $this->error( return $this->error(
ErrorCode::INVALID_CREDENTIALS, ErrorCode::INVALID_CREDENTIALS,

View file

@ -8,28 +8,31 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Admin; use App\Models\Admin;
use App\Models\Client; use App\Models\Client;
use App\Models\LlmProvider;
use App\Services\Auth\TokenService; use App\Services\Auth\TokenService;
use App\Services\LogService;
use App\Traits\ApiResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class ClientController extends Controller class ClientController extends Controller
{ {
use ApiResponse; /**
* @var TokenService
private Admin $admin; */
private readonly TokenService $tokenService;
public function __construct( public function __construct(
private readonly TokenService $tokenService, private readonly TokenService $tokenService,
private readonly LogService $logService, private readonly LogService $logService,
Request $request Request $request
) { ) {
$this->admin = $request->admin; $admin = $request->admin;
if (!$admin) {
throw new \RuntimeException('管理員信息未找到。');
}
$this->admin = $admin;
} }
/** /**
@ -38,12 +41,21 @@ public function __construct(
public function index(): JsonResponse public function index(): JsonResponse
{ {
try { try {
$clients = Client::select([ $query = Client::select([
'id', 'id',
'name', 'name',
'llm_provider_id', 'llm_provider_id',
'created_at', 'created_at',
])->get(); ]);
// 如果不是超級管理員,只能看到自己管理的客戶
if (!$this->admin->isSuperAdmin()) {
$query->whereHas('admins', function ($query) {
$query->where('admin_id', $this->admin->id);
});
}
$clients = $query->get();
return $this->success([ return $this->success([
'items' => $clients->map(fn($client) => [ 'items' => $clients->map(fn($client) => [
@ -87,12 +99,25 @@ public function store(Request $request): JsonResponse
], ],
]); ]);
// 檢查是否有權限管理該提供商
if (!$this->admin->canManageLlmProvider($validated['llm_provider_id'])) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權使用該 LLM 提供商。'
);
}
$validated['status'] = Client::STATUS_ACTIVE; $validated['status'] = Client::STATUS_ACTIVE;
$validated['rate_limit'] = config('llm.default_rate_limit', 60); $validated['rate_limit'] = config('llm.default_rate_limit', 60);
$validated['timeout'] = config('llm.default_timeout', 30); $validated['timeout'] = config('llm.default_timeout', 30);
$client = Client::create($validated); $client = Client::create($validated);
// 如果不是超級管理員,需要建立關聯
if (!$this->admin->isSuperAdmin()) {
$client->admins()->attach($this->admin->id);
}
$this->logService->logOperation( $this->logService->logOperation(
'admin', 'admin',
$this->admin->id, $this->admin->id,

View file

@ -8,9 +8,6 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Admin; use App\Models\Admin;
use App\Models\LlmProvider; use App\Models\LlmProvider;
use App\Services\LogService;
use App\Traits\ApiResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -18,23 +15,29 @@
class LlmProviderController extends Controller class LlmProviderController extends Controller
{ {
use ApiResponse; /**
* @var Admin
*/
private readonly Admin $admin;
private Admin $admin; public function __construct(Request $request)
{
public function __construct( $admin = $request->admin;
private readonly LogService $logService, if (!$admin instanceof Admin) {
Request $request throw new \RuntimeException('管理員信息未找到。');
) { }
$this->admin = $request->admin; $this->admin = $admin;
} }
/** /**
* 獲取 LLM 提供商列表 * 獲取 LLM 提供商列表
*
* @return JsonResponse
*/ */
public function index(): JsonResponse public function index(): JsonResponse
{ {
try { try {
/** @var \Illuminate\Database\Eloquent\Builder $providers */
$providers = LlmProvider::select([ $providers = LlmProvider::select([
'id', 'id',
'name', 'name',
@ -42,9 +45,30 @@ public function index(): JsonResponse
'api_url', 'api_url',
'status', 'status',
'created_at', 'created_at',
])->get(); ]);
return $this->success($providers); // 如果不是超級管理員,只能看到自己管理的提供商
if (!$this->admin->isSuperAdmin()) {
$providers->whereHas('clients', function ($query) {
$query->whereHas('admins', function ($query) {
$query->where('admin_id', $this->admin->id);
});
});
}
/** @var \Illuminate\Database\Eloquent\Collection $providerList */
$providerList = $providers->get();
return response()->json([
'items' => $providerList->map(fn($provider) => [
'id' => $provider->id,
'name' => $provider->name,
'service_name' => $provider->service_name,
'api_url' => $provider->api_url,
'status' => $provider->status,
'created_at' => $provider->created_at->toIso8601String(),
])
]);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error fetching LLM providers', [ Log::error('Error fetching LLM providers', [
@ -52,19 +76,37 @@ public function index(): JsonResponse
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
return $this->error( return response()->json([
ErrorCode::SERVER_ERROR, 'error' => ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR) 'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
); ]);
} }
} }
/** /**
* 新增 LLM 提供商 * 新增 LLM 提供商
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/ */
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
try { try {
// 只有超級管理員可以新增提供商
if (!$this->admin->isSuperAdmin()) {
return response()->json([
'error' => ErrorCode::FORBIDDEN,
'message' => '只有超級管理員可以新增 LLM 提供商。'
]);
}
/** @var array{
* name: string,
* service_name: string,
* api_url: string,
* api_token: string
* } $validated */
$validated = $request->validate([ $validated = $request->validate([
'name' => [ 'name' => [
'required', 'required',
@ -91,15 +133,11 @@ public function store(Request $request): JsonResponse
]); ]);
$validated['status'] = LlmProvider::STATUS_ACTIVE; $validated['status'] = LlmProvider::STATUS_ACTIVE;
/** @var LlmProvider $provider */
$provider = LlmProvider::create($validated); $provider = LlmProvider::create($validated);
$this->logService->logOperation( return response()->json([
'admin',
$this->admin->id,
"Created LLM provider: {$provider->name}"
);
return $this->created([
'id' => $provider->id, 'id' => $provider->id,
'name' => $provider->name, 'name' => $provider->name,
'service_name' => $provider->service_name, 'service_name' => $provider->service_name,
@ -109,11 +147,11 @@ public function store(Request $request): JsonResponse
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->error( return response()->json([
ErrorCode::VALIDATION_ERROR, 'error' => ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR), 'message' => ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors() 'errors' => $e->errors()
); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error creating LLM provider', [ Log::error('Error creating LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -121,10 +159,10 @@ public function store(Request $request): JsonResponse
'request_data' => $request->except(['api_token']), 'request_data' => $request->except(['api_token']),
]); ]);
return $this->error( return response()->json([
ErrorCode::SERVER_ERROR, 'error' => ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR) 'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
); ]);
} }
} }
@ -137,10 +175,10 @@ public function update(Request $request, int $id): JsonResponse
$provider = LlmProvider::findOrFail($id); $provider = LlmProvider::findOrFail($id);
if (!$this->admin->canManageLlmProvider($provider->id)) { if (!$this->admin->canManageLlmProvider($provider->id)) {
return $this->error( return response()->json([
ErrorCode::FORBIDDEN, 'error' => ErrorCode::FORBIDDEN,
'您無權管理該 LLM 提供商。' 'message' => '您無權管理該 LLM 提供商。'
); ]);
} }
$validated = $request->validate([ $validated = $request->validate([
@ -177,22 +215,16 @@ public function update(Request $request, int $id): JsonResponse
if ($validated['status'] === LlmProvider::STATUS_INACTIVE && if ($validated['status'] === LlmProvider::STATUS_INACTIVE &&
$provider->status === LlmProvider::STATUS_ACTIVE) { $provider->status === LlmProvider::STATUS_ACTIVE) {
if ($provider->clients()->exists()) { if ($provider->clients()->exists()) {
return $this->error( return response()->json([
ErrorCode::RESOURCE_IN_USE, 'error' => ErrorCode::RESOURCE_IN_USE,
'該提供商正在被客戶使用,無法停用。' 'message' => '該提供商正在被客戶使用,無法停用。'
); ]);
} }
} }
$provider->update($validated); $provider->update($validated);
$this->logService->logOperation( return response()->json([
'admin',
$this->admin->id,
"Updated LLM provider: {$provider->name}"
);
return $this->success([
'id' => $provider->id, 'id' => $provider->id,
'name' => $provider->name, 'name' => $provider->name,
'service_name' => $provider->service_name, 'service_name' => $provider->service_name,
@ -202,16 +234,11 @@ public function update(Request $request, int $id): JsonResponse
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->error( return response()->json([
ErrorCode::VALIDATION_ERROR, 'error' => ErrorCode::VALIDATION_ERROR,
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR), 'message' => ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
$e->errors() 'errors' => $e->errors()
); ]);
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::RESOURCE_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND)
);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error updating LLM provider', [ Log::error('Error updating LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -220,10 +247,10 @@ public function update(Request $request, int $id): JsonResponse
'request_data' => $request->except(['api_token']), 'request_data' => $request->except(['api_token']),
]); ]);
return $this->error( return response()->json([
ErrorCode::SERVER_ERROR, 'error' => ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR) 'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
); ]);
} }
} }
@ -236,36 +263,27 @@ public function destroy(int $id): JsonResponse
$provider = LlmProvider::findOrFail($id); $provider = LlmProvider::findOrFail($id);
if (!$this->admin->canManageLlmProvider($provider->id)) { if (!$this->admin->canManageLlmProvider($provider->id)) {
return $this->error( return response()->json([
ErrorCode::FORBIDDEN, 'error' => ErrorCode::FORBIDDEN,
'您無權管理該 LLM 提供商。' 'message' => '您無權管理該 LLM 提供商。'
); ]);
} }
// 檢查是否有客戶在使用 // 檢查是否有客戶在使用
if ($provider->clients()->exists()) { if ($provider->clients()->exists()) {
return $this->error( return response()->json([
ErrorCode::RESOURCE_IN_USE, 'error' => ErrorCode::RESOURCE_IN_USE,
'該提供商正在被客戶使用,無法刪除。' 'message' => '該提供商正在被客戶使用,無法刪除。'
); ]);
} }
$providerName = $provider->name; $providerName = $provider->name;
$provider->delete(); $provider->delete();
$this->logService->logOperation( return response()->json([
'admin', 'message' => 'LLM 提供商已刪除。'
$this->admin->id, ]);
"Deleted LLM provider: {$providerName}"
);
return $this->success(null, 'LLM 提供商已刪除。');
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::RESOURCE_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND)
);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error deleting LLM provider', [ Log::error('Error deleting LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -273,10 +291,10 @@ public function destroy(int $id): JsonResponse
'provider_id' => $id, 'provider_id' => $id,
]); ]);
return $this->error( return response()->json([
ErrorCode::SERVER_ERROR, 'error' => ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR) 'message' => ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
); ]);
} }
} }
} }

View file

@ -7,7 +7,6 @@
use App\Constants\ErrorCode; use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\Auth\TokenService; use App\Services\Auth\TokenService;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -15,25 +14,35 @@
class AuthController extends Controller class AuthController extends Controller
{ {
use ApiResponse; /**
* @var TokenService
*/
private readonly TokenService $tokenService;
public function __construct( public function __construct(TokenService $tokenService)
private readonly TokenService $tokenService, {
) {} $this->tokenService = $tokenService;
}
/** /**
* 獲取訪問令牌 * 獲取訪問令牌
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/ */
public function getAccessToken(Request $request): JsonResponse public function getAccessToken(Request $request): JsonResponse
{ {
try { try {
/** @var array{auth_token: string} $validated */
$validated = $request->validate([ $validated = $request->validate([
'auth_token' => 'required|string|size:64', 'auth_token' => 'required|string|size:64',
]); ]);
/** @var array{client_id: int, expires_at: string}|null $authTokenData */
$authTokenData = $this->tokenService->validateAuthToken($validated['auth_token']); $authTokenData = $this->tokenService->validateAuthToken($validated['auth_token']);
if (!$authTokenData) { if (!$authTokenData || !isset($authTokenData['expires_at'])) {
return $this->error( return $this->error(
ErrorCode::TOKEN_INVALID, ErrorCode::TOKEN_INVALID,
'認證令牌無效。' '認證令牌無效。'
@ -47,9 +56,17 @@ public function getAccessToken(Request $request): JsonResponse
); );
} }
/** @var array{access_token: string, expires_in: int} $result */
$result = $this->tokenService->generateAccessToken($authTokenData); $result = $this->tokenService->generateAccessToken($authTokenData);
return $this->success($result); if (!isset($result['access_token']) || !isset($result['expires_in'])) {
throw new \RuntimeException('生成訪問令牌失敗。');
}
return $this->success([
'access_token' => $result['access_token'],
'expires_in' => $result['expires_in'],
]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return $this->error( return $this->error(

View file

@ -7,7 +7,6 @@
use App\Constants\ErrorCode; use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\LlmService; use App\Services\LlmService;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -15,18 +14,38 @@
class LlmController extends Controller class LlmController extends Controller
{ {
use ApiResponse; /**
* @var LlmService
*/
private readonly LlmService $llmService;
public function __construct( public function __construct(LlmService $llmService)
private readonly LlmService $llmService, {
) {} $this->llmService = $llmService;
}
/** /**
* 發送 LLM 請求 * 發送 LLM 請求
*
* @param Request $request
* @return JsonResponse
* @throws ValidationException|\RuntimeException
*/ */
public function request(Request $request): JsonResponse public function request(Request $request): JsonResponse
{ {
try { try {
if (!$request->client) {
throw new \RuntimeException('客戶信息未找到。');
}
/** @var array{
* prompt: string,
* max_tokens?: int,
* temperature?: float,
* top_p?: float,
* frequency_penalty?: float,
* presence_penalty?: float
* } $validated */
$validated = $request->validate([ $validated = $request->validate([
'prompt' => 'required|string|max:4000', 'prompt' => 'required|string|max:4000',
'max_tokens' => 'nullable|integer|min:1|max:4000', 'max_tokens' => 'nullable|integer|min:1|max:4000',
@ -36,12 +55,17 @@ public function request(Request $request): JsonResponse
'presence_penalty' => 'nullable|numeric|min:-2|max:2', 'presence_penalty' => 'nullable|numeric|min:-2|max:2',
]); ]);
/** @var array{response: string} $result */
$result = $this->llmService->sendRequest( $result = $this->llmService->sendRequest(
$request->client, $request->client,
$validated['prompt'], $validated['prompt'],
array_filter($validated, fn($key) => $key !== 'prompt', ARRAY_FILTER_USE_KEY) array_filter($validated, fn($key) => $key !== 'prompt', ARRAY_FILTER_USE_KEY)
); );
if (!isset($result['response'])) {
throw new \RuntimeException('LLM 提供商返回的響應格式無效。');
}
return $this->success([ return $this->success([
'response' => $result['response'], 'response' => $result['response'],
]); ]);

View file

@ -1,8 +1,103 @@
<?php <?php
declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
abstract class Controller abstract class Controller
{ {
// /**
* 成功响应
*
* @param mixed $data 响应数据
* @param string|null $message 成功消息
* @param int $code HTTP状态码
*/
protected function success(mixed $data = null, ?string $message = null, int $code = Response::HTTP_OK): JsonResponse
{
$response = [
'success' => true,
];
if ($data !== null) {
$response['data'] = $data;
}
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response, $code);
}
/**
* 错误响应
*
* @param string $error 错误代码
* @param string $message 错误消息
* @param mixed $errors 详细错误信息
* @param int $code HTTP状态码
*/
protected function error(
string $error,
string $message,
mixed $errors = null,
int $code = Response::HTTP_BAD_REQUEST
): JsonResponse {
$response = [
'success' => false,
'error' => $error,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
/**
* 分页响应
*
* @param array $items 分页数据
* @param array $meta 分页元数据
* @param string|null $message 成功消息
*/
protected function paginate(array $items, array $meta, ?string $message = null): JsonResponse
{
$response = [
'success' => true,
'data' => $items,
'meta' => $meta,
];
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response);
}
/**
* 创建成功响应
*
* @param mixed $data 创建的资源数据
* @param string|null $message 成功消息
*/
protected function created(mixed $data, ?string $message = null): JsonResponse
{
return $this->success($data, $message, Response::HTTP_CREATED);
}
/**
* 无内容响应
*/
protected function noContent(): JsonResponse
{
return response()->json(null, Response::HTTP_NO_CONTENT);
}
} }

View file

@ -40,10 +40,13 @@ class Admin extends Authenticatable
'role' => 'string', 'role' => 'string',
]; ];
/**
* @return BelongsToMany<Client>
*/
public function clients(): BelongsToMany public function clients(): BelongsToMany
{ {
return $this->belongsToMany(Client::class, 'admin_client') return $this->belongsToMany(Client::class, 'admin_client')
->withTimestamp('assigned_at'); ->withTimestamps();
} }
/** /**
@ -72,6 +75,8 @@ public function isValidAdmin(): bool
/** /**
* 检查是否可以管理指定的客户 * 检查是否可以管理指定的客户
*
* @param int $clientId
*/ */
public function canManageClient(int $clientId): bool public function canManageClient(int $clientId): bool
{ {
@ -84,6 +89,8 @@ public function canManageClient(int $clientId): bool
/** /**
* 检查是否可以管理指定的LLM提供商 * 检查是否可以管理指定的LLM提供商
*
* @param int $providerId
*/ */
public function canManageLlmProvider(int $providerId): bool public function canManageLlmProvider(int $providerId): bool
{ {

View file

@ -40,11 +40,17 @@ class Client extends Model
'status' => 'string', 'status' => 'string',
]; ];
/**
* @return BelongsTo<LlmProvider>
*/
public function llmProvider(): BelongsTo public function llmProvider(): BelongsTo
{ {
return $this->belongsTo(LlmProvider::class); return $this->belongsTo(LlmProvider::class);
} }
/**
* @return BelongsToMany<Admin>
*/
public function admins(): BelongsToMany public function admins(): BelongsToMany
{ {
return $this->belongsToMany(Admin::class, 'admin_client') return $this->belongsToMany(Admin::class, 'admin_client')
@ -64,6 +70,8 @@ public function isActive(): bool
*/ */
public function canSendLlmRequest(): bool public function canSendLlmRequest(): bool
{ {
return $this->isActive() && $this->llmProvider->isActive(); /** @var LlmProvider|null $provider */
$provider = $this->llmProvider;
return $this->isActive() && $provider && $provider->isActive();
} }
} }

View file

@ -41,6 +41,9 @@ class LlmProvider extends Model
'status' => 'string', 'status' => 'string',
]; ];
/**
* @return HasMany<Client>
*/
public function clients(): HasMany public function clients(): HasMany
{ {
return $this->hasMany(Client::class); return $this->hasMany(Client::class);

View file

@ -1,3 +1,4 @@
<?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Providers; namespace App\Providers;

View file

@ -13,16 +13,20 @@ class ThrottleAuthToken implements Rule
private const MAX_ATTEMPTS = 5; // 最大尝试次数 private const MAX_ATTEMPTS = 5; // 最大尝试次数
private const DECAY_MINUTES = 1; // 重置时间(分钟) private const DECAY_MINUTES = 1; // 重置时间(分钟)
public function validate(string $attribute, mixed $value, Closure $fail): void public function passes($attribute, $value): bool
{ {
$key = 'auth_token_' . $value; $key = 'auth_token_' . $value;
if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) { if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) {
$seconds = RateLimiter::availableIn($key); return false;
$fail("请求过于频繁,请在 {$seconds} 秒后重试。");
return;
} }
RateLimiter::hit($key, self::DECAY_MINUTES * 60); RateLimiter::hit($key, self::DECAY_MINUTES * 60);
return true;
}
public function message(): string
{
return '请求过于频繁,请稍后重试。';
} }
} }

View file

@ -65,21 +65,26 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null):
/** /**
* 驗證認證令牌 * 驗證認證令牌
* *
* @param string $token
* @return array{client_id: int, token: string, created_at: string, expires_at: string}|null * @return array{client_id: int, token: string, created_at: string, expires_at: string}|null
*/ */
public function validateAuthToken(string $token): ?array public function validateAuthToken(string $token): ?array
{ {
// 檢查黑名單 // 檢查黑名單
if (Redis::exists(self::BLACKLIST_PREFIX . $token)) { /** @var bool $isBlacklisted */
$isBlacklisted = Redis::exists(self::BLACKLIST_PREFIX . $token);
if ($isBlacklisted) {
return null; return null;
} }
// 檢查令牌是否存在且有效 // 檢查令牌是否存在且有效
/** @var string|null $tokenData */
$tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token);
if (!$tokenData) { if (!$tokenData) {
return null; return null;
} }
/** @var array{client_id: int, token: string, created_at: string, expires_at: string}|null $data */
$data = json_decode($tokenData, true); $data = json_decode($tokenData, true);
if (!$data || now()->isAfter($data['expires_at'])) { if (!$data || now()->isAfter($data['expires_at'])) {
Redis::del(self::AUTH_TOKEN_PREFIX . $token); Redis::del(self::AUTH_TOKEN_PREFIX . $token);
@ -125,16 +130,20 @@ public function generateAccessToken(array $authTokenData): array
/** /**
* 驗證訪問令牌 * 驗證訪問令牌
* *
* @param string $token
* @return array{client_id: int, auth_token: string, created_at: string}|null * @return array{client_id: int, auth_token: string, created_at: string}|null
*/ */
public function validateAccessToken(string $token): ?array public function validateAccessToken(string $token): ?array
{ {
/** @var string|null $tokenData */
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
if (!$tokenData) { if (!$tokenData) {
return null; return null;
} }
return json_decode($tokenData, true); /** @var array{client_id: int, auth_token: string, created_at: string}|null $data */
$data = json_decode($tokenData, true);
return $data;
} }
/** /**

View file

@ -32,6 +32,7 @@ public function sendRequest(Client $client, string $prompt, array $options = [])
} }
try { try {
/** @var \App\Models\LlmProvider $provider */
$provider = $client->llmProvider; $provider = $client->llmProvider;
if ($provider->status !== 'active') { if ($provider->status !== 'active') {

View file

@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Traits;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
trait ApiResponse
{
/**
* 成功响应
*
* @param mixed $data 响应数据
* @param string|null $message 成功消息
* @param int $code HTTP状态码
* @return JsonResponse
*/
protected function success(mixed $data = null, ?string $message = null, int $code = Response::HTTP_OK): JsonResponse
{
$response = [
'success' => true,
];
if ($data !== null) {
$response['data'] = $data;
}
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response, $code);
}
/**
* 错误响应
*
* @param string $error 错误代码
* @param string $message 错误消息
* @param mixed $errors 详细错误信息
* @param int $code HTTP状态码
* @return JsonResponse
*/
protected function error(
string $error,
string $message,
mixed $errors = null,
int $code = Response::HTTP_BAD_REQUEST
): JsonResponse {
$response = [
'success' => false,
'error' => $error,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
/**
* 分页响应
*
* @param array $items 分页数据
* @param array $meta 分页元数据
* @param string|null $message 成功消息
* @return JsonResponse
*/
protected function paginate(array $items, array $meta, ?string $message = null): JsonResponse
{
$response = [
'success' => true,
'data' => $items,
'meta' => $meta,
];
if ($message !== null) {
$response['message'] = $message;
}
return response()->json($response);
}
/**
* 创建成功响应
*
* @param mixed $data 创建的资源数据
* @param string|null $message 成功消息
* @return JsonResponse
*/
protected function created(mixed $data, ?string $message = null): JsonResponse
{
return $this->success($data, $message, Response::HTTP_CREATED);
}
/**
* 无内容响应
*
* @return JsonResponse
*/
protected function noContent(): JsonResponse
{
return response()->json(null, Response::HTTP_NO_CONTENT);
}
}