From 31b69e318adaf806bc3857ef72a796549f5b4a6f Mon Sep 17 00:00:00 2001 From: Jethro Lin Date: Wed, 4 Dec 2024 12:10:15 +0800 Subject: [PATCH] =?UTF-8?q?=20=20=20=20=20-=20[4.3.2=20=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86](#432-=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86)=20=20=20=20=20=20=20=20-?= =?UTF-8?q?=20[4.3.2.1=20=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7=E7=94=A8?= =?UTF-8?q?=E6=88=B7](#4321-=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7=E7=94=A8?= =?UTF-8?q?=E6=88=B7)=20=20=20=20=20=20=20=20-=20[4.3.2.2=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AE=A2=E6=88=B7=E7=94=A8=E6=88=B7](#4322-=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AE=A2=E6=88=B7=E7=94=A8=E6=88=B7)=20=20=20=20=20?= =?UTF-8?q?=20=20=20-=20[4.3.2.3=20=E5=88=A0=E9=99=A4=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=94=A8=E6=88=B7](#4323-=E5=88=A0=E9=99=A4=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=94=A8=E6=88=B7)=20=20=20=20=20=20=20=20-=20[4.3.2.4=20?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=AE=A2=E6=88=B7=E7=94=A8=E6=88=B7=E5=88=97?= =?UTF-8?q?=E8=A1=A8](#4324-=E8=8E=B7=E5=8F=96=E5=AE=A2=E6=88=B7=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=88=97=E8=A1=A8)=20=20=20=20=20=20-=20[4.3.3=20?= =?UTF-8?q?=E7=94=9F=E6=88=90=E8=AE=A4=E8=AF=81=E4=BB=A4=E7=89=8C](#433-?= =?UTF-8?q?=E7=94=9F=E6=88=90=E8=AE=A4=E8=AF=81=E4=BB=A4=E7=89=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/Admin/ClientController.php | 430 +++++++++++++++--- routes/api.php | 2 +- 2 files changed, 372 insertions(+), 60 deletions(-) diff --git a/app/Http/Controllers/Api/Admin/ClientController.php b/app/Http/Controllers/Api/Admin/ClientController.php index 8a0f5a1..dbb0227 100644 --- a/app/Http/Controllers/Api/Admin/ClientController.php +++ b/app/Http/Controllers/Api/Admin/ClientController.php @@ -6,11 +6,11 @@ use App\Http\Controllers\Controller; use App\Models\Client; -use App\Services\Auth\TokenService; use App\Services\LogService; +use App\Services\Auth\TokenService; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; @@ -18,54 +18,90 @@ class ClientController extends Controller { public function __construct( private readonly LogService $logService, - private readonly TokenService $tokenService + private readonly TokenService $tokenService, ) {} + /** + * Get client list + * + * @param Request $request + * @return JsonResponse + * + * Query parameters: + * - page: int (optional) - 页码,默认 1 + * - per_page: int (optional) - 每页数量,默认 15 + * - search: string (optional) - 搜索关键词,支持名称和邮箱搜索 + * - status: string (optional) - 状态筛选:active 或 inactive + * + * Success response: + * { + * "data": [ + * { + * "id": 1001, + * "name": "Client Name", + * "email": "client@example.com", + * "status": "active", + * "llm_provider_id": 1, + * "rate_limit": 100, + * "timeout": 30, + * "created_at": "2023-10-01T14:00:00Z", + * "llm_provider": { + * "id": 1, + * "name": "OpenAI", + * "service_name": "openai" + * } + * } + * ], + * "meta": { + * "current_page": 1, + * "per_page": 15, + * "total": 50, + * "total_pages": 4 + * } + * } + */ public function index(Request $request): JsonResponse - { - $admin = $request->admin; - $query = $admin->isSuperAdmin() ? Client::query() : $admin->clients(); - - $clients = $query->with('llmProvider')->get(); - return response()->json($clients); - } - - public function store(Request $request): JsonResponse { try { - $validated = $request->validate([ - 'name' => 'required|string|max:100', - 'llm_provider_id' => 'required|exists:llm_providers,id', + $perPage = $request->input('per_page', 15); + $perPage = min(max($perPage, 1), 100); // 限制每页数量在 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 response()->json([ + 'data' => $clients->items(), + 'meta' => [ + 'current_page' => $clients->currentPage(), + 'per_page' => $clients->perPage(), + 'total' => $clients->total(), + 'total_pages' => $clients->lastPage(), + ], ]); - DB::beginTransaction(); - - $client = Client::create($validated); - - // Associate client with admin - $request->admin->clients()->attach($client->id); - - $this->logService->logOperation( - 'admin', - $request->admin->id, - "Created client: {$client->name}" - ); - - DB::commit(); - return response()->json($client, 201); - - } catch (ValidationException $e) { - DB::rollBack(); - return response()->json([ - 'error' => 'validation_error', - 'message' => '请求参数验证失败。', - 'errors' => $e->errors(), - ], 422); } catch (\Exception $e) { - DB::rollBack(); - Log::error('Error creating client', [ + Log::error('Error fetching clients', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'search' => $search, + 'status' => $status, ]); return response()->json([ @@ -75,18 +111,212 @@ public function store(Request $request): JsonResponse } } + /** + * Create new client + * + * @param Request $request + * @return JsonResponse + * + * Request body: + * { + * "name": "string", + * "email": "string", + * "llm_provider_id": "integer", + * "rate_limit": "integer", + * "timeout": "integer" + * } + * + * Success response (201): + * { + * "id": 1001, + * "name": "Client Name", + * "email": "client@example.com", + * "status": "active", + * "llm_provider_id": 1, + * "rate_limit": 100, + * "timeout": 30, + * "created_at": "2023-10-01T14:00:00Z" + * } + */ + 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 response()->json($client, 201); + + } catch (ValidationException $e) { + return response()->json([ + 'error' => 'validation_error', + 'message' => '请求参数验证失败。', + 'errors' => $e->errors(), + ], 422); + } catch (\Exception $e) { + Log::error('Error creating client', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + 'request_data' => $request->except(['api_token']), + ]); + + return response()->json([ + 'error' => 'server_error', + 'message' => '服务器内部错误。', + ], 500); + } + } + + /** + * Update client + * + * @param Request $request + * @param int $id + * @return JsonResponse + * + * Request body: + * { + * "name": "string", + * "email": "string", + * "llm_provider_id": "integer", + * "rate_limit": "integer", + * "timeout": "integer", + * "status": "string" + * } + * + * Success response: + * { + * "id": 1001, + * "name": "Client Name", + * "email": "client@example.com", + * "status": "active", + * "llm_provider_id": 1, + * "rate_limit": 100, + * "timeout": 30, + * "updated_at": "2023-10-02T14:00:00Z", + * "llm_provider": { + * "id": 1, + * "name": "OpenAI", + * "service_name": "openai" + * } + * } + */ public function update(Request $request, int $id): JsonResponse { try { - $client = $this->getAuthorizedClient($request->admin, $id); + $client = Client::findOrFail($id); $validated = $request->validate([ - 'name' => 'required|string|max:100', - 'llm_provider_id' => 'required|exists:llm_providers,id', + '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 response()->json([ + 'error' => 'active_tokens_exist', + 'message' => '该客户有活跃的认证令牌,请先停用所有令牌。', + ], 422); + } + } + $client->update($validated); + // 加载 LLM 提供商信息 + $client->load('llmProvider:id,name,service_name'); + $this->logService->logOperation( 'admin', $request->admin->id, @@ -101,10 +331,17 @@ public function update(Request $request, int $id): JsonResponse 'message' => '请求参数验证失败。', 'errors' => $e->errors(), ], 422); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'error' => 'not_found', + 'message' => '客户不存在。', + ], 404); } catch (\Exception $e) { Log::error('Error updating client', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + 'request_data' => $request->except(['api_token']), ]); return response()->json([ @@ -114,12 +351,39 @@ public function update(Request $request, int $id): JsonResponse } } + /** + * Delete client + * + * @param Request $request + * @param int $id + * @return JsonResponse + * + * Success response: + * { + * "message": "客户已删除。" + * } + * + * Error responses: + * - 404: 客户不存在 + * - 422: 客户有活跃的认证令牌 + * - 500: 服务器内部错误 + */ public function destroy(Request $request, int $id): JsonResponse { try { - $client = $this->getAuthorizedClient($request->admin, $id); + $client = Client::findOrFail($id); + + // 检查是否有活跃的认证令牌 + if ($client->authTokens()->where('status', 'active')->exists()) { + return response()->json([ + 'error' => 'client_in_use', + 'message' => '该客户有活跃的认证令牌,请先停用或删除令牌。', + ], 422); + } $clientName = $client->name; + + // 软删除客户记录 $client->delete(); $this->logService->logOperation( @@ -129,13 +393,19 @@ public function destroy(Request $request, int $id): JsonResponse ); return response()->json([ - 'message' => '客户用户已删除。', + 'message' => '客户已删除。', ]); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'error' => 'not_found', + 'message' => '客户不存在。', + ], 404); } catch (\Exception $e) { Log::error('Error deleting client', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), + 'client_id' => $id, ]); return response()->json([ @@ -145,18 +415,58 @@ public function destroy(Request $request, int $id): JsonResponse } } + /** + * Generate auth token for client + * + * @param Request $request + * @param int $id + * @return JsonResponse + * + * Request body: + * { + * "expires_in_days": int (optional) // 令牌有效期(天),默认 30 天 + * } + * + * Success response: + * { + * "auth_token": "string", // 认证令牌 + * "expires_at": "2024-10-01T14:00:00Z", // 过期时间 + * "client": { // 客户信息 + * "id": 1001, + * "name": "Client Name", + * "email": "client@example.com", + * "status": "active" + * } + * } + * + * Error responses: + * - 404: 客户不存在 + * - 422: 客户状态非活跃 + * - 500: 服务器内部错误 + */ public function generateAuthToken(Request $request, int $id): JsonResponse { try { - $client = $this->getAuthorizedClient($request->admin, $id); + $client = Client::findOrFail($id); + if ($client->status !== 'active') { + return response()->json([ + 'error' => 'client_inactive', + 'message' => '客户状态为非活跃,无法生成认证令牌。', + ], 422); + } + + // 验证过期时间 $validated = $request->validate([ - 'expires_in_days' => 'nullable|integer|min:1', + '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'] ?? null + $validated['expires_in_days'] ?? 30 ); $this->logService->logOperation( @@ -166,10 +476,14 @@ public function generateAuthToken(Request $request, int $id): JsonResponse ); return response()->json([ - 'client_id' => $client->id, 'auth_token' => $token->token, - 'created_at' => $token->created_at, 'expires_at' => $token->expires_at, + 'client' => [ + 'id' => $client->id, + 'name' => $client->name, + 'email' => $client->email, + 'status' => $client->status, + ], ]); } catch (ValidationException $e) { @@ -178,10 +492,17 @@ public function generateAuthToken(Request $request, int $id): JsonResponse 'message' => '请求参数验证失败。', 'errors' => $e->errors(), ], 422); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'error' => 'not_found', + 'message' => '客户不存在。', + ], 404); } 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 response()->json([ @@ -190,13 +511,4 @@ public function generateAuthToken(Request $request, int $id): JsonResponse ], 500); } } - - private function getAuthorizedClient($admin, int $id): Client - { - $query = $admin->isSuperAdmin() ? Client::query() : $admin->clients(); - - $client = $query->findOrFail($id); - - return $client; - } } diff --git a/routes/api.php b/routes/api.php index f0beb59..ae2e703 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,7 +16,7 @@ }); // Admin authentication routes -Route::prefix('admin')->group(function () { +Route::middleware(['auth:sanctum', 'admin'])->prefix('admin')->group(function () { Route::post('login', [AdminAuthController::class, 'login']); Route::middleware('auth.admin')->group(function () {