44e77dcb0e
**重构前**(旧编号 02-11): - docs/02-产品需求/ → 00 产品规划/PRD - docs/03-技术架构/ → 01-05 子目录散落 - docs/04-原型设计/ → 01-02 产品设计(HTML 原型) - docs/05-原型设计/ → screens/ - docs/06-测试素材/ → 02-E2E / 03-功能 / 04-版本测试 - docs/07-项目管理/ → 任务说明书/日报/计划 - docs/08-安全审计/ → 审计报告 - docs/09-堡垒运维/ → toolbox / deploy - docs/10-项目管理/ → 任务说明书(重复) - docs/11-历史归档/ → deploy-nas-archived **重构后**(新编号 00-07,语义化): - docs/00-产品开发流程与文档管理规范.md - docs/00-版本迭代总览.md - docs/01-产品文档/ (PRD/原型/认证/会话/AI 服务/坐席/集成) - docs/02-技术文档/ (技术方案/架构图/重构记录/前端改造/实现配置) - docs/03-测试文档/ (E2E/功能用例/版本报告/缺陷单) - docs/04-运维文档/ (部署运维/运维指南) - docs/05-运营文档/ (品牌推广/用户手册) - docs/06-安全审计/ (审计报告) - docs/07-项目管理/ (任务说明书/日报/计划/看板) **净收益**: - 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号) - 消除 02-产品需求 与 10-项目管理 的编号重叠 - 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录) - 把运维/安全/项目管理从 0X 散落改为 04/06/07 合计 494 文件 + 78495 行 / - 14076 行
15 KiB
15 KiB
系统架构设计 — 历史会话开关功能
架构师:高见远(Bob)
日期:2026-07-01
基于 PRD v1.0 + 现有代码结构分析
Part A: 系统设计
1. 实现方案 + 框架选型
1.1 核心技术挑战
| 挑战 | 说明 | 方案 |
|---|---|---|
| 跨会话消息聚合 | 需要将同一员工的所有会话消息合并为一条时间线,按时间排序 | 后端新增按 employee_id 聚合查询的接口,JOIN conversations + messages 表,按 created_at 全局排序 |
| 分隔条主题提取 | 分隔条显示该会话中员工首条消息摘要(前20字),不新增数据库字段 | 后端在聚合查询时,对每个会话查找 sender_type='employee' 的最早一条消息,截取前20字作为 conversation_summaries 返回 |
| 游标分页(跨会话) | 向上滚动加载更多历史消息,需跨会话游标分页 | 使用 before 参数(消息ID),后端根据该消息的 created_at 查询更早的消息,全局时间线分页 |
| 模式切换无闪烁 | 开关切换时正常模式↔历史模式,消息列表无缝切换 | Store 新增 displayMessages computed,根据 historyMode 返回不同数据源;前端 v-if 切换加载态 |
| 历史模式只读 | 历史模式下隐藏输入框、回复建议区 | ChatArea 中用 historyMode 控制 ReplyBox / ReplySuggestArea 的 v-if |
| 会话切换自动重置 | 切换会话时关闭历史模式 | Store 的 selectConversation() 中调用 resetHistoryState() |
1.2 框架与库选型
| 层 | 技术 | 说明 |
|---|---|---|
| 后端 | FastAPI + SQLAlchemy 2.0 (async) | 沿用现有技术栈,新增一个 GET 接口 |
| 前端 | Vue 3 + Pinia + Element Plus | 沿用现有技术栈,新增一个组件 + Store 扩展 |
| 分页 | 游标分页(before 参数) |
与现有 getMessages() 的分页方式一致,前端向上滚动触发 |
1.3 后端新接口设计
GET /api/employees/{employee_id}/history-messages
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
employee_id |
path (str) | — | 员工企微 UserID |
limit |
query (int) | 50 | 每页消息数量(1~100) |
before |
query (str?) | null | 游标:加载此消息ID之前的消息(向上翻页) |
current_conversation_id |
query (str?) | null | 当前会话ID(用于标记当前会话的分隔条) |
响应体:
{
"code": 200,
"data": {
"items": [ /* Message[] — 按时间倒序(最新在前),前端 reverse 后展示 */ ],
"has_more": true,
"conversation_summaries": {
"conv-uuid-1": "VPN连接不上怎么办急",
"conv-uuid-2": "邮箱登录失败提示密码"
}
}
}
后端查询逻辑:
- 查询
conversations表中employee_id = ?的所有会话,获取会话ID列表 - 查询
messages表中conversation_id IN (会话ID列表)的消息 - 如有
before参数,获取该消息的created_at,只查更早的消息 - 按
created_at DESC排序,取limit + 1条(多取1条判断has_more) - 对涉及的每个会话,查询其
sender_type='employee'的最早一条消息,取前20字作为摘要 - 返回消息列表 +
has_more+conversation_summaries
1.4 前端组件改动方案
| 文件 | 改动类型 | 改动概述 |
|---|---|---|
UserInfoBar.vue |
修改 | chips 区末尾(L86 备注chip之后)新增"历史会话"开关按钮,三态视觉(关闭/打开/加载中),新增 toggle-history emit |
ChatArea.vue |
修改 | 消息列表使用 displayMessages 替代 messages;渲染时插入 ConversationSeparator;历史模式隐藏 ReplyBox/ReplySuggestArea;监听向上滚动触发分页 |
ConversationSeparator.vue |
新增 | 会话分隔条组件,props: summary(首条消息摘要前20字)、isCurrent(是否当前会话) |
conversation.ts (Store) |
修改 | 新增历史模式状态(6个 ref + 2个 computed + 4个 action) |
message.ts (API) |
修改 | 新增 getHistoryMessages() 函数 + HistoryMessageListData 类型 |
data.ts (Mock) |
修改 | 新增 mock 历史消息数据(开发环境 fallback) |
1.5 状态管理方案(Store 新增)
新增 State(6个 ref):
historyMode: ref<boolean>(false) // 历史模式开关
historyMessages: ref<Message[]>([]) // 历史合并时间线消息
historyLoading: ref<boolean>(false) // 加载中状态
historyHasMore: ref<boolean>(false) // 是否还有更多历史消息
historyConversationSummaries: ref<Record<string, string>>({}) // 会话ID→首条消息摘要
historyCursor: ref<string | null>(null) // 分页游标(最后加载的消息ID)
新增 Getters(2个 computed):
displayMessages // historyMode ? historyMessages : messages
isHistoryReadonly // historyMode(历史模式只读)
新增 Actions(4个):
enableHistoryMode() // 打开历史模式,加载初始消息
disableHistoryMode() // 关闭历史模式,清空历史状态
loadMoreHistory() // 向上滚动加载更多(分页)
resetHistoryState() // 重置所有历史状态(切换会话时调用)
2. 文件列表及相对路径
| # | 文件路径 | 改动类型 | 改动概述 |
|---|---|---|---|
| 1 | backend/app/api/messages.py |
修改 | 新增 GET /employees/{employee_id}/history-messages 路由处理函数 |
| 2 | backend/app/services/conversation/session_query_service.py |
修改 | 新增 get_employee_history_messages() 方法 |
| 3 | backend/app/schemas/message.py |
修改 | 新增 HistoryMessageListResponse Pydantic Schema |
| 4 | frontend-agent/src/api/message.ts |
修改 | 新增 getHistoryMessages() API 函数 + HistoryMessageListData 接口 |
| 5 | frontend-agent/src/stores/conversation.ts |
修改 | 新增历史模式 state/getters/actions,修改 selectConversation 加入重置逻辑 |
| 6 | frontend-agent/src/mock/data.ts |
修改 | 新增 mockHistoryMessageData mock 数据(开发 fallback) |
| 7 | frontend-agent/src/components/chat/UserInfoBar.vue |
修改 | chips 区末尾新增历史开关按钮 + toggle-history emit + 三态样式 |
| 8 | frontend-agent/src/components/chat/ConversationSeparator.vue |
新增 | 会话分隔条组件 |
| 9 | frontend-agent/src/components/chat/ChatArea.vue |
修改 | 消息列表切换、分隔条插入、只读模式、滚动分页、空状态提示 |
3. 数据结构和接口
3.1 类图
详见
docs/class-diagram.mermaid
3.2 后端 Schema(Pydantic)
# backend/app/schemas/message.py — 新增
class ConversationSummary(BaseModel):
"""会话分隔条摘要信息"""
conversation_id: str
summary: str # 员工首条消息前20字
status: str # 会话状态
created_at: datetime # 会话创建时间
class HistoryMessageListResponse(BaseModel):
"""历史消息列表响应(跨会话聚合)"""
items: List[MessageResponse] # 消息列表(按时间倒序)
has_more: bool # 是否还有更多
conversation_summaries: Dict[str, str] # {conversation_id: "前20字摘要"}
3.3 前端 TypeScript 类型定义
// frontend-agent/src/api/message.ts — 新增
/** 历史消息列表响应(跨会话聚合) */
export interface HistoryMessageListData {
/** 消息列表(按时间倒序,最新在前) */
items: Message[]
/** 是否还有更多历史消息 */
has_more: boolean
/** 会话ID → 首条消息摘要(前20字) */
conversation_summaries: Record<string, string>
}
// frontend-agent/src/components/chat/ConversationSeparator.vue — Props
interface ConversationSeparatorProps {
/** 分隔条显示文本(首条消息摘要前20字) */
summary: string
/** 是否为当前会话(当前会话高亮显示) */
isCurrent: boolean
}
4. 程序调用流程(时序图)
详见
docs/sequence-diagram.mermaid
核心流程:
- 打开历史模式:点击开关 → Store.enableHistoryMode() → API 请求 → 渲染合并时间线
- 向上滚动加载更多:检测滚动到顶部 → Store.loadMoreHistory() → API 请求(before游标)→ 前插消息
- 关闭历史模式:点击开关 → Store.disableHistoryMode() → 恢复正常消息列表
- 切换会话重置:selectConversation() → resetHistoryState() → historyMode=false
5. 任务列表
| 任务ID | 任务名称 | 涉及文件 | 依赖 | 优先级 |
|---|---|---|---|---|
| T01 | 后端 — 历史消息聚合接口 | backend/app/api/messages.py、backend/app/services/conversation/session_query_service.py、backend/app/schemas/message.py |
无 | P0 |
| T02 | 前端数据层 — API + Store + Mock | frontend-agent/src/api/message.ts、frontend-agent/src/stores/conversation.ts、frontend-agent/src/mock/data.ts |
T01 | P0 |
| T03 | 前端组件层 — 开关 + 分隔条 + 消息列表改造 | frontend-agent/src/components/chat/UserInfoBar.vue、frontend-agent/src/components/chat/ConversationSeparator.vue(新增)、frontend-agent/src/components/chat/ChatArea.vue |
T02 | P0 |
6. 依赖包列表
无需新增任何第三方依赖。
- 后端:复用现有 FastAPI + SQLAlchemy 2.0 async
- 前端:复用现有 Vue 3 + Pinia + Element Plus + Axios
7. 共享知识(跨文件约定)
7.1 消息合并时间线数据结构约定
历史模式 displayMessages 返回的是一维 Message[] 数组(与正常模式相同的类型),
按 created_at 升序排列(最旧在前,最新在后),与正常聊天列表一致。
前端在渲染时遍历 displayMessages,当检测到相邻两条消息的 conversation_id 不同时,
在它们之间插入一个 ConversationSeparator 组件。
conversation_summaries 是一个 Record<string, string> 映射:
key = conversation_id
value = 该会话中员工首条消息的前20字摘要
分隔条的 summary 从 conversation_summaries[message.conversation_id] 获取。
7.2 分隔条组件 Props 约定
// ConversationSeparator.vue
interface Props {
summary: string // 首条消息摘要(前20字),已由后端截取
isCurrent: boolean // 是否为当前会话(当前会话的分隔条高亮/加粗)
}
// 无 emit,纯展示组件(P1 搁置跳转功能)
7.3 Store 状态切换约定
正常模式 → 历史模式:
1. historyMode = true
2. historyLoading = true(触发 UI loading 态)
3. 调用 API 加载初始50条
4. 成功后:historyMessages = data.items.reverse(),historyHasMore = data.has_more
5. historyCursor = historyMessages[0]?.id(最旧消息ID,用于下次分页)
6. historyLoading = false
历史模式 → 正常模式:
1. historyMode = false
2. 清空 historyMessages、historyConversationSummaries、historyCursor
3. messages ref 不受影响(正常模式数据源未变)
切换会话时:
1. resetHistoryState() — 强制 historyMode = false,清空所有历史状态
2. 然后执行正常的 fetchMessages()
7.4 API 响应格式约定
所有后端 API 响应统一使用 success_response() 包装:
{
"code": 200,
"data": { ... },
"message": "success"
}
前端 apiClient 拦截器已自动解包,返回 response.data.data。
因此 getHistoryMessages() 返回的是 data 字段内容(HistoryMessageListData)。
7.5 消息排序约定
后端返回:按 created_at DESC(最新在前)
前端 Store:reverse() 后存储为 ASC(最旧在前,最新在后)
前端渲染:从上到下 = 从旧到新(与正常聊天一致)
分页游标:historyCursor = 最旧消息的ID(数组第一个元素)
向上滚动:用 before=historyCursor 请求更旧的消息,prepend 到数组头部
7.6 分隔条插入逻辑约定
// ChatArea.vue 渲染逻辑伪代码
const renderedItems = computed(() => {
const msgs = conversationStore.displayMessages
const result: Array<{ type: 'separator'; data: SeparatorData } | { type: 'message'; data: Message }> = []
let lastConvId = ''
for (const msg of msgs) {
if (msg.conversation_id !== lastConvId) {
// 会话切换,插入分隔条
result.push({
type: 'separator',
data: {
summary: conversationStore.historyConversationSummaries[msg.conversation_id] || '未知会话',
isCurrent: msg.conversation_id === conversationStore.currentConversationId,
}
})
lastConvId = msg.conversation_id
}
result.push({ type: 'message', data: msg })
}
return result
})
8. 待明确事项
| # | 问题 | 当前假设 | 建议确认方 |
|---|---|---|---|
| 1 | "当前会话排在最上方"的视觉含义 | 假设为:消息按时间正序排列(旧→新,与正常聊天一致),当前会话因最新而位于列表底部(用户初始可视区域)。分隔条中当前会话高亮标记。 | 产品经理 |
| 2 | 历史模式下是否暂停消息轮询 | 假设:历史模式下暂停当前会话的消息轮询(stopMessagePoll),避免新消息混入历史时间线。关闭历史模式后恢复轮询。 |
产品经理 |
| 3 | 历史消息是否需要标记已读 | 假设:历史消息不触发标记已读逻辑(只读查看,不修改 is_read 状态) | 产品经理 |
| 4 | 员工无任何历史会话(仅当前会话)时的展示 | 假设:正常展示当前会话消息 + 一条当前会话的分隔条,不显示"暂无历史会话"提示 | 产品经理 |
| 5 | 分隔条中是否显示会话状态(如"已结单") | 假设:P0 仅显示首条消息摘要,不显示状态标签。P1 可扩展。 | 产品经理 |
| 6 | 历史模式下 WebSocket 新消息推送的处理 | 假设:历史模式下收到新消息仍更新 messages ref(正常数据源),但不混入 historyMessages。关闭历史模式后即可看到。 |
架构师 |
9. 任务依赖图
graph TD
T01[T01: 后端历史消息聚合接口]
T02[T02: 前端数据层 API+Store+Mock]
T03[T03: 前端组件层 开关+分隔条+消息列表]
T01 --> T02
T02 --> T03
style T01 fill:#4CAF50,color:#fff
style T02 fill:#2196F3,color:#fff
style T03 fill:#FF9800,color:#fff
说明:
- T01(后端)无依赖,可最先开始
- T02(前端数据层)依赖 T01 的接口契约(URL、参数、响应格式),但可基于接口契约先行开发 mock
- T03(前端组件层)依赖 T02 的 Store API,是最终集成层