bug fix
This commit is contained in:
parent
383e73c2cd
commit
74790f6180
5 changed files with 284 additions and 132 deletions
|
|
@ -33,32 +33,121 @@ public function __construct(
|
|||
}
|
||||
|
||||
/**
|
||||
* 檢查客戶是否有活躍的令牌
|
||||
* 獲取客戶列表
|
||||
*/
|
||||
private function hasActiveTokens(Client $client): bool
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$pattern = 'auth_token:*';
|
||||
$keys = Redis::keys($pattern);
|
||||
try {
|
||||
$clients = Client::with('llmProvider:id,name,service_name')
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
'llm_provider_id',
|
||||
'rate_limit',
|
||||
'timeout',
|
||||
'status',
|
||||
'created_at',
|
||||
])
|
||||
->get();
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$data = Redis::get($key);
|
||||
if ($data) {
|
||||
$tokenData = json_decode($data, true);
|
||||
if ($tokenData &&
|
||||
$tokenData['client_id'] === $client->id &&
|
||||
now()->isBefore($tokenData['expires_at'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return $this->success($clients);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching clients', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return $this->error(
|
||||
ErrorCode::SERVER_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用客戶
|
||||
* 新增客戶
|
||||
*/
|
||||
public function deactivate(int $id): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
'unique:clients',
|
||||
],
|
||||
'llm_provider_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:llm_providers,id',
|
||||
],
|
||||
'rate_limit' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:1000',
|
||||
],
|
||||
'timeout' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:300',
|
||||
],
|
||||
]);
|
||||
|
||||
$validated['status'] = Client::STATUS_ACTIVE;
|
||||
$client = Client::create($validated);
|
||||
|
||||
// 加載 LLM 提供商信息
|
||||
$client->load('llmProvider:id,name,service_name');
|
||||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
$this->admin->id,
|
||||
"Created client: {$client->name}"
|
||||
);
|
||||
|
||||
return $this->created([
|
||||
'id' => $client->id,
|
||||
'name' => $client->name,
|
||||
'llm_provider_id' => $client->llm_provider_id,
|
||||
'llm_provider' => [
|
||||
'id' => $client->llmProvider->id,
|
||||
'name' => $client->llmProvider->name,
|
||||
'service_name' => $client->llmProvider->service_name,
|
||||
],
|
||||
'rate_limit' => $client->rate_limit,
|
||||
'timeout' => $client->timeout,
|
||||
'status' => $client->status,
|
||||
'created_at' => $client->created_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(
|
||||
ErrorCode::VALIDATION_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
|
||||
$e->errors()
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating client', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'request_data' => $request->all(),
|
||||
]);
|
||||
|
||||
return $this->error(
|
||||
ErrorCode::SERVER_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改客戶
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$client = Client::findOrFail($id);
|
||||
|
|
@ -70,32 +159,91 @@ public function deactivate(int $id): JsonResponse
|
|||
);
|
||||
}
|
||||
|
||||
if ($this->hasActiveTokens($client)) {
|
||||
return $this->error(
|
||||
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
|
||||
'該客戶有活躍的認證令牌,無法停用。'
|
||||
);
|
||||
$validated = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
"unique:clients,name,{$id}",
|
||||
],
|
||||
'llm_provider_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
'exists:llm_providers,id',
|
||||
],
|
||||
'rate_limit' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:1000',
|
||||
],
|
||||
'timeout' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:300',
|
||||
],
|
||||
'status' => [
|
||||
'required',
|
||||
'string',
|
||||
'in:active,inactive',
|
||||
],
|
||||
]);
|
||||
|
||||
// 如果要停用客戶,檢查是否有活躍的令牌
|
||||
if ($validated['status'] === Client::STATUS_INACTIVE &&
|
||||
$client->status === Client::STATUS_ACTIVE) {
|
||||
if ($this->hasActiveTokens($client)) {
|
||||
return $this->error(
|
||||
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
|
||||
'該客戶有活躍的認證令牌,無法停用。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$client->update(['status' => Client::STATUS_INACTIVE]);
|
||||
$client->update($validated);
|
||||
|
||||
// 加載 LLM 提供商信息
|
||||
$client->load('llmProvider:id,name,service_name');
|
||||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
$this->admin->id,
|
||||
"Deactivated client: {$client->name}"
|
||||
"Updated client: {$client->name}"
|
||||
);
|
||||
|
||||
return $this->success();
|
||||
return $this->success([
|
||||
'id' => $client->id,
|
||||
'name' => $client->name,
|
||||
'llm_provider_id' => $client->llm_provider_id,
|
||||
'llm_provider' => [
|
||||
'id' => $client->llmProvider->id,
|
||||
'name' => $client->llmProvider->name,
|
||||
'service_name' => $client->llmProvider->service_name,
|
||||
],
|
||||
'rate_limit' => $client->rate_limit,
|
||||
'timeout' => $client->timeout,
|
||||
'status' => $client->status,
|
||||
'updated_at' => $client->updated_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(
|
||||
ErrorCode::VALIDATION_ERROR,
|
||||
ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
|
||||
$e->errors()
|
||||
);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return $this->error(
|
||||
ErrorCode::CLIENT_NOT_FOUND,
|
||||
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deactivating client', [
|
||||
Log::error('Error updating client', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'client_id' => $id,
|
||||
'request_data' => $request->all(),
|
||||
]);
|
||||
|
||||
return $this->error(
|
||||
|
|
@ -135,7 +283,7 @@ public function destroy(int $id): JsonResponse
|
|||
"Deleted client: {$client->name}"
|
||||
);
|
||||
|
||||
return $this->success();
|
||||
return $this->success(null, '客戶已刪除。');
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return $this->error(
|
||||
|
|
@ -146,6 +294,7 @@ public function destroy(int $id): JsonResponse
|
|||
Log::error('Error deleting client', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'client_id' => $id,
|
||||
]);
|
||||
|
||||
return $this->error(
|
||||
|
|
@ -177,7 +326,7 @@ public function generateAuthToken(int $id): JsonResponse
|
|||
);
|
||||
}
|
||||
|
||||
$token = $this->tokenService->generateAuthToken($client);
|
||||
$result = $this->tokenService->generateAuthToken($client);
|
||||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
|
|
@ -185,15 +334,7 @@ public function generateAuthToken(int $id): JsonResponse
|
|||
"Generated auth token for client: {$client->name}"
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'auth_token' => $token['token'],
|
||||
'expires_at' => $token['expires_at'],
|
||||
'client' => [
|
||||
'id' => $client->id,
|
||||
'name' => $client->name,
|
||||
'status' => $client->status,
|
||||
],
|
||||
]);
|
||||
return $this->success($result);
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return $this->error(
|
||||
|
|
@ -204,6 +345,7 @@ public function generateAuthToken(int $id): JsonResponse
|
|||
Log::error('Error generating auth token', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'client_id' => $id,
|
||||
]);
|
||||
|
||||
return $this->error(
|
||||
|
|
@ -212,4 +354,27 @@ public function generateAuthToken(int $id): JsonResponse
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查客戶是否有活躍的令牌
|
||||
*/
|
||||
private function hasActiveTokens(Client $client): bool
|
||||
{
|
||||
$pattern = 'auth_token:*';
|
||||
$keys = Redis::keys($pattern);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$data = Redis::get($key);
|
||||
if ($data) {
|
||||
$tokenData = json_decode($data, true);
|
||||
if ($tokenData &&
|
||||
$tokenData['client_id'] === $client->id &&
|
||||
now()->isBefore($tokenData['expires_at'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Constants\ErrorCode;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Admin;
|
||||
use App\Models\LlmProvider;
|
||||
use App\Services\LogService;
|
||||
use App\Traits\ApiResponse;
|
||||
|
|
@ -19,48 +20,31 @@ class LlmProviderController extends Controller
|
|||
{
|
||||
use ApiResponse;
|
||||
|
||||
private Admin $admin;
|
||||
|
||||
public function __construct(
|
||||
private readonly LogService $logService,
|
||||
) {}
|
||||
Request $request
|
||||
) {
|
||||
$this->admin = $request->admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 LLM 提供商列表
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* 獲取 LLM 提供商列表
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$search = $request->input('search');
|
||||
$status = $request->input('status');
|
||||
$providers = LlmProvider::select([
|
||||
'id',
|
||||
'name',
|
||||
'service_name',
|
||||
'api_url',
|
||||
'status',
|
||||
'created_at',
|
||||
])->get();
|
||||
|
||||
$query = LlmProvider::query()->orderBy('created_at', 'desc');
|
||||
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('service_name', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($status && in_array($status, ['active', 'inactive'])) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$providers = $query->paginate($perPage);
|
||||
|
||||
return $this->paginate(
|
||||
$providers->items(),
|
||||
[
|
||||
'current_page' => $providers->currentPage(),
|
||||
'per_page' => $providers->perPage(),
|
||||
'total' => $providers->total(),
|
||||
'total_pages' => $providers->lastPage(),
|
||||
]
|
||||
);
|
||||
return $this->success($providers);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching LLM providers', [
|
||||
|
|
@ -76,10 +60,7 @@ public function index(Request $request): JsonResponse
|
|||
}
|
||||
|
||||
/**
|
||||
* 创建 LLM 提供商
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* 新增 LLM 提供商
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
|
|
@ -90,38 +71,42 @@ public function store(Request $request): JsonResponse
|
|||
'string',
|
||||
'max:100',
|
||||
'unique:llm_providers',
|
||||
'regex:/^[\w\-\s]+$/u',
|
||||
],
|
||||
'service_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
'regex:/^[\w\-]+$/u',
|
||||
],
|
||||
'api_url' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'url',
|
||||
'starts_with:https',
|
||||
'max:255',
|
||||
],
|
||||
'api_token' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'api_token' => 'required|string|max:255',
|
||||
], [
|
||||
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。',
|
||||
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
|
||||
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
|
||||
]);
|
||||
|
||||
$validated['status'] = 'active';
|
||||
$validated['status'] = LlmProvider::STATUS_ACTIVE;
|
||||
$provider = LlmProvider::create($validated);
|
||||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
$request->admin->id,
|
||||
$this->admin->id,
|
||||
"Created LLM provider: {$provider->name}"
|
||||
);
|
||||
|
||||
return $this->created($provider);
|
||||
return $this->created([
|
||||
'id' => $provider->id,
|
||||
'name' => $provider->name,
|
||||
'service_name' => $provider->service_name,
|
||||
'api_url' => $provider->api_url,
|
||||
'api_token' => $provider->api_token,
|
||||
'created_at' => $provider->created_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(
|
||||
|
|
@ -144,57 +129,57 @@ public function store(Request $request): JsonResponse
|
|||
}
|
||||
|
||||
/**
|
||||
* 更新 LLM 提供商
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
* 修改 LLM 提供商
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$provider = LlmProvider::findOrFail($id);
|
||||
|
||||
if (!$this->admin->canManageLlmProvider($provider->id)) {
|
||||
return $this->error(
|
||||
ErrorCode::FORBIDDEN,
|
||||
'您無權管理該 LLM 提供商。'
|
||||
);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
"unique:llm_providers,name,{$id}",
|
||||
'regex:/^[\w\-\s]+$/u',
|
||||
],
|
||||
'service_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:100',
|
||||
'regex:/^[\w\-]+$/u',
|
||||
],
|
||||
'api_url' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
'url',
|
||||
'starts_with:https',
|
||||
'max:255',
|
||||
],
|
||||
'api_token' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'api_token' => 'required|string|max:255',
|
||||
'status' => [
|
||||
'required',
|
||||
'string',
|
||||
'in:active,inactive',
|
||||
],
|
||||
], [
|
||||
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。',
|
||||
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
|
||||
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
|
||||
'status.in' => '状态只能是 active 或 inactive。',
|
||||
]);
|
||||
|
||||
// 如果状态改为 inactive,检查是否有客户正在使用
|
||||
if ($validated['status'] === 'inactive' && $provider->status === 'active') {
|
||||
// 如果要停用提供商,檢查是否有客戶在使用
|
||||
if ($validated['status'] === LlmProvider::STATUS_INACTIVE &&
|
||||
$provider->status === LlmProvider::STATUS_ACTIVE) {
|
||||
if ($provider->clients()->exists()) {
|
||||
return $this->error(
|
||||
ErrorCode::RESOURCE_IN_USE,
|
||||
'该提供商正在被客户使用,无法停用。'
|
||||
'該提供商正在被客戶使用,無法停用。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -203,11 +188,18 @@ public function update(Request $request, int $id): JsonResponse
|
|||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
$request->admin->id,
|
||||
$this->admin->id,
|
||||
"Updated LLM provider: {$provider->name}"
|
||||
);
|
||||
|
||||
return $this->success($provider);
|
||||
return $this->success([
|
||||
'id' => $provider->id,
|
||||
'name' => $provider->name,
|
||||
'service_name' => $provider->service_name,
|
||||
'api_url' => $provider->api_url,
|
||||
'api_token' => $provider->api_token,
|
||||
'updated_at' => $provider->updated_at->toIso8601String(),
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(
|
||||
|
|
@ -236,22 +228,25 @@ public function update(Request $request, int $id): JsonResponse
|
|||
}
|
||||
|
||||
/**
|
||||
* 删除 LLM 提供商
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
* 刪除 LLM 提供商
|
||||
*/
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$provider = LlmProvider::findOrFail($id);
|
||||
|
||||
// 检查是否有客户正在使用
|
||||
if (!$this->admin->canManageLlmProvider($provider->id)) {
|
||||
return $this->error(
|
||||
ErrorCode::FORBIDDEN,
|
||||
'您無權管理該 LLM 提供商。'
|
||||
);
|
||||
}
|
||||
|
||||
// 檢查是否有客戶在使用
|
||||
if ($provider->clients()->exists()) {
|
||||
return $this->error(
|
||||
ErrorCode::RESOURCE_IN_USE,
|
||||
'该提供商正在被客户使用,无法删除。'
|
||||
'該提供商正在被客戶使用,無法刪除。'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -260,11 +255,11 @@ public function destroy(Request $request, int $id): JsonResponse
|
|||
|
||||
$this->logService->logOperation(
|
||||
'admin',
|
||||
$request->admin->id,
|
||||
$this->admin->id,
|
||||
"Deleted LLM provider: {$providerName}"
|
||||
);
|
||||
|
||||
return $this->success(null, '提供商已删除。');
|
||||
return $this->success(null, 'LLM 提供商已刪除。');
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return $this->error(
|
||||
|
|
|
|||
|
|
@ -49,11 +49,7 @@ public function getAccessToken(Request $request): JsonResponse
|
|||
|
||||
$result = $this->tokenService->generateAccessToken($authTokenData);
|
||||
|
||||
return $this->success([
|
||||
'access_token' => $result['access_token'],
|
||||
'expires_in' => $result['expires_in'],
|
||||
'token_type' => $result['token_type'],
|
||||
]);
|
||||
return $this->success($result);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->error(
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@ public function __construct(
|
|||
) {}
|
||||
|
||||
/**
|
||||
* 发送 LLM 请求
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* 發送 LLM 請求
|
||||
*/
|
||||
public function request(Request $request): JsonResponse
|
||||
{
|
||||
|
|
@ -47,7 +44,6 @@ public function request(Request $request): JsonResponse
|
|||
|
||||
return $this->success([
|
||||
'response' => $result['response'],
|
||||
'usage' => $result['usage'],
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public function __construct(
|
|||
/**
|
||||
* 生成認證令牌
|
||||
*
|
||||
* @return array{token: string, expires_at: string}
|
||||
* @return array{auth_token: string, expires_at: string, client_id: int}
|
||||
*/
|
||||
public function generateAuthToken(Client $client, ?int $expiresInHours = null): array
|
||||
{
|
||||
|
|
@ -56,8 +56,9 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null):
|
|||
);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'auth_token' => $token,
|
||||
'expires_at' => $expiresAt->toIso8601String(),
|
||||
'client_id' => $client->id,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ public function validateAuthToken(string $token): ?array
|
|||
/**
|
||||
* 生成訪問令牌
|
||||
*
|
||||
* @return array{access_token: string, expires_in: int, token_type: string}
|
||||
* @return array{access_token: string, expires_in: int}
|
||||
*/
|
||||
public function generateAccessToken(array $authTokenData): array
|
||||
{
|
||||
|
|
@ -118,7 +119,6 @@ public function generateAccessToken(array $authTokenData): array
|
|||
return [
|
||||
'access_token' => $accessToken,
|
||||
'expires_in' => self::ACCESS_TOKEN_TTL,
|
||||
'token_type' => 'Bearer',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue