llmbackend/app/Http/Controllers/Api/Admin/ClientController.php
Jethro Lin 31b69e318a - [4.3.2 客户用户管理](#432-客户用户管理)
- [4.3.2.1 新增客户用户](#4321-新增客户用户)
       - [4.3.2.2 修改客户用户](#4322-修改客户用户)
       - [4.3.2.3 删除客户用户](#4323-删除客户用户)
       - [4.3.2.4 获取客户用户列表](#4324-获取客户用户列表)
     - [4.3.3 生成认证令牌](#433-生成认证令牌)
2024-12-04 12:10:15 +08:00

514 lines
16 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\Http\Controllers\Controller;
use App\Models\Client;
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\Log;
use Illuminate\Validation\ValidationException;
class ClientController extends Controller
{
public function __construct(
private readonly LogService $logService,
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
{
try {
$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(),
],
]);
} catch (\Exception $e) {
Log::error('Error fetching clients', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'search' => $search,
'status' => $status,
]);
return response()->json([
'error' => 'server_error',
'message' => '服务器内部错误。',
], 500);
}
}
/**
* 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 = 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 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,
"Updated client: {$client->name}"
);
return response()->json($client);
} catch (ValidationException $e) {
return response()->json([
'error' => 'validation_error',
'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([
'error' => 'server_error',
'message' => '服务器内部错误。',
], 500);
}
}
/**
* 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 = 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(
'admin',
$request->admin->id,
"Deleted client: {$clientName}"
);
return response()->json([
'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([
'error' => 'server_error',
'message' => '服务器内部错误。',
], 500);
}
}
/**
* 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 = 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|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 response()->json([
'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 response()->json([
'error' => 'validation_error',
'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([
'error' => 'server_error',
'message' => '服务器内部错误。',
], 500);
}
}
}