This commit is contained in:
Jethro Lin 2024-12-04 12:27:33 +08:00
parent c5258233a8
commit 9df4178800
6 changed files with 366 additions and 554 deletions

View file

@ -4,9 +4,11 @@
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
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;
@ -16,27 +18,36 @@
class AuthController extends Controller class AuthController extends Controller
{ {
use ApiResponse;
public function __construct( public function __construct(
private readonly LogService $logService private readonly LogService $logService,
) {} ) {}
/**
* 管理员登录
*
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse public function login(Request $request): JsonResponse
{ {
try { try {
$validated = $request->validate([ $validated = $request->validate([
'username' => 'required|string', 'email' => 'required|email',
'password' => 'required|string', 'password' => 'required|string',
]); ]);
if (!Auth::guard('admin')->attempt($validated)) { $admin = Admin::where('email', $validated['email'])->first();
return response()->json([
'error' => 'invalid_credentials', if (!$admin || !Hash::check($validated['password'], $admin->password)) {
'message' => '用户名或密码错误。', return $this->error(
], 401); ErrorCode::INVALID_CREDENTIALS,
ErrorCode::getMessage(ErrorCode::INVALID_CREDENTIALS)
);
} }
/** @var Admin $admin */ $token = $admin->createToken('admin-token')->plainTextToken;
$admin = Auth::guard('admin')->user();
$this->logService->logOperation( $this->logService->logOperation(
'admin', 'admin',
@ -44,52 +55,74 @@ public function login(Request $request): JsonResponse
'Admin logged in' 'Admin logged in'
); );
return response()->json([ return $this->success([
'id' => $admin->id, 'token' => $token,
'username' => $admin->username, 'admin' => [
'email' => $admin->email, 'id' => $admin->id,
'role' => $admin->role, 'name' => $admin->name,
'email' => $admin->email,
],
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error during admin login', [ Log::error('Error during admin login', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/**
* 管理员登出
*
* @param Request $request
* @return JsonResponse
*/
public function logout(Request $request): JsonResponse public function logout(Request $request): JsonResponse
{ {
/** @var Admin $admin */ try {
$admin = Auth::guard('admin')->user(); $admin = $request->user();
$admin->currentAccessToken()->delete();
Auth::guard('admin')->logout(); $this->logService->logOperation(
$request->session()->invalidate(); 'admin',
$request->session()->regenerateToken(); $admin->id,
'Admin logged out'
);
$this->logService->logOperation( return $this->success(null, '已成功登出。');
'admin',
$admin->id,
'Admin logged out'
);
return response()->json([ } catch (\Exception $e) {
'message' => '已成功退出登录。', Log::error('Error during admin logout', [
]); 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'admin_id' => $request->user()?->id,
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
} }
/**
* 修改管理员密码
*
* @param Request $request
* @return JsonResponse
*/
public function changePassword(Request $request): JsonResponse public function changePassword(Request $request): JsonResponse
{ {
try { try {
@ -98,46 +131,43 @@ public function changePassword(Request $request): JsonResponse
'new_password' => 'required|string|min:8|confirmed', 'new_password' => 'required|string|min:8|confirmed',
]); ]);
/** @var Admin $admin */ $admin = $request->user();
$admin = Auth::guard('admin')->user();
if (!Hash::check($validated['current_password'], $admin->password)) { if (!Hash::check($validated['current_password'], $admin->password)) {
return response()->json([ return $this->error(
'error' => 'invalid_password', ErrorCode::INVALID_CREDENTIALS,
'message' => '当前密码错误。', '当前密码错误。'
], 422); );
} }
$admin->update([ $admin->password = Hash::make($validated['new_password']);
'password' => Hash::make($validated['new_password']), $admin->save();
]);
$this->logService->logOperation( $this->logService->logOperation(
'admin', 'admin',
$admin->id, $admin->id,
'Changed password' 'Admin changed password'
); );
return response()->json([ return $this->success(null, '密码修改成功。');
'message' => '密码已成功修改。',
]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error changing admin password', [ Log::error('Error changing admin password', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'admin_id' => $request->user()?->id,
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
} }

View file

@ -4,10 +4,12 @@
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Client; use App\Models\Client;
use App\Services\LogService; use App\Services\LogService;
use App\Services\Auth\TokenService; use App\Services\Auth\TokenService;
use App\Traits\ApiResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -16,62 +18,30 @@
class ClientController extends Controller class ClientController extends Controller
{ {
use ApiResponse;
public function __construct( public function __construct(
private readonly LogService $logService, private readonly LogService $logService,
private readonly TokenService $tokenService, private readonly TokenService $tokenService,
) {} ) {}
/** /**
* Get client list * 获取客户列表
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @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 public function index(Request $request): JsonResponse
{ {
try { try {
$perPage = $request->input('per_page', 15); $perPage = $request->input('per_page', 15);
$perPage = min(max($perPage, 1), 100); // 限制每页数量在 1-100 之间 $perPage = min(max($perPage, 1), 100);
$search = $request->input('search'); $search = $request->input('search');
$status = $request->input('status'); $status = $request->input('status');
$query = Client::with('llmProvider:id,name,service_name') $query = Client::with('llmProvider:id,name,service_name')
->orderBy('created_at', 'desc'); ->orderBy('created_at', 'desc');
// 搜索条件
if ($search) { if ($search) {
$query->where(function ($q) use ($search) { $query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%") $q->where('name', 'like', "%{$search}%")
@ -79,22 +49,21 @@ public function index(Request $request): JsonResponse
}); });
} }
// 状态筛选
if ($status && in_array($status, ['active', 'inactive'])) { if ($status && in_array($status, ['active', 'inactive'])) {
$query->where('status', $status); $query->where('status', $status);
} }
$clients = $query->paginate($perPage); $clients = $query->paginate($perPage);
return response()->json([ return $this->paginate(
'data' => $clients->items(), $clients->items(),
'meta' => [ [
'current_page' => $clients->currentPage(), 'current_page' => $clients->currentPage(),
'per_page' => $clients->perPage(), 'per_page' => $clients->perPage(),
'total' => $clients->total(), 'total' => $clients->total(),
'total_pages' => $clients->lastPage(), 'total_pages' => $clients->lastPage(),
], ]
]); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error fetching clients', [ Log::error('Error fetching clients', [
@ -104,39 +73,18 @@ public function index(Request $request): JsonResponse
'status' => $status, 'status' => $status,
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Create new client * 创建客户
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @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 public function store(Request $request): JsonResponse
{ {
@ -194,14 +142,14 @@ public function store(Request $request): JsonResponse
"Created client: {$client->name}" "Created client: {$client->name}"
); );
return response()->json($client, 201); return $this->created($client);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error creating client', [ Log::error('Error creating client', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -209,46 +157,19 @@ public function store(Request $request): JsonResponse
'request_data' => $request->except(['api_token']), 'request_data' => $request->except(['api_token']),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Update client * 更新客户
* *
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return JsonResponse * @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 public function update(Request $request, int $id): JsonResponse
{ {
@ -305,10 +226,10 @@ public function update(Request $request, int $id): JsonResponse
// 如果状态改为 inactive检查是否有活跃的认证令牌 // 如果状态改为 inactive检查是否有活跃的认证令牌
if ($validated['status'] === 'inactive' && $client->status === 'active') { if ($validated['status'] === 'inactive' && $client->status === 'active') {
if ($client->authTokens()->where('status', 'active')->exists()) { if ($client->authTokens()->where('status', 'active')->exists()) {
return response()->json([ return $this->error(
'error' => 'active_tokens_exist', ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'message' => '该客户有活跃的认证令牌,请先停用所有令牌。', '该客户有活跃的认证令牌,请先停用所有令牌。'
], 422); );
} }
} }
@ -323,19 +244,19 @@ public function update(Request $request, int $id): JsonResponse
"Updated client: {$client->name}" "Updated client: {$client->name}"
); );
return response()->json($client); return $this->success($client);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::CLIENT_NOT_FOUND,
'message' => '客户不存在。', ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
], 404); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error updating client', [ Log::error('Error updating client', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -344,29 +265,19 @@ public function update(Request $request, int $id): JsonResponse
'request_data' => $request->except(['api_token']), 'request_data' => $request->except(['api_token']),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Delete client * 删除客户
* *
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return JsonResponse * @return JsonResponse
*
* Success response:
* {
* "message": "客户已删除。"
* }
*
* Error responses:
* - 404: 客户不存在
* - 422: 客户有活跃的认证令牌
* - 500: 服务器内部错误
*/ */
public function destroy(Request $request, int $id): JsonResponse public function destroy(Request $request, int $id): JsonResponse
{ {
@ -375,15 +286,13 @@ public function destroy(Request $request, int $id): JsonResponse
// 检查是否有活跃的认证令牌 // 检查是否有活跃的认证令牌
if ($client->authTokens()->where('status', 'active')->exists()) { if ($client->authTokens()->where('status', 'active')->exists()) {
return response()->json([ return $this->error(
'error' => 'client_in_use', ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'message' => '该客户有活跃的认证令牌,请先停用或删除令牌。', '该客户有活跃的认证令牌,请先停用或删除令牌。'
], 422); );
} }
$clientName = $client->name; $clientName = $client->name;
// 软删除客户记录
$client->delete(); $client->delete();
$this->logService->logOperation( $this->logService->logOperation(
@ -392,15 +301,13 @@ public function destroy(Request $request, int $id): JsonResponse
"Deleted client: {$clientName}" "Deleted client: {$clientName}"
); );
return response()->json([ return $this->success(null, '客户已删除。');
'message' => '客户已删除。',
]);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::CLIENT_NOT_FOUND,
'message' => '客户不存在。', ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
], 404); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error deleting client', [ Log::error('Error deleting client', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -408,41 +315,19 @@ public function destroy(Request $request, int $id): JsonResponse
'client_id' => $id, 'client_id' => $id,
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Generate auth token for client * 生成认证令牌
* *
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return JsonResponse * @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 public function generateAuthToken(Request $request, int $id): JsonResponse
{ {
@ -450,10 +335,10 @@ public function generateAuthToken(Request $request, int $id): JsonResponse
$client = Client::findOrFail($id); $client = Client::findOrFail($id);
if ($client->status !== 'active') { if ($client->status !== 'active') {
return response()->json([ return $this->error(
'error' => 'client_inactive', ErrorCode::CLIENT_INACTIVE,
'message' => '客户状态为非活跃,无法生成认证令牌。', ErrorCode::getMessage(ErrorCode::CLIENT_INACTIVE)
], 422); );
} }
// 验证过期时间 // 验证过期时间
@ -475,7 +360,7 @@ public function generateAuthToken(Request $request, int $id): JsonResponse
"Generated auth token for client: {$client->name}" "Generated auth token for client: {$client->name}"
); );
return response()->json([ return $this->success([
'auth_token' => $token->token, 'auth_token' => $token->token,
'expires_at' => $token->expires_at, 'expires_at' => $token->expires_at,
'client' => [ 'client' => [
@ -487,16 +372,16 @@ public function generateAuthToken(Request $request, int $id): JsonResponse
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::CLIENT_NOT_FOUND,
'message' => '客户不存在。', ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
], 404); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error generating auth token', [ Log::error('Error generating auth token', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -505,10 +390,10 @@ public function generateAuthToken(Request $request, int $id): JsonResponse
'request_data' => $request->except(['api_token']), 'request_data' => $request->except(['api_token']),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
} }

View file

@ -4,9 +4,11 @@
namespace App\Http\Controllers\Api\Admin; namespace App\Http\Controllers\Api\Admin;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\LlmProvider; use App\Models\LlmProvider;
use App\Services\LogService; use App\Services\LogService;
use App\Traits\ApiResponse;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -15,57 +17,50 @@
class LlmProviderController extends Controller class LlmProviderController extends Controller
{ {
use ApiResponse;
public function __construct( public function __construct(
private readonly LogService $logService private readonly LogService $logService,
) {} ) {}
/** /**
* Get LLM provider list * 获取 LLM 提供商列表
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*
* Query parameters:
* - page: int (optional) - Page number
* - per_page: int (optional) - Items per page
*
* Success response:
* {
* "data": [
* {
* "id": 1001,
* "name": "OpenAI",
* "service_name": "openai",
* "api_url": "https://api.openai.com/v1/chat/completions",
* "created_at": "2023-10-01T14:00:00Z"
* }
* ],
* "meta": {
* "current_page": 1,
* "per_page": 15,
* "total": 50,
* "total_pages": 4
* }
* }
*/ */
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
try { try {
$perPage = $request->input('per_page', 15); $perPage = $request->input('per_page', 15);
$perPage = min(max($perPage, 1), 100); // 限制每页数量在 1-100 之间 $perPage = min(max($perPage, 1), 100);
$search = $request->input('search');
$status = $request->input('status');
$providers = LlmProvider::orderBy('created_at', 'desc') $query = LlmProvider::query()->orderBy('created_at', 'desc');
->paginate($perPage);
return response()->json([ if ($search) {
'data' => $providers->items(), $query->where(function ($q) use ($search) {
'meta' => [ $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(), 'current_page' => $providers->currentPage(),
'per_page' => $providers->perPage(), 'per_page' => $providers->perPage(),
'total' => $providers->total(), 'total' => $providers->total(),
'total_pages' => $providers->lastPage(), 'total_pages' => $providers->lastPage(),
], ]
]); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error fetching LLM providers', [ Log::error('Error fetching LLM providers', [
@ -73,45 +68,18 @@ public function index(Request $request): JsonResponse
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Create new LLM provider * 创建 LLM 提供商
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*
* Request body:
* {
* "name": "string",
* "service_name": "string",
* "api_url": "string",
* "api_token": "string"
* }
*
* Success response (201):
* {
* "id": 1001,
* "name": "OpenAI",
* "service_name": "openai",
* "api_url": "https://api.openai.com/v1/chat/completions",
* "created_at": "2023-10-01T14:00:00Z"
* }
*
* Error response (422):
* {
* "error": "validation_error",
* "message": "请求参数验证失败。",
* "errors": {
* "name": ["提供商名称已存在。"],
* "api_url": ["API URL 格式不正确。"]
* }
* }
*/ */
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
@ -122,28 +90,29 @@ public function store(Request $request): JsonResponse
'string', 'string',
'max:100', 'max:100',
'unique:llm_providers', 'unique:llm_providers',
'regex:/^[\w\-\s]+$/u', // 只允许字母、数字、下划线、横线和空格 'regex:/^[\w\-\s]+$/u',
], ],
'service_name' => [ 'service_name' => [
'required', 'required',
'string', 'string',
'max:100', 'max:100',
'regex:/^[\w\-]+$/u', // 只允许字母、数字、下划线和横线 'regex:/^[\w\-]+$/u',
], ],
'api_url' => [ 'api_url' => [
'required', 'required',
'string', 'string',
'max:255', 'max:255',
'url', 'url',
'starts_with:https', // 强制使用 HTTPS 'starts_with:https',
], ],
'api_token' => 'required|string|max:255', 'api_token' => 'required|string|max:255',
], [ ], [
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空<EFBFBD><EFBFBD>。', 'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空。',
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。', 'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。', 'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
]); ]);
$validated['status'] = 'active';
$provider = LlmProvider::create($validated); $provider = LlmProvider::create($validated);
$this->logService->logOperation( $this->logService->logOperation(
@ -152,50 +121,34 @@ public function store(Request $request): JsonResponse
"Created LLM provider: {$provider->name}" "Created LLM provider: {$provider->name}"
); );
return response()->json($provider, 201); return $this->created($provider);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error creating LLM provider', [ Log::error('Error creating LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'request_data' => $request->except(['api_token']),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Update LLM provider * 更新 LLM 提供商
* *
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return JsonResponse * @return JsonResponse
*
* Request body:
* {
* "name": "string",
* "service_name": "string",
* "api_url": "string",
* "api_token": "string"
* }
*
* Success response:
* {
* "id": 1001,
* "name": "OpenAI Updated",
* "service_name": "openai",
* "api_url": "https://api.openai.com/v1/chat/completions",
* "updated_at": "2023-10-02T14:00:00Z"
* }
*/ */
public function update(Request $request, int $id): JsonResponse public function update(Request $request, int $id): JsonResponse
{ {
@ -208,28 +161,44 @@ public function update(Request $request, int $id): JsonResponse
'string', 'string',
'max:100', 'max:100',
"unique:llm_providers,name,{$id}", "unique:llm_providers,name,{$id}",
'regex:/^[\w\-\s]+$/u', // 只允许字母、数字、下划线、横线和空格 'regex:/^[\w\-\s]+$/u',
], ],
'service_name' => [ 'service_name' => [
'required', 'required',
'string', 'string',
'max:100', 'max:100',
'regex:/^[\w\-]+$/u', // 只允许字母、数字、下划线和横线 'regex:/^[\w\-]+$/u',
], ],
'api_url' => [ 'api_url' => [
'required', 'required',
'string', 'string',
'max:255', 'max:255',
'url', 'url',
'starts_with:https', // 强制使用 HTTPS 'starts_with:https',
], ],
'api_token' => 'required|string|max:255', 'api_token' => 'required|string|max:255',
'status' => [
'required',
'string',
'in:active,inactive',
],
], [ ], [
'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。', 'name.regex' => '提供商名称只能包含字母、数字、下划线、横线和空格。',
'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。', 'service_name.regex' => '服务名称只能包含字母、数字、下划线和横线。',
'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。', 'api_url.starts_with' => 'API URL 必须使用 HTTPS 协议。',
'status.in' => '状态只能是 active 或 inactive。',
]); ]);
// 如果状态改为 inactive检查是否有客户正在使用
if ($validated['status'] === 'inactive' && $provider->status === 'active') {
if ($provider->clients()->exists()) {
return $this->error(
ErrorCode::RESOURCE_IN_USE,
'该提供商正在被客户使用,无法停用。'
);
}
}
$provider->update($validated); $provider->update($validated);
$this->logService->logOperation( $this->logService->logOperation(
@ -238,55 +207,52 @@ public function update(Request $request, int $id): JsonResponse
"Updated LLM provider: {$provider->name}" "Updated LLM provider: {$provider->name}"
); );
return response()->json($provider); return $this->success($provider);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'errors' => $e->errors(), $e->errors()
], 422); );
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::RESOURCE_NOT_FOUND,
'message' => 'LLM 提供商不存在。', ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND)
], 404); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error updating LLM provider', [ Log::error('Error updating LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'provider_id' => $id,
'request_data' => $request->except(['api_token']),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
/** /**
* Delete LLM provider * 删除 LLM 提供商
* *
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return JsonResponse * @return JsonResponse
*
* Success response:
* {
* "message": "提供商已删除。"
* }
*/ */
public function destroy(Request $request, int $id): JsonResponse public function destroy(Request $request, int $id): JsonResponse
{ {
try { try {
$provider = LlmProvider::findOrFail($id); $provider = LlmProvider::findOrFail($id);
// 检查是否有客户正在使用该提供商 // 检查是否有客户正在使用
if ($provider->clients()->exists()) { if ($provider->clients()->exists()) {
return response()->json([ return $this->error(
'error' => 'provider_in_use', ErrorCode::RESOURCE_IN_USE,
'message' => '该提供商正在被客户使用,无法删除。', '该提供商正在被客户使用,无法删除。'
], 422); );
} }
$providerName = $provider->name; $providerName = $provider->name;
@ -298,15 +264,13 @@ public function destroy(Request $request, int $id): JsonResponse
"Deleted LLM provider: {$providerName}" "Deleted LLM provider: {$providerName}"
); );
return response()->json([ return $this->success(null, '提供商已删除。');
'message' => '提供商已删除。',
]);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::RESOURCE_NOT_FOUND,
'message' => 'LLM 提供商不存在。', ErrorCode::getMessage(ErrorCode::RESOURCE_NOT_FOUND)
], 404); );
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error deleting LLM provider', [ Log::error('Error deleting LLM provider', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -314,10 +278,10 @@ public function destroy(Request $request, int $id): JsonResponse
'provider_id' => $id, 'provider_id' => $id,
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
} }

