Files
Neo-ZQYY/.kiro/specs/rns1-chat-integration/design.md

30 KiB
Raw Blame History

技术设计文档 — RNS1.4CHAT 对齐与联调收尾

概述

RNS1.4 是 RNS1 系列的收尾 spec覆盖三大块工作

  1. CHAT 模块路径迁移与功能补全T4-1 ~ T4-3将现有 /api/ai/* 路由迁移到 /api/xcx/chat/*,实现 CHAT-1/2/3/4 四个端点,支持 referenceCard 和多入口参数路由
  2. FDW 端到端验证T4-4验证 test_zqyy_apptest_etl_feiqiu 链路上所有 FDW 视图可访问、性能达标、索引完备
  3. 全量前后端联调T4-513 个页面移除 mock 数据,连接真实后端,修复 notes 触底加载和 customer-service-records 按月请求

设计原则

  • 复用优先:现有 biz.ai_conversations / biz.ai_messages 表结构通过 DDL 迁移扩展,新增 CHAT 所需字段(customer_idtitlelast_messagereference_card),不新建表
  • 契约驱动:所有端点严格遵循 API-contract.md 中 CHAT-1/2/3/4 的定义
  • 权限一致:所有 CHAT 端点使用 require_approved() 依赖,与 RNS1.1-1.3 保持一致
  • DWD-DOC 强制规则referenceCard 中涉及金额使用 items_sum 口径,会员信息通过 member_id JOIN dim_member

依赖关系

graph LR
    RNS10[RNS1.0<br/>基础设施] --> RNS14[RNS1.4<br/>CHAT + 联调]
    RNS11[RNS1.1<br/>任务/绩效] --> RNS14
    RNS12[RNS1.2<br/>客户/助教] --> RNS14
    RNS13[RNS1.3<br/>三看板] --> RNS14
    style RNS14 fill:#f9f,stroke:#333

架构

整体架构

graph TB
    subgraph "微信小程序 (apps/miniprogram/)"
        FE_CHAT[pages/chat/chat.ts]
        FE_HIST[pages/chat-history/chat-history.ts]
        FE_API[services/api.ts]
        FE_REQ[utils/request.ts]
        FE_CHAT --> FE_API
        FE_HIST --> FE_API
        FE_API --> FE_REQ
    end

    subgraph "FastAPI 后端 (apps/backend/app/)"
        MW[ResponseWrapperMiddleware<br/>SSE 自动跳过]
        ROUTER[routers/xcx_chat.py<br/>CHAT-1/2/3/4]
        SCHEMA[schemas/xcx_chat.py<br/>Pydantic CamelModel]
        SVC[services/chat_service.py<br/>对话业务逻辑]
        AI_SVC[ai/conversation_service.py<br/>AI 调用 + 持久化]
        BAILIAN[ai/bailian_client.py<br/>百炼 API]
        FDW_Q[services/fdw_queries.py<br/>FDW 查询]
        DB[database.py]

        MW --> ROUTER
        ROUTER --> SCHEMA
        ROUTER --> SVC
        SVC --> AI_SVC
        SVC --> FDW_Q
        AI_SVC --> BAILIAN
        SVC --> DB
    end

    subgraph "数据库"
        APP_DB[(zqyy_app<br/>biz.ai_conversations<br/>biz.ai_messages)]
        ETL_DB[(etl_feiqiu via FDW<br/>fdw_etl.v_dim_member<br/>fdw_etl.v_dws_member_*)]
    end

    FE_REQ -->|HTTP JSON / SSE| MW
    DB --> APP_DB
    FDW_Q --> ETL_DB

    style ROUTER fill:#f9f,stroke:#333
    style SVC fill:#f9f,stroke:#333

请求-响应流程SSE 流式)

sequenceDiagram
    participant MP as 小程序 chat.ts
    participant MW as ResponseWrapper
    participant R as xcx_chat.py
    participant S as chat_service.py
    participant AI as BailianClient
    participant DB as zqyy_app

    MP->>MW: POST /api/xcx/chat/stream<br/>{chatId, content}
    MW->>R: 透传SSE 跳过包装)
    R->>R: require_approved() + 验证 chatId 归属
    R->>S: stream_chat(chatId, content, user)
    S->>DB: INSERT user message → ai_messages
    S->>AI: 流式调用百炼 API

    loop 逐 token
        AI-->>S: token 片段
        S-->>R: SSEEvent(type=message, token=...)
        R-->>MW: data: {"token": "..."}
        MW-->>MP: 透传 SSE 事件
    end

    S->>DB: INSERT AI reply → ai_messages
    S->>DB: UPDATE ai_conversations.last_message
    S-->>R: SSEEvent(type=done, messageId, createdAt)
    R-->>MW: data: {"messageId": "...", "createdAt": "..."}
    MW-->>MP: 透传 done 事件

组件与接口

组件 1xcx_chat 路由模块(路径迁移 + CHAT-1/2/3/4

位置apps/backend/app/routers/xcx_chat.py(新文件,替代 xcx_ai_chat.py

职责:将现有 /api/ai/* 路由迁移到 /api/xcx/chat/*,并实现 CHAT-1/2/3/4 四个端点。

迁移策略

  • 新建 xcx_chat.pyprefix 为 /api/xcx/chat
  • xcx_ai_chat.py 迁移 SSE 流式对话、历史列表、消息查询三个端点
  • main.py 中替换路由注册:移除 xcx_ai_chat.router,注册 xcx_chat.router
  • 删除 xcx_ai_chat.py(不保留旧路径兼容)

端点定义

router = APIRouter(prefix="/api/xcx/chat", tags=["小程序 CHAT"])

# CHAT-1: 对话历史列表
@router.get("/history")
async def list_chat_history(
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    user: CurrentUser = Depends(require_approved()),
) -> ChatHistoryResponse: ...

# CHAT-2a: 通过 chatId 查询消息
@router.get("/{chat_id}/messages")
async def get_chat_messages(
    chat_id: int,
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=100),
    user: CurrentUser = Depends(require_approved()),
) -> ChatMessagesResponse: ...

# CHAT-2b: 通过上下文查询消息(自动查找/创建对话)
@router.get("/messages")
async def get_chat_messages_by_context(
    context_type: str = Query(..., alias="contextType"),  # task / customer / coach
    context_id: str = Query(..., alias="contextId"),
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=100),
    user: CurrentUser = Depends(require_approved()),
) -> ChatMessagesResponse: ...

# CHAT-3: 发送消息(同步回复)
@router.post("/{chat_id}/messages")
async def send_message(
    chat_id: int,
    body: SendMessageRequest,
    user: CurrentUser = Depends(require_approved()),
) -> SendMessageResponse: ...

# CHAT-4: SSE 流式端点
@router.post("/stream")
async def chat_stream(
    body: ChatStreamRequest,
    user: CurrentUser = Depends(require_approved()),
) -> StreamingResponse: ...

组件 2chat_service 业务逻辑层

位置apps/backend/app/services/chat_service.py(新文件)

职责:封装 CHAT 模块的核心业务逻辑包括对话管理、消息持久化、referenceCard 组装。

关键方法

class ChatService:
    """CHAT 模块业务逻辑。"""

    def get_chat_history(
        self, user_id: int, site_id: int, page: int, page_size: int
    ) -> tuple[list[dict], int]:
        """CHAT-1: 查询对话历史列表,返回 (items, total)。"""

    def get_or_create_session(
        self, user_id: int, site_id: int,
        context_type: str, context_id: str | None
    ) -> int:
        """按入口上下文查找或创建对话,返回 chat_id。
        复用规则:
        - context_type='task': 同一 taskId 始终复用(无时限)
        - context_type='customer'/'coach': 最后消息 ≤ 3 天复用,> 3 天新建
        - context_type='general': 始终新建
        """

    def get_messages(
        self, chat_id: int, user_id: int, site_id: int,
        page: int, page_size: int
    ) -> tuple[list[dict], int, int]:
        """CHAT-2: 查询消息列表,返回 (messages, total, chat_id)。
        验证 chat_id 归属当前用户。"""

    def send_message_sync(
        self, chat_id: int, content: str, user_id: int, site_id: int
    ) -> dict:
        """CHAT-3: 发送消息并获取同步 AI 回复。
        1. 验证 chatId 归属
        2. 存入用户消息
        3. 调用 AI 获取回复
        4. 存入 AI 回复
        5. 更新 session 的 last_message / last_message_at
        6. AI 失败时返回错误提示消息HTTP 200"""

    def build_reference_card(
        self, customer_id: int, site_id: int
    ) -> dict | None:
        """组装 referenceCard从 FDW 查询客户关键指标。
        遵循 DWD-DOC 规则:金额用 items_sum会员信息通过 member_id JOIN dim_member。"""

    def generate_title(self, session: dict) -> str:
        """生成对话标题:自定义标题 > 上下文名称 > 首条消息前20字。"""

referenceCard 组装逻辑

def build_reference_card(self, customer_id: int, site_id: int) -> dict | None:
    """从 FDW 查询客户指标,组装为 referenceCard 结构。"""
    # 1. 通过 member_id JOIN fdw_etl.v_dim_member 获取客户姓名
    # 2. 通过 fdw_etl.v_dws_member_consumption_summary 获取:
    #    - 余额balance
    #    - 近30天消费items_sum 口径,非 consume_money
    #    - 到店次数
    # 3. 组装为 referenceCard 结构
    return {
        "type": "customer",
        "title": f"{member_name} — 消费概览",
        "summary": f"余额 ¥{balance}近30天消费 ¥{consume_30d}",
        "data": {
            "余额": f{balance}",
            "近30天消费": f{consume_30d}",
            "到店次数": f"{visit_count}次",
        }
    }

组件 3Pydantic SchemaCamelModel

位置apps/backend/app/schemas/xcx_chat.py(新文件)

职责:定义 CHAT 模块所有请求/响应的 Pydantic schema继承 CamelModel 统一 camelCase 输出。

from app.schemas.base import CamelModel

class ChatHistoryItem(CamelModel):
    id: int
    title: str
    customer_name: str | None = None
    last_message: str | None = None
    timestamp: str          # ISO 8601最后消息时间
    unread_count: int = 0

class ChatHistoryResponse(CamelModel):
    items: list[ChatHistoryItem]
    total: int
    page: int
    page_size: int

class ReferenceCard(CamelModel):
    type: str               # 'customer' | 'record'
    title: str
    summary: str
    data: dict[str, str]    # 键值对详情

class ChatMessageItem(CamelModel):
    id: int
    role: str               # 'user' | 'assistant'
    content: str
    created_at: str         # ISO 8601统一字段名替代 timestamp / created_at
    reference_card: ReferenceCard | None = None

class ChatMessagesResponse(CamelModel):
    chat_id: int
    items: list[ChatMessageItem]
    total: int
    page: int
    page_size: int

class SendMessageRequest(CamelModel):
    content: str

class SendMessageResponse(CamelModel):
    user_message: MessageBrief
    ai_reply: MessageBrief

class MessageBrief(CamelModel):
    id: int
    content: str
    created_at: str

class ChatStreamRequest(CamelModel):
    chat_id: int
    content: str

组件 4前端 chat 页面改造

位置apps/miniprogram/miniprogram/pages/chat/chat.ts

改造要点

  1. 多入口参数路由GAP-49/50

    onLoad(options) {
      if (options.historyId) {
        // 从 chat-history 跳转:直接用 historyId 作为 chatId
        this.chatId = options.historyId
        this.loadMessages(this.chatId)
      } else if (options.taskId) {
        // 从 task-detail 跳转:同一 taskId 始终复用同一对话
        this.loadMessagesByContext('task', options.taskId)
      } else if (options.customerId) {
        // 从 customer-detail 跳转3 天内复用,超过 3 天新建
        this.loadMessagesByContext('customer', options.customerId)
      } else if (options.coachId) {
        // 从 coach-detail 跳转3 天内复用,超过 3 天新建
        this.loadMessagesByContext('coach', options.coachId)
      } else {
        // 无参数:始终新建通用对话
        this.loadMessagesByContext('general', '')
      }
    }
    
  2. SSE 替换 mock 流式输出

    • 移除 simulateStreamOutput() 调用和 mockAIReplies
    • 使用 wx.request + enableChunked: true(微信基础库 2.20.2+)接收 SSE
    • 备选方案:轮询 CHAT-3 同步端点(不支持 chunked 的低版本基础库)
  3. referenceCard 渲染

    • 消息列表中检测 referenceCard 字段,已有 toDataList() 和 WXML 模板,无需大改
    • 确保从真实 API 返回的 referenceCard 结构与 mock 一致

组件 5前端 chat-history 页面改造

位置apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts

改造要点

  • 移除 mockChatHistory 导入
  • 调用 fetchChatHistory() 获取真实数据
  • 响应字段映射:后端返回 timestampISO 8601→ 前端 formatRelativeTime() 处理

组件 6前端 services/api.ts CHAT 模块对接

位置apps/miniprogram/miniprogram/services/api.ts

改造要点

  • fetchChatHistory():调用 GET /api/xcx/chat/history
  • fetchChatMessages():调用 GET /api/xcx/chat/{chatId}/messages
  • fetchChatMessagesByContext(contextType, contextId):新增,调用 GET /api/xcx/chat/messages?contextType={type}&contextId={id}
  • sendChatMessage():调用 POST /api/xcx/chat/{chatId}/messages
  • 移除所有 CHAT 相关 mock 数据导入
  • USE_REAL_API 开关对 CHAT 模块设为 true

组件 7FDW 验证脚本

位置scripts/ops/verify_fdw_e2e.py(新文件)

职责:一次性验证脚本,检查 test_zqyy_apptest_etl_feiqiu FDW 链路。

验证项

  1. 所有 fdw_etl.* 视图可访问SELECT 1 FROM ... LIMIT 1
  2. 带典型过滤条件的查询响应时间 < 3s
  3. 关键索引存在检查(chat_sessions(assistant_id, customer_id) 等)
  4. 结果输出为结构化报告JSON失败项标注需 DBA 介入

组件 8联调修复 — notes 触底加载 & customer-service-records 按月请求

notes 页面pages/notes/notes.ts

  • 实现 onReachBottom() 生命周期函数
  • 维护 page 状态,触底时 page++ 调用 fetchNotes({ page, pageSize })
  • 追加数据到已有列表,hasMore === false 时停止加载

customer-service-records 页面pages/customer-service-records/customer-service-records.ts

  • 月份切换时调用 fetchCustomerRecords({ customerId, year, month })
  • 清空已有列表 → 显示 loading → 渲染新数据
  • 首次加载默认当前月份

数据模型

表结构变更:扩展 biz.ai_conversations

现有 biz.ai_conversations 表需新增字段以支持 CHAT API 契约:

-- 迁移脚本:扩展 ai_conversations 支持 CHAT 模块
ALTER TABLE biz.ai_conversations
    ADD COLUMN IF NOT EXISTS context_type varchar(20),     -- 关联上下文类型task / customer / coach / general
    ADD COLUMN IF NOT EXISTS context_id varchar(50),       -- 关联上下文 IDtaskId / customerId / coachId
    ADD COLUMN IF NOT EXISTS title varchar(200),           -- 对话标题
    ADD COLUMN IF NOT EXISTS last_message text,            -- 最后一条消息摘要
    ADD COLUMN IF NOT EXISTS last_message_at timestamptz;  -- 最后消息时间

COMMENT ON COLUMN biz.ai_conversations.context_type IS '对话关联上下文类型task任务/ customer客户/ coach助教/ general通用';
COMMENT ON COLUMN biz.ai_conversations.context_id IS '关联上下文 IDtask 入口为 taskIdcustomer 入口为 customerIdcoach 入口为 coachIdgeneral 为 NULL';
COMMENT ON COLUMN biz.ai_conversations.title IS '对话标题:自定义 > 上下文名称 > 首条消息前20字';
COMMENT ON COLUMN biz.ai_conversations.last_message IS '最后一条消息内容摘要截断至100字';
COMMENT ON COLUMN biz.ai_conversations.last_message_at IS '最后消息时间,用于历史列表排序和对话复用时限判断';

新增索引

-- 上下文对话查找(按 context_type + context_id 查找可复用对话)
CREATE INDEX idx_ai_conv_context
    ON biz.ai_conversations (user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST)
    WHERE context_type IS NOT NULL;

-- 历史列表排序优化CHAT-1: 按 last_message_at 倒序)
CREATE INDEX idx_ai_conv_last_msg
    ON biz.ai_conversations (user_id, site_id, last_message_at DESC NULLS LAST);

对话复用规则

不同入口的对话创建/复用策略:

入口 context_type context_id 复用规则
task-detail task taskId 始终复用:同一 taskId 归为同一个对话,无时限
customer-detail customer customerId 3 天时限:最后一条消息 ≤ 3 天则复用,> 3 天则新建
coach-detail coach coachId 3 天时限:最后一条消息 ≤ 3 天则复用,> 3 天则新建
chat-history 直接打开:用 historyId 作为 chatId 加载已有对话
无参数AI 按钮等) general NULL 始终新建:每次创建新对话

复用查找 SQL 模式:

-- task 入口:始终复用(无时限)
SELECT id FROM biz.ai_conversations
WHERE user_id = :user_id AND site_id = :site_id
  AND context_type = 'task' AND context_id = :task_id
ORDER BY created_at DESC LIMIT 1;

-- customer / coach 入口3 天时限复用
SELECT id FROM biz.ai_conversations
WHERE user_id = :user_id AND site_id = :site_id
  AND context_type = :type AND context_id = :context_id
  AND last_message_at > NOW() - INTERVAL '3 days'
ORDER BY last_message_at DESC LIMIT 1;

表结构变更:扩展 biz.ai_messages

-- 迁移脚本:扩展 ai_messages 支持 referenceCard
ALTER TABLE biz.ai_messages
    ADD COLUMN IF NOT EXISTS reference_card jsonb;  -- 引用卡片 JSON

COMMENT ON COLUMN biz.ai_messages.reference_card IS 'referenceCard JSON{type, title, summary, data}';

变更后完整表结构

biz.ai_conversations扩展后

字段 类型 说明
id bigint PK 对话 ID即 chatId
user_id varchar(50) 用户 ID助教
nickname varchar(100) 用户昵称
app_id varchar(30) AI 应用 IDCHAT 模块固定为 app1_chat
site_id bigint 门店 ID
source_page varchar(100) 来源页面
source_context jsonb 来源上下文
created_at timestamptz 创建时间
customer_id 已移除 — 改用 context_type + context_id 通用方案
context_type varchar(20) 新增 — 对话关联上下文类型task/customer/coach/general
context_id varchar(50) 新增 — 关联上下文 IDtaskId/customerId/coachId
title varchar(200) 新增 — 对话标题
last_message text 新增 — 最后消息摘要
last_message_at timestamptz 新增 — 最后消息时间

biz.ai_messages扩展后

字段 类型 说明
id bigint PK 消息 ID
conversation_id bigint FK 对话 ID
role varchar(10) 角色:user / assistant
content text 消息内容
tokens_used integer token 消耗量
created_at timestamptz 创建时间
reference_card jsonb 新增 — 引用卡片

字段映射:数据库 → API 响应

数据库字段 API 响应字段camelCase 说明
ai_conversations.id id / chatId 对话 ID
ai_conversations.title title 对话标题
ai_conversations.last_message lastMessage 最后消息摘要
ai_conversations.last_message_at timestamp CHAT-1 历史列表时间
ai_conversations.context_id → 当 context_type=customer 时 JOIN v_dim_member customerName 客户姓名(仅 context_type=customer 时有值)
ai_messages.id id 消息 ID
ai_messages.role role 消息角色
ai_messages.content content 消息内容
ai_messages.created_at createdAt 统一时间字段名
ai_messages.reference_card referenceCard 引用卡片 JSON

referenceCard JSON 结构

{
  "type": "customer",
  "title": "张伟 — 消费概览",
  "summary": "余额 ¥5,200近30天消费 ¥2,380",
  "data": {
    "余额": "¥5,200",
    "近30天消费": "¥2,380",
    "到店次数": "8次",
    "最近到店": "3天前"
  }
}

DWD-DOC 强制规则在数据模型中的体现

规则 影响范围 实施方式
items_sum 口径 referenceCard 中"近30天消费" SQL 使用 items_sum 字段,禁用 consume_money
助教费用拆分 referenceCard 中如涉及助教费用 使用 assistant_pd_money + assistant_cx_money
会员信息 JOIN referenceCard 中客户姓名 通过 member_id JOIN fdw_etl.v_dim_memberscd2_is_current=1),禁用结算单冗余字段

正确性属性

属性Property是一个在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。

Property 1: 路由迁移完整性

For any CHAT 端点路径(/history/{chatId}/messages/messages/{chatId}/messages POST、/stream),以 /api/xcx/chat 为前缀的请求应返回非 404 响应(需认证),而以 /api/ai 为前缀的对应旧路径应返回 404。

Validates: Requirements 1.1, 1.2

Property 2: CHAT API 响应结构完整性

For any CHAT-1 历史列表项,响应必须包含 idtitlelastMessagetimestamp 字段;For any CHAT-2 消息项,响应必须包含 idrolecontentcreatedAt 字段;For any CHAT-3 发送消息响应,必须包含 userMessageaiReply,各含 idcontentcreatedAt 字段。

Validates: Requirements 2.2, 3.3, 5.3

Property 3: 列表排序不变量

For any CHAT-1 返回的对话历史列表,相邻两项的 timestamp 应满足前项 ≥ 后项(按时间倒序);For any CHAT-2 返回的消息列表,相邻两项的 createdAt 应满足前项 ≤ 后项(按时间正序)。

Validates: Requirements 2.3, 3.5

Property 4: 对话标题生成优先级

For any 对话记录,标题生成应遵循优先级链:若 title 字段非空则使用 title;否则若 customer_id 关联的客户姓名非空则使用客户姓名;否则使用首条消息内容的前 20 个字符。生成结果应始终为非空字符串。

Validates: Requirements 2.4

Property 5: 权限控制与数据隔离

For any CHAT 端点CHAT-1/2/3/4未通过审核的用户status ≠ approved应收到 HTTP 403 响应;For any 已认证用户请求的对话数据,返回的所有对话记录的 user_id 应等于当前用户 IDFor any 不属于当前用户的 chatIdCHAT-3 和 CHAT-4 应返回 HTTP 403。

Validates: Requirements 2.6, 5.6, 11.1, 11.2, 11.3

Property 6: 对话复用规则正确性

For any context_type='task' 的入口,同一 (user_id, site_id, context_id) 多次调用 get_or_create_session 应始终返回同一个 chatIdFor any context_type='customer'context_type='coach' 的入口,若最后消息时间 ≤ 3 天则返回已有 chatId,若 > 3 天则返回新的 chatIdFor any context_type='general' 的入口,每次调用应返回不同的 chatId

Validates: Requirements 3.8, 3.9, 3.10

Property 7: referenceCard 持久化 Round Trip

For any 合法的 referenceCard JSON 对象(包含 typetitlesummarydata 字段),存入 ai_messages.reference_card 后再读取,应得到与原始对象结构等价的 JSON。

Validates: Requirements 4.1, 4.3

Property 8: 消息持久化与会话元数据更新

For any 通过 CHAT-3 或 CHAT-4 发送的消息,用户消息和 AI 回复均应被持久化到 ai_messages 表;发送后对应 ai_conversations 记录的 last_message 应更新为最新消息内容,last_message_at 应更新为最新消息时间。

Validates: Requirements 5.2, 5.4, 6.3, 6.4

Property 9: SSE 事件类型有效性

For any CHAT-4 SSE 流中的事件,其 event 字段应为 messagedoneerror 三者之一;message 事件的 data 应包含 token 字段;done 事件的 data 应包含 messageIdcreatedAt 字段;error 事件的 data 应包含 message 字段。

Validates: Requirements 6.2

错误处理

后端错误处理

所有 CHAT 端点的错误响应遵循 RNS1.0 全局异常处理器格式:{ code: <HTTP状态码>, message: <错误详情> }

场景 HTTP 状态码 响应 处理方式
未认证(无 token / token 过期) 401 { code: 401, message: "无效的令牌" } get_current_user 依赖抛出
未通过审核status ≠ approved 403 { code: 403, message: "用户未通过审核,无法访问此资源" } require_approved() 依赖抛出
chatId 不属于当前用户 403 { code: 403, message: "无权访问此对话" } chat_service 验证后抛出
chatId 不存在 404 { code: 404, message: "对话不存在" } chat_service 查询后抛出
消息内容为空 422 { code: 422, message: "消息内容不能为空" } 路由层 Pydantic 验证
AI 服务调用失败CHAT-3 200 aiReply.content = "抱歉AI 助手暂时无法回复,请稍后重试" 用户消息仍保存AI 回复为错误提示
AI 服务调用失败CHAT-4 SSE SSE error 事件 event: error\ndata: {"message": "AI 服务暂时不可用"} 流中发送 error 事件后关闭
数据库连接失败 500 { code: 500, message: "Internal Server Error" } 全局 unhandled_exception_handler
FDW 查询失败referenceCard 静默降级 referenceCard 返回 null 不影响消息本身,仅 referenceCard 缺失

CHAT-3 AI 失败降级策略

async def send_message_sync(self, chat_id, content, user_id, site_id):
    # 1. 存入用户消息(无论 AI 是否成功)
    user_msg_id = self._save_message(chat_id, "user", content)

    # 2. 调用 AI
    try:
        ai_reply = await self._call_ai(content, chat_id)
    except Exception as e:
        logger.error("AI 服务调用失败: %s", e)
        ai_reply = "抱歉AI 助手暂时无法回复,请稍后重试"

    # 3. 存入 AI 回复(包括错误提示)
    ai_msg_id = self._save_message(chat_id, "assistant", ai_reply)

    # 4. 更新 session 元数据
    self._update_session_metadata(chat_id, ai_reply)

    # 5. HTTP 200 返回(不抛异常)
    return { "userMessage": {...}, "aiReply": {...} }

前端错误处理

场景 处理方式
CHAT API 返回 401 跳转登录页(request() 全局拦截)
CHAT API 返回 403 Toast 提示"权限不足"
CHAT API 返回 404 Toast 提示"对话不存在"
CHAT API 返回 500 Toast 提示"服务暂时不可用"
SSE 连接中断 停止流式显示,显示"连接中断"提示,允许重试
网络超时 wx.request fail 回调,显示网络错误提示

测试策略

双轨测试方法

RNS1.4 采用属性测试Property-Based Testing+ 单元测试Unit Testing双轨并行

  • 属性测试验证对话管理、消息持久化、权限控制、referenceCard round trip 等通用规则
  • 单元测试验证具体端点行为、边界条件、AI 失败降级等

属性测试配置

  • 测试库HypothesisPython项目已使用
  • 测试位置tests/ 目录Monorepo 级属性测试)
  • 最小迭代次数:每个属性测试 100 次(@settings(max_examples=100)
  • 标签格式:每个测试函数的 docstring 中标注 Feature: rns1-chat-integration, Property {N}: {property_text}

属性测试清单

Property 测试函数 生成器 验证逻辑
P3: 列表排序不变量 test_chat_list_ordering st.lists(st.datetimes()) 生成随机时间戳列表 对话列表按时间倒序,消息列表按时间正序
P4: 标题生成优先级 test_title_generation_priority st.fixed_dictionaries({"title": st.one_of(st.none(), st.text(min_size=1)), "customer_name": st.one_of(st.none(), st.text(min_size=1)), "first_message": st.text(min_size=1)}) 标题遵循优先级链,结果非空
P6: customerId 幂等性 test_customer_id_get_or_create_idempotent st.integers(min_value=1) 生成随机 user_id 和 customer_id 多次调用返回同一 chatId
P7: referenceCard Round Trip test_reference_card_roundtrip st.fixed_dictionaries({"type": st.sampled_from(["customer", "record"]), "title": st.text(min_size=1), "summary": st.text(), "data": st.dictionaries(st.text(min_size=1), st.text())}) JSON 序列化→存储→读取→反序列化等于原始对象
P8: 消息持久化 test_message_persistence_after_send st.text(min_size=1, max_size=500) 生成随机消息内容 发送后 ai_messages 包含用户消息和 AI 回复session 元数据已更新
P9: SSE 事件类型 test_sse_event_type_validity st.sampled_from(["message", "done", "error"]) + 对应 data 结构 事件类型为三者之一data 结构符合定义

单元测试清单

测试目标 测试文件 关键用例
路由迁移P1 apps/backend/tests/unit/test_xcx_chat_routes.py /api/xcx/chat/history 返回 200/api/ai/conversations 返回 404
响应结构P2 apps/backend/tests/unit/test_xcx_chat_schema.py ChatHistoryItem / ChatMessageItem / SendMessageResponse 序列化验证
权限控制P5 apps/backend/tests/unit/test_xcx_chat_auth.py 未审核用户 403chatId 不属于当前用户 403
AI 失败降级edge case apps/backend/tests/unit/test_xcx_chat_ai_fallback.py AI 超时时返回错误提示消息HTTP 200
SSE 跳过包装 已由 RNS1.0 测试覆盖 text/event-stream 不经过 ResponseWrapper
FDW 验证 scripts/ops/verify_fdw_e2e.py 一次性运行,输出验证报告
联调验证 手动测试 13 页面逐一验证真实数据渲染

测试执行命令

# 属性测试Hypothesis
cd C:\NeoZQYY && pytest tests/ -v -k "rns1_chat"

# 单元测试
cd apps/backend && pytest tests/unit/ -v -k "xcx_chat"

# FDW 验证脚本
cd C:\NeoZQYY && uv run python scripts/ops/verify_fdw_e2e.py