llmbackend/app/Http/Controllers/Api/Admin/ClientController.php
2024-12-04 13:32:14 +08:00

473 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller;
use App\Models\Client;
use App\Services\LogService;
use App\Services\Auth\TokenService;
use App\Traits\ApiResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Redis;
class ClientController extends Controller
{
use ApiResponse;
public function __construct(
private readonly LogService $logService,
private readonly TokenService $tokenService,
) {}
/**
* 获取客户列表
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
try {
$perPage = $request->input('per_page', 15);
$perPage = min(max($perPage, 1), 100);
$search = $request->input('search');
$status = $request->input('status');
$query = Client::with('llmProvider:id,name,service_name')
->orderBy('created_at', 'desc');
if ($search) {
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
if ($status && in_array($status, ['active', 'inactive'])) {
$query->where('status', $status);
}
$clients = $query->paginate($perPage);
return $this->paginate(
$clients->items(),
[
'current_page' => $clients->currentPage(),
'per_page' => $clients->perPage(),
'total' => $clients->total(),
'total_pages' => $clients->lastPage(),
]
);
} catch (\Exception $e) {
Log::error('Error fetching clients', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'search' => $search,
'status' => $status,
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 创建客户
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request): JsonResponse
{
try {
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
'regex:/^[\w\-\s]+$/u',
],
'email' => [
'required',
'string',
'email',
'max:255',
'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',
],
], [
'name.regex' => '客户名称只能包含字母、数字、下划线、横线和空格。',
'email.unique' => '该邮箱地址已被使用。',
'llm_provider_id.exists' => '选择的 LLM 提供商不存在。',
'rate_limit.min' => '速率限制不能小于 1。',
'rate_limit.max' => '速率限制不能大于 1000。',
'timeout.min' => '超时时间不能小于 1 秒。',
'timeout.max' => '超时时间不能大于 300 秒。',
]);
$validated['status'] = 'active';
$client = Client::create($validated);
// 加载 LLM 提供商信息
$client->load('llmProvider:id,name,service_name');
$this->logService->logOperation(
'admin',
$request->admin->id,
"Created client: {$client->name}"
);
return $this->created($client);
} 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->except(['api_token']),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 更新客户
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function update(Request $request, int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
$validated = $request->validate([
'name' => [
'required',
'string',
'max:100',
'regex:/^[\w\-\s]+$/u',
],
'email' => [
'required',
'string',
'email',
'max:255',
"unique:clients,email,{$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',
],
], [
'name.regex' => '客户名称只能包含字母、数字、下划线、横线和空格。',
'email.unique' => '该邮箱地址已被使用。',
'llm_provider_id.exists' => '选择的 LLM 提供商不存在。',
'rate_limit.min' => '速率限制不能小于 1。',
'rate_limit.max' => '速率限制不能大于 1000。',
'timeout.min' => '超时时间不能小于 1 秒。',
'timeout.max' => '超时时间不能大于 300 秒。',
'status.in' => '状态只能是 active 或 inactive。',
]);
// 如果状态改为 inactive检查是否有活跃的认证令牌
if ($validated['status'] === 'inactive' && $client->status === 'active') {
if ($client->authTokens()->where('status', 'active')->exists()) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'该客户有活跃的认证令牌,请先停用所有令牌。'
);
}
}
$client->update($validated);
// 加载 LLM 提供商信息
$client->load('llmProvider:id,name,service_name');
$this->logService->logOperation(
'admin',
$request->admin->id,
"Updated client: {$client->name}"
);
return $this->success($client);
} 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 updating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'request_data' => $request->except(['api_token']),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 檢查客戶是否有活躍的令牌
*/
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;
}
/**
* 停用客戶
*/
public function deactivate(int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法停用。'
);
}
$client->update(['status' => Client::STATUS_INACTIVE]);
$this->logService->logOperation(
'admin',
$this->admin->id,
"Deactivated client: {$client->name}"
);
return $this->success();
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error deactivating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 刪除客戶
*/
public function destroy(int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法刪除。'
);
}
$client->delete();
$this->logService->logOperation(
'admin',
$this->admin->id,
"Deleted client: {$client->name}"
);
return $this->success();
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error deleting client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 生成认证令牌
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function generateAuthToken(Request $request, int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if ($client->status !== 'active') {
return $this->error(
ErrorCode::CLIENT_INACTIVE,
ErrorCode::getMessage(ErrorCode::CLIENT_INACTIVE)
);
}
// 验证过期时间
$validated = $request->validate([
'expires_in_days' => 'nullable|integer|min:1|max:365',
], [
'expires_in_days.min' => '令牌有效期不能小于 1 天。',
'expires_in_days.max' => '令牌有效期不能超过 365 天。',
]);
$token = $this->tokenService->generateAuthToken(
$client,
$validated['expires_in_days'] ?? 30
);
$this->logService->logOperation(
'admin',
$request->admin->id,
"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,
'email' => $client->email,
'status' => $client->status,
],
]);
} 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 generating auth token', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'client_id' => $id,
'request_data' => $request->except(['api_token']),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
}