main: 增强会话功能,支持消息管理和接口文档

- 添加 `last_message_id` 字段至 `chat_sessions` 表,更新其关联索引
- 实现会话更新接口,支持修改名称与状态并添加验证逻辑
- 增加会话列表接口,支持状态过滤与关键字查询
- 提供会话和消息相关的资源类和请求验证类
- 扩展 `ChatService` 服务层逻辑以处理会话更新和消息附加
- 编写测试用例以验证新功能的正确性
- 增加接口文档及 OpenAPI 规范文件,覆盖新增功能
- 更新数据库播种器,添加默认用户
This commit is contained in:
2025-12-14 20:20:27 +08:00
parent c6d6534b63
commit 6356baacc0
14 changed files with 852 additions and 4 deletions

View File

@@ -6,6 +6,8 @@ use App\Enums\ChatSessionStatus;
use App\Exceptions\ChatSessionStatusException;
use App\Models\ChatSession;
use App\Models\Message;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -13,6 +15,12 @@ use Illuminate\Support\Str;
class ChatService
{
/**
* 创建一个新聊天会话。
* @param string|null $name
* @return ChatSession
*/
public function createSession(string $name = null): ChatSession
{
return ChatSession::create([
@@ -23,13 +31,31 @@ class ChatService
]);
}
/**
* 根据会话ID获取聊天会话对象
*
* @param string $sessionId 会话唯一标识符
* @return ChatSession 返回对应的聊天会话对象
* @throws ModelNotFoundException 当找不到指定会话时抛出异常
*/
public function getSession(string $sessionId): ChatSession
{
return ChatSession::query()->whereKey($sessionId)->firstOrFail();
}
/**
* @param array<string, mixed> $dto
* 将消息追加到指定的聊天会话中。
*
* @param array<string, mixed> $dto 包含消息详细信息的数据传输对象,包括以下键:
* - session_id: 聊天会话的唯一标识符
* - role: 消息角色(如发件人或收件人)
* - type: 消息类型
* - content: 消息内容(可选)
* - payload: 附加信息(可选)
* - reply_to: 被回复的消息 ID可选
* - dedupe_key: 消息去重键(可选)
* @return Message 返回成功追加的消息实例。如果存在去重键并已存在重复消息,则返回现有的消息。
*/
public function appendMessage(array $dto): Message
{
@@ -88,6 +114,7 @@ class ChatService
$session->update([
'last_seq' => $newSeq,
'last_message_id' => $message->message_id,
'updated_at' => now(),
]);
@@ -95,6 +122,14 @@ class ChatService
});
}
/**
* 列出指定会话中指定序号之后的消息。
*
* @param string $sessionId 会话唯一标识符
* @param int $afterSeq 指定序号之后的消息将被列出
* @param int $limit 最多返回的消息数量,默认为 50
* @return Collection 返回指定序号之后的消息列表
*/
public function listMessagesBySeq(string $sessionId, int $afterSeq, int $limit = 50): Collection
{
$this->getSession($sessionId); // ensure exists
@@ -107,6 +142,89 @@ class ChatService
->get();
}
/**
* 获取会话列表
*
* @param array $filter 过滤条件数组支持status状态过滤和q关键字搜索
* @param int $page 页码
* @param int $perPage 每页数量
* @return LengthAwarePaginator 分页结果
*/
public function listSessions(array $filter, int $page, int $perPage): LengthAwarePaginator
{
// 构建基础查询
$query = $this->baseSessionQuery();
// 根据状态过滤会话
if (! empty($filter['status'])) {
$query->where('chat_sessions.status', $filter['status']);
}
// 根据会话名称进行模糊搜索
if (! empty($filter['q'])) {
$q = $filter['q'];
$query->where('chat_sessions.session_name', 'ilike', '%'.$q.'%');
}
// 按更新时间倒序排列
$query->orderByDesc('chat_sessions.updated_at');
// 执行分页查询
return $query->paginate($perPage, ['*'], 'page', $page);
}
/**
* 更新聊天会话信息
*
* @param string $sessionId 会话ID
* @param array $patch 需要更新的字段数据
* @return ChatSession 更新后的聊天会话对象
* @throws ChatSessionStatusException 当尝试重新打开已关闭的会话时抛出异常
*/
public function updateSession(string $sessionId, array $patch): ChatSession
{
// 查找包含最后一条消息的会话
$session = $this->findSessionWithLastMessage($sessionId);
// 检查会话状态变更合法性:已关闭的会话不能重新打开
if (isset($patch['status']) && $session->status === ChatSessionStatus::CLOSED && $patch['status'] !== ChatSessionStatus::CLOSED) {
throw new ChatSessionStatusException('Closed session cannot be reopened');
}
// 填充更新数据并保存
$session->fill($patch);
$session->updated_at = now();
$session->save();
// 返回更新后的会话信息
return $this->findSessionWithLastMessage($sessionId);
}
private function baseSessionQuery()
{
return ChatSession::query()
->leftJoin('messages as lm', 'lm.message_id', '=', 'chat_sessions.last_message_id')
->select('chat_sessions.*')
->addSelect([
'lm.created_at as last_message_at',
'lm.content as last_message_content',
'lm.role as last_message_role',
'lm.type as last_message_type',
]);
}
private function findSessionWithLastMessage(string $sessionId): ChatSession
{
/** @var ChatSession $session */
$session = $this->baseSessionQuery()
->where('chat_sessions.session_id', $sessionId)
->firstOrFail();
return $session;
}
private function ensureCanAppend(ChatSession $session, string $role, string $type): void
{
if ($session->status === ChatSessionStatus::CLOSED) {