main: 用户管理和会话功能初始实现

- 添加用户管理功能的测试,包括创建、更新、停用、激活用户及用户登录 JWT 测试
- 提供用户管理相关的请求验证类与控制器
- 引入 CORS 配置信息,支持跨域请求
- 添加数据库播种器以便创建根用户
- 配置 API 默认使用 JWT 认证
- 添加聊天会话和消息的模型、迁移文件及关联功能
This commit is contained in:
2025-12-14 17:49:08 +08:00
parent e28318b4ec
commit c6d6534b63
36 changed files with 2119 additions and 16 deletions

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Services;
use App\Enums\ChatSessionStatus;
use App\Exceptions\ChatSessionStatusException;
use App\Models\ChatSession;
use App\Models\Message;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ChatService
{
public function createSession(string $name = null): ChatSession
{
return ChatSession::create([
'session_id' => (string) Str::uuid(),
'session_name' => $name,
'status' => ChatSessionStatus::OPEN,
'last_seq' => 0,
]);
}
public function getSession(string $sessionId): ChatSession
{
return ChatSession::query()->whereKey($sessionId)->firstOrFail();
}
/**
* @param array<string, mixed> $dto
*/
public function appendMessage(array $dto): Message
{
return DB::transaction(function () use ($dto) {
/** @var ChatSession $session */
$session = ChatSession::query()
->whereKey($dto['session_id'])
->lockForUpdate()
->firstOrFail();
$this->ensureCanAppend($session, $dto['role'], $dto['type']);
$dedupeKey = $dto['dedupe_key'] ?? null;
if ($dedupeKey) {
$existing = Message::query()
->where('session_id', $session->session_id)
->where('dedupe_key', $dedupeKey)
->first();
if ($existing) {
return $existing;
}
}
$newSeq = $session->last_seq + 1;
$message = new Message([
'message_id' => (string) Str::uuid(),
'session_id' => $session->session_id,
'role' => $dto['role'],
'type' => $dto['type'],
'content' => $dto['content'] ?? null,
'payload' => $dto['payload'] ?? null,
'reply_to' => $dto['reply_to'] ?? null,
'dedupe_key' => $dedupeKey,
'seq' => $newSeq,
'created_at' => now(),
]);
try {
$message->save();
} catch (QueryException $e) {
if ($this->isUniqueConstraint($e) && $dedupeKey) {
$existing = Message::query()
->where('session_id', $session->session_id)
->where('dedupe_key', $dedupeKey)
->first();
if ($existing) {
return $existing;
}
}
throw $e;
}
$session->update([
'last_seq' => $newSeq,
'updated_at' => now(),
]);
return $message;
});
}
public function listMessagesBySeq(string $sessionId, int $afterSeq, int $limit = 50): Collection
{
$this->getSession($sessionId); // ensure exists
return Message::query()
->where('session_id', $sessionId)
->where('seq', '>', $afterSeq)
->orderBy('seq')
->limit($limit)
->get();
}
private function ensureCanAppend(ChatSession $session, string $role, string $type): void
{
if ($session->status === ChatSessionStatus::CLOSED) {
$allowed = $role === Message::ROLE_SYSTEM && in_array($type, ['run.status', 'error'], true);
if (! $allowed) {
throw new ChatSessionStatusException('Session is closed');
}
}
if ($session->status === ChatSessionStatus::LOCKED) {
if ($role === Message::ROLE_USER && $type === 'user.prompt') {
throw new ChatSessionStatusException('Session is locked');
}
}
}
private function isUniqueConstraint(QueryException $e): bool
{
$sqlState = $e->getCode() ?: ($e->errorInfo[0] ?? null);
return $sqlState === '23505';
}
}