View file

@ -4,9 +4,10 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Rules\ThrottleAuthToken;
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;
@ -14,74 +15,53 @@
class AuthController extends Controller class AuthController extends Controller
{ {
use ApiResponse;
public function __construct( public function __construct(
private readonly TokenService $tokenService private readonly TokenService $tokenService,
) {} ) {}
/** /**
* Get access token using auth token * 获取访问令牌
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*
* Request body:
* {
* "auth_token": "string"
* }
*
* Success response:
* {
* "access_token": "string",
* "expires_in": 3600
* }
*
* Error response:
* {
* "error": "error_code",
* "message": "错误描述"
* }
*/ */
public function getAccessToken(Request $request): JsonResponse public function getAccessToken(Request $request): JsonResponse
{ {
try { try {
$validated = $request->validate([ $validated = $request->validate([
'auth_token' => [ 'auth_token' => 'required|string|size:64',
'required',
'string',
'size:64',
new ThrottleAuthToken,
],
]); ]);
$authToken = $this->tokenService->validateAuthToken($validated['auth_token']); $result = $this->tokenService->generateAccessToken($validated['auth_token']);
if (!$authToken) { return $this->success([
return response()->json([ 'access_token' => $result['token'],
'error' => 'invalid_auth_token', 'expires_at' => $result['expires_at'],
'message' => '认证令牌无效。', ]);
], 401);
}
$result = $this->tokenService->generateAccessToken($authToken);
return response()->json($result);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'invalid_request', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数缺失或格式错误。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'details' => $e->errors(), $e->errors()
], 400); );
} catch (\InvalidArgumentException $e) {
return $this->error(
ErrorCode::TOKEN_INVALID,
$e->getMessage()
);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error generating access token', [ Log::error('Error generating access token', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
return response()->json([ return $this->error(
'error' => 'server_error', ErrorCode::SERVER_ERROR,
'message' => '服务器内部错误。', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
], 500); );
} }
} }
} }

