Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/system_design.md
T
Simon 44e77dcb0e chore(docs): docs/ 目录全面重新编号 + 重组
**重构前**(旧编号 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 行
2026-08-03 18:46:55 +08:00

15 KiB
Raw Blame History

系统架构设计 — 历史会话开关功能

架构师:高见远(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 / ReplySuggestAreav-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": "邮箱登录失败提示密码"
    }
  }
}

后端查询逻辑:

  1. 查询 conversations 表中 employee_id = ? 的所有会话,获取会话ID列表
  2. 查询 messages 表中 conversation_id IN (会话ID列表) 的消息
  3. 如有 before 参数,获取该消息的 created_at,只查更早的消息
  4. created_at DESC 排序,取 limit + 1 条(多取1条判断 has_more
  5. 对涉及的每个会话,查询其 sender_type='employee' 的最早一条消息,取前20字作为摘要
  6. 返回消息列表 + 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 新增)

新增 State6个 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

新增 Getters2个 computed):

displayMessages    // historyMode ? historyMessages : messages
isHistoryReadonly  // historyMode(历史模式只读)

新增 Actions4个):

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 后端 SchemaPydantic

# 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

核心流程:

  1. 打开历史模式:点击开关 → Store.enableHistoryMode() → API 请求 → 渲染合并时间线
  2. 向上滚动加载更多:检测滚动到顶部 → Store.loadMoreHistory() → API 请求(before游标)→ 前插消息
  3. 关闭历史模式:点击开关 → Store.disableHistoryMode() → 恢复正常消息列表
  4. 切换会话重置selectConversation() → resetHistoryState() → historyMode=false

5. 任务列表

任务ID 任务名称 涉及文件 依赖 优先级
T01 后端 — 历史消息聚合接口 backend/app/api/messages.pybackend/app/services/conversation/session_query_service.pybackend/app/schemas/message.py P0
T02 前端数据层 — API + Store + Mock frontend-agent/src/api/message.tsfrontend-agent/src/stores/conversation.tsfrontend-agent/src/mock/data.ts T01 P0
T03 前端组件层 — 开关 + 分隔条 + 消息列表改造 frontend-agent/src/components/chat/UserInfoBar.vuefrontend-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(最新在前)
前端 Storereverse() 后存储为 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,是最终集成层