View file

@ -4,146 +4,80 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Constants\ErrorCode;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Client; use App\Services\LlmService;
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\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class LlmController extends Controller class LlmController extends Controller
{ {
private const REQUEST_TIMEOUT = 30; // 请求超时时间(秒) use ApiResponse;
private const MAX_ATTEMPTS = 60; // 每分钟最大请求次数
private const DECAY_MINUTES = 1; // 重置时间(分钟)
public function __construct( public function __construct(
private readonly LogService $logService private readonly LlmService $llmService,
) {} ) {}
/** /**
* Send prompt request to LLM provider * 发送 LLM 请求
* *
* @param Request $request * @param Request $request
* @return JsonResponse * @return JsonResponse
*
* Request headers:
* - Content-Type: application/json
* - Authorization: Bearer {access_token}
*
* Request body:
* {
* "prompt": "string"
* }
*
* Success response:
* {
* "response": "LLM 提供商返回的响应内容"
* }
*
* Error response:
* {
* "error": "error_code",
* "message": "错误描述"
* }
*/ */
public function request(Request $request): JsonResponse public function request(Request $request): JsonResponse
{ {
try { try {
$clientId = $request->get('client_id');
// 检查请求频率限制
$key = 'llm_request_' . $clientId;
if (RateLimiter::tooManyAttempts($key, self::MAX_ATTEMPTS)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'error' => 'too_many_requests',
'message' => "请求过于频繁,请在 {$seconds} 秒后重试。",
], 429);
}
$validated = $request->validate([ $validated = $request->validate([
'prompt' => 'required|string|max:4096', 'prompt' => 'required|string|max:4000',
'max_tokens' => 'nullable|integer|min:1|max:4000',
'temperature' => 'nullable|numeric|min:0|max:2',
'top_p' => 'nullable|numeric|min:0|max:1',
'frequency_penalty' => 'nullable|numeric|min:-2|max:2',
'presence_penalty' => 'nullable|numeric|min:-2|max:2',
]); ]);
$client = Client::with('llmProvider')->findOrFail($clientId); $result = $this->llmService->sendRequest(
$provider = $client->llmProvider; $request->client,
$validated['prompt'],
// Send request to LLM provider with timeout array_filter($validated, fn($key) => $key !== 'prompt', ARRAY_FILTER_USE_KEY)
$response = Http::timeout(self::REQUEST_TIMEOUT)
->withToken($provider->api_token)
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post($provider->api_url, [
'prompt' => $validated['prompt'],
]);
if (!$response->successful()) {
Log::error('LLM provider request failed', [
'status' => $response->status(),
'body' => $response->body(),
'provider' => $provider->name,
]);
throw new \Exception('LLM provider request failed: ' . $response->body());
}
// 记录成功的请求
RateLimiter::hit($key, self::DECAY_MINUTES * 60);
$this->logService->logOperation(
'client',
$client->id,
'Sent prompt to LLM provider: ' . $provider->name
); );
return response()->json([ return $this->success([
'response' => $response->json(), 'response' => $result['response'],
'usage' => $result['usage'],
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
return response()->json([ return $this->error(
'error' => 'validation_error', ErrorCode::VALIDATION_ERROR,
'message' => '请求参数验证失败。', ErrorCode::getMessage(ErrorCode::VALIDATION_ERROR),
'details' => $e->errors(), $e->errors()
], 422); );
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { } catch (\InvalidArgumentException $e) {
return response()->json([ return $this->error(
'error' => 'not_found', ErrorCode::INVALID_REQUEST_FORMAT,
'message' => '客户用户不存在。', $e->getMessage()
], 404); );
} catch (\Illuminate\Http\Client\ConnectionException $e) { } catch (\RuntimeException $e) {
Log::error('LLM provider connection timeout', [ return $this->error(
'error' => $e->getMessage(), ErrorCode::PROVIDER_ERROR,
'trace' => $e->getTraceAsString(), $e->getMessage()
]); );
return response()->json([
'error' => 'provider_timeout',
'message' => 'LLM 提供商响应超时,请稍后重试。',
], 504);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Error processing LLM request', [ Log::error('Error processing LLM request', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'client_id' => $request->client->id,
'request_data' => $request->except(['prompt']),
]); ]);
if (str_contains($e->getMessage(), 'LLM provider request failed')) { return $this->error(
return response()->json([ ErrorCode::SERVER_ERROR,
'error' => 'provider_error', ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
'message' => 'LLM 提供商服务异常,请稍后重试。', );
], 502);
}
return response()->json([
'error' => 'server_error',
'message' => '服务器内部错误。',
], 500);
} }
} }
} }

View file

@ -7,27 +7,24 @@
class Kernel extends HttpKernel class Kernel extends HttpKernel
{ {
/** /**
* The application's route middleware. * The application's global HTTP middleware stack.
* *
* These middleware may be assigned to groups or used individually. * These middleware are run during every request to your application.
* *
* @var array<string, class-string|string> * @var array<int, class-string|string>
*/ */
protected $routeMiddleware = [ protected $middleware = [
'auth' => \App\Http\Middleware\Authenticate::class, // \App\Http\Middleware\TrustHosts::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, \App\Http\Middleware\TrustProxies::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\Http\Middleware\HandleCors::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, \App\Http\Middleware\TrimStrings::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
]; ];
/** /**
* The application's middleware groups. * The application's route middleware groups.
* *
* @var array<string, array<int, class-string|string>> * @var array<string, array<int, class-string|string>>
*/ */
@ -47,4 +44,26 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
]; ];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth.access_token' => \App\Http\Middleware\ValidateAccessToken::class,
'admin' => \App\Http\Middleware\AdminAuthenticate::class,
];
} }