feat: chat integration, tenant admin spec, backend chat service, miniprogram updates, DEMO moved to tmp, XCX-TEST removed, migrations & docs

This commit is contained in:
Neo
2026-03-20 09:02:10 +08:00
parent 3d2e5f8165
commit beb88d5bea
388 changed files with 6436 additions and 25458 deletions

View File

@@ -0,0 +1 @@
{"specId": "9eccc890-b6c3-41a3-8ba1-bb2f0e09f653", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,704 @@
# 技术设计文档 — 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_app``test_etl_feiqiu` 链路上所有 FDW 视图可访问、性能达标、索引完备
3. **全量前后端联调**T4-513 个页面移除 mock 数据,连接真实后端,修复 notes 触底加载和 customer-service-records 按月请求
### 设计原则
- **复用优先**:现有 `biz.ai_conversations` / `biz.ai_messages` 表结构通过 DDL 迁移扩展,新增 CHAT 所需字段(`customer_id``title``last_message``reference_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`
### 依赖关系
```mermaid
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
```
## 架构
### 整体架构
```mermaid
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 流式)
```mermaid
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.py`prefix 为 `/api/xcx/chat`
-`xcx_ai_chat.py` 迁移 SSE 流式对话、历史列表、消息查询三个端点
-`main.py` 中替换路由注册:移除 `xcx_ai_chat.router`,注册 `xcx_chat.router`
- 删除 `xcx_ai_chat.py`(不保留旧路径兼容)
**端点定义**
```python
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 组装。
**关键方法**
```python
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 组装逻辑**
```python
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 输出。
```python
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
```typescript
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()` 获取真实数据
- 响应字段映射:后端返回 `timestamp`ISO 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_app` → `test_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 契约:
```sql
-- 迁移脚本:扩展 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 '最后消息时间,用于历史列表排序和对话复用时限判断';
```
**新增索引**
```sql
-- 上下文对话查找(按 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 模式:
```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`
```sql
-- 迁移脚本:扩展 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 结构
```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_member``scd2_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 历史列表项,响应必须包含 `id`、`title`、`lastMessage`、`timestamp` 字段;*For any* CHAT-2 消息项,响应必须包含 `id`、`role`、`content`、`createdAt` 字段;*For any* CHAT-3 发送消息响应,必须包含 `userMessage` 和 `aiReply`,各含 `id`、`content`、`createdAt` 字段。
**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` 应等于当前用户 ID*For any* 不属于当前用户的 `chatId`CHAT-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` 应始终返回同一个 `chatId`*For any* `context_type='customer'` 或 `context_type='coach'` 的入口,若最后消息时间 ≤ 3 天则返回已有 `chatId`,若 > 3 天则返回新的 `chatId`*For any* `context_type='general'` 的入口,每次调用应返回不同的 `chatId`。
**Validates: Requirements 3.8, 3.9, 3.10**
### Property 7: referenceCard 持久化 Round Trip
*For any* 合法的 referenceCard JSON 对象(包含 `type`、`title`、`summary`、`data` 字段),存入 `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` 字段应为 `message`、`done`、`error` 三者之一;`message` 事件的 `data` 应包含 `token` 字段;`done` 事件的 `data` 应包含 `messageId` 和 `createdAt` 字段;`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 失败降级策略
```python
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 失败降级等
### 属性测试配置
- **测试库**[Hypothesis](https://hypothesis.readthedocs.io/)Python项目已使用
- **测试位置**`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 页面逐一验证真实数据渲染 |
### 测试执行命令
```bash
# 属性测试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
```

View File

@@ -0,0 +1,201 @@
# 需求文档 — RNS1.4CHAT 对齐与联调收尾
## 简介
RNS1.4 是 NS1 小程序后端 API 补全项目的最后一个子 spec负责 CHAT 模块路径迁移和功能补全、FDW 端到端验证、以及全量前后端联调。本 spec 依赖 RNS1.0-1.3 全部完成,是整个 RNS1 系列的收尾阶段,确保 13 个页面全部连接真实后端运行,无 mock 数据残留。
### 依赖
- RNS1.0(基础设施与契约重写)— 全局响应包装中间件、camelCase 转换、重写后的 API 契约
- RNS1.1(任务与绩效接口)— TASK-1/2、PERF-1/2、PIN 接口已实现
- RNS1.2(客户与助教接口)— CUST-1/2、COACH-1 接口已实现
- RNS1.3(三看板接口)— BOARD-1/2/3、CONFIG-1 接口已实现
- 后端已有 `xcx_ai.py`(现有 `/api/ai/*` 路由,需迁移)
- 前端已有 chat.ts 和 chat-history.ts 页面P5.2 交付),当前使用 mock 数据和模拟流式输出
### 来源文档
- `docs/prd/Neo_Specs/RNS1-split-plan.md` — 拆分计划主文档
- `docs/miniprogram-dev/API-contract.md` — API 契约RNS1.0 T0-5 重写后版本)
- `docs/prd/Neo_Specs/storyboard-walkthrough-assistant-view.md` — 助教视角走查报告GAP-45~51
## 术语表
- **Backend**FastAPI 后端应用,位于 `apps/backend/`
- **Miniprogram**:微信小程序前端应用,位于 `apps/miniprogram/`
- **CHAT_1_API**:对话历史列表接口 `GET /api/xcx/chat/history`,返回分页的对话列表
- **CHAT_2_API**:对话消息接口 `GET /api/xcx/chat/{chatId}/messages``GET /api/xcx/chat/messages?customerId={customerId}`,返回指定对话的消息列表
- **CHAT_3_API**:发送消息接口 `POST /api/xcx/chat/{chatId}/messages`,发送用户消息并获取 AI 同步回复
- **CHAT_4_SSE**SSE 流式端点 `POST /api/xcx/chat/stream`,通过 Server-Sent Events 逐 token 返回 AI 回复
- **SSE**Server-Sent Events服务端推送事件协议用于 AI 流式回复的逐 token 输出
- **referenceCard**:引用卡片,消息中附带的结构化上下文数据(类型/标题/摘要/键值对),用于展示客户概览等信息
- **FDW**PostgreSQL Foreign Data Wrapper用于从业务库 `zqyy_app` 访问 ETL 库 `etl_feiqiu` 的数据
- **chat_sessions**:业务库 `zqyy_app` 中的对话会话表,存储对话元数据
- **chat_messages**:业务库 `zqyy_app` 中的消息表,存储对话消息内容
- **Response_Wrapper**RNS1.0 实现的全局响应包装中间件,`text/event-stream` 响应自动跳过包装
- **items_sum**DWD-DOC 强制使用的消费金额口径
- **联调**:前后端联合调试,验证所有页面使用真实后端数据正常运行
## 需求
### 需求 1CHAT 路径迁移T4-1
**用户故事:** 作为前后端开发者,我希望 CHAT 模块的 API 路径从 `/api/ai/*` 统一迁移到 `/api/xcx/chat/*`,以便与其他小程序接口保持一致的路径命名规范。
#### 验收标准
1. THE Backend SHALL 将现有 `/api/ai/*` 路由全部迁移到 `/api/xcx/chat/*` 路径下,包括同步端点和 SSE 流式端点
2. THE Backend SHALL 在迁移后移除原 `/api/ai/*` 路径,不保留旧路径的兼容映射
3. THE Backend SHALL 将迁移后的路由注册到 `xcx_chat` router或等效命名与其他 `xcx_*` 路由模块保持一致的组织结构
4. THE Miniprogram SHALL 更新 `services/api.ts` 中所有 CHAT 相关的 API 调用路径,从 `/api/ai/*` 改为 `/api/xcx/chat/*`
5. WHEN CHAT_4_SSE 端点迁移到 `/api/xcx/chat/stream`THE Response_Wrapper SHALL 继续对 `text/event-stream` 响应跳过包装RNS1.0 已实现的行为不受路径变更影响)
### 需求 2实现 CHAT-1 对话历史列表T4-2 历史列表部分)
**用户故事:** 作为助教,我希望在对话历史页面看到所有对话记录(含对话标题和最后消息摘要),以便快速找到并继续之前的对话。
#### 验收标准
1. THE CHAT_1_API SHALL 实现 `GET /api/xcx/chat/history` 端点,接受 `page`(默认 1`pageSize`(默认 20查询参数返回分页的对话历史列表
2. THE CHAT_1_API SHALL 为每条对话记录返回以下字段:`id`(对话 ID`title`(对话标题)、`customerName`(关联客户姓名,可选)、`lastMessage`(最后一条消息摘要)、`timestamp`最后消息时间ISO 8601 格式)、`unreadCount`(未读消息数)
3. THE CHAT_1_API SHALL 从 `zqyy_app.chat_sessions` 查询对话列表,按最后消息时间倒序排列
4. THE CHAT_1_API SHALL 为 `title` 字段生成对话标题:优先使用对话会话中存储的自定义标题,若无自定义标题则使用关联客户姓名,若均无则使用首条消息内容的前 20 个字符作为标题
5. THE CHAT_1_API SHALL 返回标准分页字段:`total`(总记录数)、`page`(当前页码)、`pageSize`(每页条数)
6. THE CHAT_1_API SHALL 通过当前登录用户的身份过滤对话列表,确保每位助教只能看到自己的对话记录
### 需求 3实现 CHAT-2 对话消息查看T4-2 消息查看部分)
**用户故事:** 作为助教,我希望查看指定对话的消息列表,以便回顾与 AI 助手的对话内容。
#### 验收标准
1. THE CHAT_2_API SHALL 实现两个等效的消息查询端点:`GET /api/xcx/chat/{chatId}/messages`(通过对话 ID 查询)和 `GET /api/xcx/chat/messages?contextType={type}&contextId={id}`(通过上下文类型和 ID 查询)
2. THE CHAT_2_API SHALL 接受 `page`(默认 1`pageSize`(默认 50查询参数返回分页的消息列表
3. THE CHAT_2_API SHALL 为每条消息返回以下字段:`id`(消息 ID`role``user``assistant`)、`content`(消息内容)、`createdAt`创建时间ISO 8601 格式)、`referenceCard`(引用卡片,可选)
4. THE CHAT_2_API SHALL 统一使用 `createdAt` 作为消息时间字段名(替代前端使用的 `timestamp` 和旧契约的 `created_at`,遵循 camelCase 规范)
5. THE CHAT_2_API SHALL 从 `zqyy_app.chat_messages` 查询消息列表,按 `createdAt` 正序排列(最早的消息在前)
6. THE CHAT_2_API SHALL 在响应中返回 `chatId` 字段,供前端后续发送消息时使用(尤其是通过上下文入口时,前端需要获取对应的 `chatId`
7. THE CHAT_2_API SHALL 返回标准分页字段:`total`(总记录数)、`page`(当前页码)、`pageSize`(每页条数)
#### 3.2 上下文对话复用规则
8. WHEN 通过 `contextType``contextId` 查询参数调用消息端点时THE CHAT_2_API SHALL 按以下规则查找或创建对话:
- `contextType='task'`:查找同一用户、同一 `contextId`taskId的已有对话找到则复用无时限找不到则新建
- `contextType='customer'``contextType='coach'`:查找同一用户、同一 `contextId` 的已有对话,若最后消息时间 ≤ 3 天则复用,> 3 天或不存在则新建
- `contextType='general'`:始终新建对话
9. THE CHAT_2_API SHALL 在 `ai_conversations` 中记录 `context_type``context_id` 字段,用于后续对话查找
10. THE CHAT_2_API SHALL 确保对话复用查找基于 `(user_id, site_id, context_type, context_id)` 组合,不同用户的对话互不影响
### 需求 4CHAT referenceCard 支持T4-3
**用户故事:** 作为助教,我希望在与 AI 助手对话时,消息中能附带客户概览卡片(含余额、消费、到店频次等键值对数据),以便在对话上下文中快速查看客户关键信息。
#### 验收标准
1. THE CHAT_2_API SHALL 为消息返回可选的 `referenceCard` 字段,结构包含:`type`(引用类型,`customer``record` 枚举)、`title`(卡片标题,如 `"张伟 — 消费概览"`)、`summary`(摘要文字)、`data``Record<string, string>` 键值对详情,如 `{ "近30天消费": "¥2,380", "到店次数": "8次" }`
2. WHEN AI 助手回复消息涉及特定客户时THE Backend SHALL 从 FDW 查询该客户的关键指标(余额、近期消费、到店频次等),组装为 `referenceCard` 附加到 AI 回复消息中
3. THE Backend SHALL 将 `referenceCard` 数据持久化存储到 `chat_messages` 表中(作为 JSON 字段),以便历史消息查看时仍能展示引用卡片
4. THE Miniprogram SHALL 在 chat 页面的消息列表中,检测消息的 `referenceCard` 字段,若存在则渲染为结构化卡片组件(标题 + 摘要 + 键值对列表)
#### 4.2 多入口参数路由GAP-50
5. THE Miniprogram SHALL 在 chat 页面的 `onLoad(options)` 中实现多入口参数路由逻辑,按以下优先级处理入口参数:
-`options.historyId` 存在(从 chat-history 跳转),使用 `historyId` 作为 `chatId` 直接加载历史消息
-`options.taskId` 存在(从 task-detail 跳转),使用 `contextType=task` + `contextId=taskId` 调用 CHAT_2_API由后端查找同一任务的已有对话始终复用无时限
-`options.customerId` 存在(从 customer-detail 跳转),使用 `contextType=customer` + `contextId=customerId` 调用 CHAT_2_API由后端按 3 天时限判断复用或新建
-`options.coachId` 存在(从 coach-detail 跳转),使用 `contextType=coach` + `contextId=coachId` 调用 CHAT_2_API由后端按 3 天时限判断复用或新建
6. WHEN 通过上下文入口进入对话后THE Miniprogram SHALL 将后端返回的 `chatId` 缓存到页面 data 中,后续发送消息和 SSE 流式请求均使用该 `chatId`
7. IF chat 页面未收到任何入口参数(`historyId`/`taskId`/`customerId`/`coachId` 均为空THEN THE Miniprogram SHALL 使用 `contextType=general` 调用 CHAT_2_API 创建一个通用对话
### 需求 5CHAT-3 发送消息T4-2 发送部分)
**用户故事:** 作为助教,我希望在对话页面发送消息后能立即收到 AI 的同步回复,以便在不支持 SSE 的场景下也能正常使用 AI 助手。
#### 验收标准
1. THE CHAT_3_API SHALL 实现 `POST /api/xcx/chat/{chatId}/messages` 端点,接受请求体 `{ content: string }`
2. THE CHAT_3_API SHALL 将用户消息存入 `chat_messages` 表,调用 AI 服务获取回复,将 AI 回复也存入 `chat_messages`
3. THE CHAT_3_API SHALL 返回包含用户消息和 AI 回复的响应:`userMessage`(含 `id`/`content`/`createdAt`)和 `aiReply`(含 `id`/`content`/`createdAt`
4. THE CHAT_3_API SHALL 在发送消息后更新 `chat_sessions` 表的 `lastMessage` 和最后消息时间字段
5. IF AI 服务调用失败或超时THEN THE CHAT_3_API SHALL 仍保存用户消息,并返回 AI 回复为错误提示消息(如 `{ content: "抱歉AI 助手暂时无法回复,请稍后重试" }`HTTP 状态码保持 200
6. THE CHAT_3_API SHALL 验证请求的 `chatId` 属于当前登录助教,不属于时返回 HTTP 403
### 需求 6CHAT-4 SSE 流式端点T4-1 SSE 部分)
**用户故事:** 作为助教,我希望 AI 助手的回复能以流式方式逐字显示,以便获得更自然的对话体验,减少等待感。
#### 验收标准
1. THE CHAT_4_SSE SHALL 实现 `POST /api/xcx/chat/stream` 端点,接受请求体 `{ chatId: string, content: string }`,响应内容类型为 `text/event-stream`
2. THE CHAT_4_SSE SHALL 发送以下三种 SSE 事件类型:
- `event: message` — 逐 token 输出,`data``{"token": "<文本片段>"}`
- `event: done` — 流结束,`data``{"messageId": "<完整消息ID>", "createdAt": "<ISO 8601>"}`
- `event: error` — 错误,`data``{"message": "<错误描述>"}`
3. THE CHAT_4_SSE SHALL 在流开始前将用户消息存入 `chat_messages` 表,在流结束后将完整的 AI 回复存入 `chat_messages`
4. THE CHAT_4_SSE SHALL 在流结束后更新 `chat_sessions` 表的 `lastMessage` 和最后消息时间字段
5. THE Response_Wrapper SHALL 对 `text/event-stream` 响应跳过全局包装,直接透传 SSE 事件流RNS1.0 已实现)
6. THE CHAT_4_SSE SHALL 验证请求的 `chatId` 属于当前登录助教,不属于时返回 HTTP 403此时响应为普通 JSON 错误,非 SSE
7. THE Miniprogram SHALL 将 chat 页面现有的 `simulateStreamOutput()`(模拟逐字输出)替换为真实的 SSE 连接,通过 `wx.request` 或兼容方案接收 `text/event-stream` 响应
### 需求 7FDW 端到端验证T4-4
**用户故事:** 作为后端开发者,我希望验证所有 FDW 查询在测试环境链路(`test_zqyy_app``test_etl_feiqiu`)上正常工作,以便确保 RNS1.1-1.3 实现的所有接口在真实数据链路上不会因 FDW 连接、权限或性能问题而失败。
#### 验收标准
1. THE Backend SHALL 验证所有 `fdw_etl.*` 视图在 `test_zqyy_app` 数据库中可正常访问,包括但不限于:`v_dws_assistant_salary_calc``v_dwd_assistant_service_log``v_dim_member``v_dim_assistant``v_dws_member_consumption_summary``v_dws_member_assistant_relation_index``v_dws_finance_*` 系列视图
2. THE Backend SHALL 验证每个 FDW 视图的查询响应时间在可接受范围内(单次查询不超过 3 秒),对超时的查询记录慢查询日志并评估是否需要添加索引
3. THE Backend SHALL 验证 FDW 查询在带有典型过滤条件(如 `assistant_id``member_id`、日期范围)时能正确返回数据,且结果集与直接查询 `test_etl_feiqiu` 的结果一致
4. IF 某个 FDW 视图不存在或权限不足THEN THE Backend SHALL 记录具体的错误信息(视图名、错误类型),并在验证报告中标注需要 DBA 介入修复
5. THE Backend SHALL 检查 FDW 链路上的关键索引是否存在:`chat_sessions` 表的 `(assistant_id, customer_id)` 索引、`chat_messages` 表的 `(session_id, created_at)` 索引
### 需求 8前端联调修复 — notes 页触底加载T4-5 F11
**用户故事:** 作为助教,我希望在备注列表页面滚动到底部时自动加载更多备注,以便查看全部备注记录而不需要手动翻页。
#### 验收标准
1. THE Miniprogram SHALL 在 notes 页面实现 `onReachBottom()` 生命周期函数,当用户滚动到页面底部时自动请求下一页备注数据
2. WHEN 触底加载触发时THE Miniprogram SHALL 将 `page` 参数加 1调用 `fetchNotes({ page, pageSize })` 接口,将返回的备注追加到已有列表末尾
3. WHEN 后端返回的 `hasMore``false` 或返回的备注数量小于 `pageSize`THE Miniprogram SHALL 停止触底加载,显示"没有更多了"提示
4. THE Miniprogram SHALL 在触底加载过程中显示加载状态指示器,防止重复触发请求
### 需求 9前端联调修复 — customer-service-records 按月请求T4-5 F10
**用户故事:** 作为助教,我希望客户服务记录页面在切换月份时向后端请求对应月份的数据,以便在数据量大时页面仍能快速响应,而不是全量加载后本地过滤。
#### 验收标准
1. THE Miniprogram SHALL 修改 customer-service-records 页面的月份切换逻辑,从当前的"全量加载后本地过滤"改为"按月请求 API"
2. WHEN 用户切换月份时THE Miniprogram SHALL 使用新的 `year`/`month` 参数调用 `fetchCustomerRecords({ customerId, year, month })` 接口,加载对应月份的服务记录
3. THE Miniprogram SHALL 在月份切换时清空已有记录列表,显示加载状态,待新数据返回后渲染
4. THE Miniprogram SHALL 在首次加载时默认请求当前月份的数据,而非全量数据
5. THE Backend SHALL 确保 CUST-2 接口支持 `year``month` 查询参数仅返回指定月份的服务记录RNS1.2 T2-4 已实现按月查询能力)
### 需求 10全量前后端联调T4-5 联调部分)
**用户故事:** 作为开发团队,我们希望 13 个小程序页面全部连接真实后端运行,无 mock 数据残留,以便确认整个应用在真实数据环境下功能完整、交互正常。
#### 验收标准
1. THE Miniprogram SHALL 移除所有页面中的内联 mock 数据和 mock 数据导入(`import { mockXxx } from '../../utils/mock-data'`),全部替换为真实 API 调用
2. THE Miniprogram SHALL 确保以下 13 个页面均能使用真实后端数据正常渲染:`task-list``task-detail``notes``performance``performance-records``customer-detail``customer-service-records``coach-detail``board-coach``board-customer``board-finance``chat-history``chat`
3. WHEN 某个页面的 API 调用失败时THE Miniprogram SHALL 显示友好的错误提示(如 Toast 或空状态占位),不出现白屏或未捕获异常
4. THE Miniprogram SHALL 验证所有页面间的跳转参数传递正确RNS1.0 T0-6 已修复的跨页面参数),确保目标页面能正确加载对应数据
5. THE Miniprogram SHALL 验证 chat 页面从 4 个入口task-detail、customer-detail、coach-detail、chat-history进入时均能正确关联上下文并加载对应对话
6. IF 联调过程中发现新的 Bug 或数据不一致问题THEN THE Miniprogram 和 Backend SHALL 在本 spec 范围内修复,修复内容记录到联调问题清单中
### 需求 11全局约束与权限控制
**用户故事:** 作为系统管理员,我希望所有 CHAT 接口遵循统一的权限控制和数据隔离规则,以确保每位助教只能访问自己的对话数据。
#### 验收标准
1. THE Backend SHALL 对所有 RNS1.4 CHAT 接口CHAT-1、CHAT-2、CHAT-3、CHAT-4执行 `require_approved()` 权限检查,确保用户状态为 `approved`
2. THE Backend SHALL 通过当前登录用户的身份信息过滤对话数据,确保每位助教只能访问自己创建的或与自己关联的对话
3. IF 当前用户未通过审核(状态非 `approved`THEN THE Backend SHALL 返回 HTTP 403 `{ code: 403, message: "用户未通过审核,无法访问此资源" }`
4. THE Backend SHALL 对所有 CHAT 接口的响应字段名统一使用 camelCase 格式(与 RNS1.0 的 CamelCase_Converter 一致)
5. THE Backend SHALL 确保 CHAT 模块的错误响应格式与全局异常处理器一致:`{ code: <HTTP状态码>, message: <错误详情> }`
6. WHEN CHAT 模块查询 FDW 数据(如为 referenceCard 获取客户指标THE Backend SHALL 遵循 DWD-DOC 强制规则:金额使用 `items_sum` 口径,会员信息通过 `member_id` JOIN `dim_member` 获取

View File

@@ -0,0 +1,311 @@
# 实施计划RNS1.4 CHAT 对齐与联调收尾
## 概述
按照设计文档的 8 个组件将实施拆分为DDL 迁移 → 后端 Schema/Service/Router → FDW 验证 → 前端改造 → 联调收尾。每个任务增量构建确保无孤立代码。属性测试Hypothesis和单元测试作为可选子任务紧跟实现步骤。
## 任务
- [x] 1. DDL 迁移:扩展 ai_conversations 和 ai_messages 表
- [x] 1.1 创建 DDL 迁移脚本 `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`
- ALTER TABLE `biz.ai_conversations` 新增 `context_type varchar(20)``context_id varchar(50)``title varchar(200)``last_message text``last_message_at timestamptz` 五个字段
- ALTER TABLE `biz.ai_messages` 新增 `reference_card` jsonb 字段
- 创建索引 `idx_ai_conv_context` ON `(user_id, site_id, context_type, context_id, last_message_at DESC NULLS LAST) WHERE context_type IS NOT NULL`
- 创建排序索引 `idx_ai_conv_last_msg` ON `(user_id, site_id, last_message_at DESC NULLS LAST)`
- 添加 COMMENT ON COLUMN 注释
- _需求: R2.3, R3.8, R3.10, R4.3, R7.5_
- [x] 2. 后端 Pydantic Schema 定义(组件 3
- [x] 2.1 创建 `apps/backend/app/schemas/xcx_chat.py`
- 继承 `CamelModel` 基类,定义 `ChatHistoryItem``ChatHistoryResponse``ReferenceCard``ChatMessageItem``ChatMessagesResponse``SendMessageRequest``SendMessageResponse``MessageBrief``ChatStreamRequest`
- 字段类型和可选性严格遵循设计文档组件 3 定义
- _需求: R2.2, R3.3, R4.1, R5.1, R5.3, R6.1, R11.4_
- [x] 2.2 编写 Schema 序列化单元测试 `apps/backend/tests/unit/test_xcx_chat_schema.py`
- 验证 ChatHistoryItem / ChatMessageItem / SendMessageResponse 的 camelCase 序列化输出
- 验证 ReferenceCard 可选字段为 None 时不报错
- _需求: R2.2, R3.3, R5.3, R11.4_
- [x] 3. 后端 chat_service 业务逻辑层(组件 2
- [x] 3.1 创建 `apps/backend/app/services/chat_service.py`
- 实现 `ChatService` 类,包含 `get_chat_history``get_or_create_session``get_messages``send_message_sync``build_reference_card``generate_title` 方法
- `get_chat_history`:查询 `biz.ai_conversations`,按 `last_message_at` 倒序JOIN `v_dim_member` 获取 `customerName`,分页返回
- `get_or_create_session`:按 `(user_id, site_id, context_type, context_id)` 查找或创建对话。复用规则task 入口始终复用无时限customer/coach 入口 ≤ 3 天复用、> 3 天新建general 入口始终新建
- `get_messages`:查询 `biz.ai_messages`,按 `created_at` 正序,验证 chatId 归属当前用户
- `send_message_sync`:存入用户消息 → 调用 AI → 存入 AI 回复 → 更新 session 元数据AI 失败时返回错误提示消息HTTP 200
- ⚠️ **P5 PRD 合规**:对话落库必须遵循 `docs/prd/specs/P5-miniapp-ai-integration.md` 数据写入规则:
- `app_id` 固定为 `app1_chat`
- 用户消息发送时即写入 `ai_messages`role=user
- 流式完成后完整 assistant 回复写入 `ai_messages`role=assistant`tokens_used`
- 首条消息为页面上下文 JSON`current_time`/`source_page`/`page_context`/`screen_content`
- `get_or_create_session` 仅用于 task/customer/coach 入口的对话复用task 无时限customer/coach 3 天时限general 入口始终新建(保持 P5 PRD 兼容)
- `build_reference_card`:从 FDW 查询客户指标(`items_sum` 口径),组装 referenceCard JSON
- `generate_title`:自定义标题 > 客户姓名 > 首条消息前 20 字
- _需求: R2.1-R2.6, R3.1-R3.10, R4.1-R4.3, R5.1-R5.6, R11.2, R11.6_
- [x] 3.2 编写属性测试:标题生成优先级
- **Property 4: 对话标题生成优先级**
- 使用 Hypothesis `st.fixed_dictionaries` 生成随机 title/customer_name/first_message 组合
- 验证:有 title 用 title否则用 customer_name否则用首条消息前 20 字,结果始终非空
- **验证: 需求 R2.4**
- [x] 3.3 编写属性测试:对话复用规则正确性
- **Property 6: 对话复用规则正确性**
- 使用 Hypothesis 生成随机 context_type/context_id/last_message_at 组合
- 验证task 入口同一 context_id 始终返回同一 chatIdcustomer/coach 入口 ≤ 3 天复用、> 3 天新建general 入口每次返回不同 chatId
- **验证: 需求 R3.8, R3.9, R3.10**
- [x] 3.4 编写属性测试referenceCard Round Trip
- **Property 7: referenceCard 持久化 Round Trip**
- 使用 Hypothesis 生成随机 referenceCard JSONtype/title/summary/data
- 验证JSON 序列化→存储→读取→反序列化等于原始对象
- **验证: 需求 R4.1, R4.3**
- [x] 3.5 编写属性测试:消息持久化与会话元数据更新
- **Property 8: 消息持久化与会话元数据更新**
- 使用 Hypothesis `st.text(min_size=1, max_size=500)` 生成随机消息内容
- 验证:发送后 ai_messages 包含用户消息和 AI 回复session 的 last_message 和 last_message_at 已更新
- **验证: 需求 R5.2, R5.4, R6.3, R6.4**
- [x] 3.6 编写单元测试AI 失败降级
- 测试文件 `apps/backend/tests/unit/test_xcx_chat_ai_fallback.py`
- 验证 AI 服务超时/异常时用户消息仍保存AI 回复为错误提示消息HTTP 200
- _需求: R5.5_
- [x] 4. 后端路由迁移与 CHAT-1/2/3/4 端点实现(组件 1
- [x] 4.1 创建 `apps/backend/app/routers/xcx_chat.py`,实现 CHAT-1/2/3/4 五个端点
- `GET /history` — CHAT-1 对话历史列表,调用 `chat_service.get_chat_history`
- `GET /{chat_id}/messages` — CHAT-2a 通过 chatId 查询消息
- `GET /messages?contextType=&contextId=` — CHAT-2b 通过上下文查询消息(按复用规则自动查找/创建对话)
- `POST /{chat_id}/messages` — CHAT-3 发送消息(同步回复)
- `POST /stream` — CHAT-4 SSE 流式端点,返回 `StreamingResponse(media_type="text/event-stream")`
- 所有端点使用 `Depends(require_approved())` 权限检查
- chatId 归属验证CHAT-3/4 不属于当前用户返回 HTTP 403
- _需求: R1.1, R1.3, R2.1, R3.1, R5.1, R6.1, R11.1, R11.2_
- [x] 4.2 在 `apps/backend/app/main.py` 中注册 `xcx_chat.router`,移除 `xcx_ai_chat.router`
- 删除 `xcx_ai_chat.py` 文件(不保留旧路径兼容)
- _需求: R1.2, R1.3_
- [x] 4.3 编写属性测试SSE 事件类型有效性
- **Property 9: SSE 事件类型有效性**
- 使用 Hypothesis `st.sampled_from(["message", "done", "error"])` + 对应 data 结构
- 验证事件类型为三者之一data 结构符合定义message→token, done→messageId+createdAt, error→message
- **验证: 需求 R6.2**
- [x] 4.4 编写属性测试:列表排序不变量
- **Property 3: 列表排序不变量**
- 使用 Hypothesis `st.lists(st.datetimes())` 生成随机时间戳列表
- 验证CHAT-1 对话列表按时间倒序CHAT-2 消息列表按时间正序
- **验证: 需求 R2.3, R3.5**
- [x] 4.5 编写单元测试:路由迁移与权限控制
- 测试文件 `apps/backend/tests/unit/test_xcx_chat_routes.py`
- 验证 `/api/xcx/chat/history` 返回 200需认证`/api/ai/conversations` 返回 404
- 验证未审核用户收到 403chatId 不属于当前用户收到 403
- **Property 1: 路由迁移完整性** / **Property 5: 权限控制与数据隔离**
- **验证: 需求 R1.1, R1.2, R5.6, R6.6, R11.1, R11.3**
- [x] 5. 检查点 — 后端实现验证
- 确保所有后端测试通过ask the user if questions arise.
- [x] 6. FDW 端到端验证脚本(组件 7
- [x] 6.1 创建 `scripts/ops/verify_fdw_e2e.py`
- 验证所有 `fdw_etl.*` 视图在 `test_zqyy_app` 中可访问SELECT 1 FROM ... LIMIT 1
- 验证带典型过滤条件assistant_id、member_id、日期范围的查询响应时间 < 3s
- 检查关键索引存在:`chat_sessions(assistant_id, customer_id)``chat_messages(session_id, created_at)`
- 输出结构化 JSON 报告,失败项标注需 DBA 介入
- 使用 `load_dotenv` 加载根 `.env`,连接 `test_zqyy_app`(遵循 testing-env.md 规范)
- _需求: R7.1, R7.2, R7.3, R7.4, R7.5_
- [x] 7. 前端 services/api.ts CHAT 模块对接(组件 6
- [x] 7.1 修改 `apps/miniprogram/miniprogram/services/api.ts`
- `fetchChatHistory()`:调用 `GET /api/xcx/chat/history`
- `fetchChatMessages(chatId)`:调用 `GET /api/xcx/chat/{chatId}/messages`
- 新增 `fetchChatMessagesByContext(contextType, contextId)`:调用 `GET /api/xcx/chat/messages?contextType={type}&contextId={id}`
- `sendChatMessage(chatId, content)`:调用 `POST /api/xcx/chat/{chatId}/messages`
- 移除所有 CHAT 相关 mock 数据导入,`USE_REAL_API` 对 CHAT 模块设为 `true`
- _需求: R1.4, R10.1_
- [x] 8. 前端 chat 页面改造(组件 4
- [x] 8.1 修改 `apps/miniprogram/miniprogram/pages/chat/chat.ts` 实现多入口参数路由
- `onLoad(options)` 中按优先级处理:`historyId``taskId``customerId``coachId` → 无参数(通用对话)
- `historyId` 入口:直接用作 chatId 加载历史消息
- `taskId` 入口:调用 `fetchChatMessagesByContext('task', taskId)`,同一 taskId 始终复用同一对话(无时限)
- `customerId` 入口:调用 `fetchChatMessagesByContext('customer', customerId)`,≤ 3 天复用、> 3 天新建
- `coachId` 入口:调用 `fetchChatMessagesByContext('coach', coachId)`,≤ 3 天复用、> 3 天新建
- 无参数入口:调用 `fetchChatMessagesByContext('general', '')`,始终新建
- _需求: R4.5, R4.6, R4.7_
- [x] 8.2 修改 chat.ts 将 `simulateStreamOutput()` 替换为真实 SSE 连接
- 使用 `wx.request` + `enableChunked: true` 接收 `POST /api/xcx/chat/stream` 的 SSE 响应
- 解析 `event: message`(逐 token 追加)、`event: done`(流结束)、`event: error`(错误处理)
- 移除 `simulateStreamOutput()``mockAIReplies` 相关代码
- SSE 连接中断时显示"连接中断"提示,允许重试
- _需求: R6.7, R10.1_
- [x] 8.3 确保 chat 页面 referenceCard 渲染与真实 API 数据兼容
- 验证 `toDataList()` 和 WXML 模板能正确渲染后端返回的 referenceCard 结构
- _需求: R4.4_
- [x] 9. 前端 chat-history 页面改造(组件 5
- [x] 9.1 修改 `apps/miniprogram/miniprogram/pages/chat-history/chat-history.ts`
- 移除 `mockChatHistory` 导入,调用 `fetchChatHistory()` 获取真实数据
- 响应字段映射:后端 `timestamp`ISO 8601`formatRelativeTime()` 处理
- 点击对话项跳转 chat 页面时传递 `historyId` 参数
- _需求: R2.1, R10.1, R10.2_
- [x] 10. 前端联调修复(组件 8
- [x] 10.1 修改 notes 页面实现触底加载
-`apps/miniprogram/miniprogram/pages/notes/notes.ts` 实现 `onReachBottom()` 生命周期函数
- 维护 `page` 状态,触底时 `page++` 调用 `fetchNotes({ page, pageSize })`
- 追加数据到已有列表,`hasMore === false` 时停止加载并显示"没有更多了"
- 加载过程中显示加载状态指示器,防止重复触发
- _需求: R8.1, R8.2, R8.3, R8.4_
- [x] 10.2 修改 customer-service-records 页面实现按月请求
-`apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` 修改月份切换逻辑
- 月份切换时调用 `fetchCustomerRecords({ customerId, year, month })`,清空列表 → loading → 渲染
- 首次加载默认当前月份数据
- _需求: R9.1, R9.2, R9.3, R9.4_
- [x] 11. 检查点 — 前端改造验证
- 确保所有前端改造完成chat 页面 4 个入口task-detail、customer-detail、coach-detail、chat-history均能正确进入并加载对话。ask the user if questions arise.
- [x] 12. Mock 数据移除与全量联调
- [x] 12.1 移除所有页面中的 mock 数据残留
- 搜索并移除所有 `import { mockXxx } from '../../utils/mock-data'` 引用
- 确保 13 个页面task-list、task-detail、notes、performance、performance-records、customer-detail、customer-service-records、coach-detail、board-coach、board-customer、board-finance、chat-history、chat均使用真实 API
- _需求: R10.1, R10.2_
- [x] 12.2 验证全量联调
- 确保所有页面 API 调用失败时显示友好错误提示Toast 或空状态占位),不出现白屏
- 验证页面间跳转参数传递正确RNS1.0 T0-6 已修复的跨页面参数)
- 验证 chat 页面从 4 个入口进入时均能正确关联上下文
- _需求: R10.2, R10.3, R10.4, R10.5_
- [x] 13. 全链路端到端测试(真实 AI 接口)
- [x] 13.1 后端全链路测试:使用真实百炼 API 验证 CHAT-3同步和 CHAT-4SSE 流式)
- 启动后端服务连接 `test_zqyy_app`,使用真实测试用户 token
- 调用 `POST /api/xcx/chat/{chatId}/messages` 发送真实消息,验证:
- AI 回复内容质量(非乱码、语义相关、中文正常)
- 用户消息和 AI 回复均已持久化到 `biz.ai_messages`
- `biz.ai_conversations``last_message``last_message_at` 已更新
- `tokens_used` 字段已记录
- 调用 `POST /api/xcx/chat/stream` 验证 SSE 流式返回:
- 逐 token 事件格式正确(`event: message``event: done`
- 完整回复拼接后语义通顺
- 流结束后消息已落库
- 手动评估 AI 返回内容质量(至少 3 轮对话),记录评估结果
- _需求: R5.2, R5.4, R6.2, R6.3, R6.4, AC1, AC9_
- [x] 13.2 前端→后端→AI→数据库全链路验证
- 在微信开发者工具中启动小程序,连接本地后端
- 从 4 个入口task-detail、customer-detail、coach-detail、chat-history进入 chat 页面
- 每个入口发送至少 1 条消息,验证:
- SSE 流式逐字显示正常
- 消息发送后页面状态正确loading → 显示回复)
- 返回 chat-history 页面能看到刚才的对话记录
- referenceCard 在有客户关联时正确渲染(如有数据)
- 验证错误场景:网络断开时的提示、空消息拦截
- _需求: R6.7, R10.5, R4.4_
- [x] 13.3 AI 对话落库合规性验证(对照 P5 PRD + 用户确认的复用规则)
- 验证对话复用规则task 入口同一 taskId 始终复用customer/coach 入口 ≤ 3 天复用、> 3 天新建general 入口始终新建
- 验证首条消息格式:应用 1 的首条消息应为页面上下文 JSON`source_page`/`page_context`/`screen_content`/`current_time`),对照 P5 PRD 应用 1 Prompt 数据结构
- 验证 `app_id` 字段CHAT 模块对话的 `app_id` 应为 `app1_chat`
- 验证 `ai_messages.role` 值:仅 `user`/`assistant`/`system` 三种CHECK 约束)
- 验证 `tokens_used` 记录AI 回复消息应记录 token 消耗量
- 验证 `source_page``source_context`:从不同入口进入时应正确记录来源页面和上下文
- 验证 `context_type``context_id`:不同入口写入正确的上下文类型和 ID
- _需求: AC9, P5-PRD 数据写入规则_
- [x] 14. DDL 迁移合并到主 DDL 基线
- [x] 14.1 执行迁移脚本到 `test_zqyy_app`
- 运行 `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`(任务 1 创建的脚本)
- 验证新字段和索引已正确创建(使用 BD 手册中的验证 SQL
- _需求: R2.3, R3.8, R3.10, R4.3, R7.5_
- [x] 14.2 合并到主 DDL 基线 `docs/database/ddl/zqyy_app__biz.sql`
-`biz.ai_conversations` 表定义中追加 5 个新字段:`context_type varchar(20)``context_id varchar(50)``title varchar(200)``last_message text``last_message_at timestamptz`
-`biz.ai_messages` 表定义中追加 1 个新字段:`reference_card jsonb`
- 在索引区追加 2 个新索引:`idx_ai_conv_context`条件索引WHERE context_type IS NOT NULL`idx_ai_conv_last_msg`
- 更新文件头部的"生成日期"注释
- _需求: DDL 基线同步_
- [x] 15. 文档更新落地
- [x] 15.1 更新 BD 手册 `docs/database/BD_Manual_ai_tables.md`
-`biz.ai_conversations` 字段明细中追加 5 个新字段context_type/context_id/title/last_message/last_message_at
-`biz.ai_messages` 字段明细中追加 reference_card 字段
- 在约束与索引表中追加 2 个新索引idx_ai_conv_context、idx_ai_conv_last_msg
- 更新兼容性影响:标注 RNS1.4 CHAT 模块依赖新字段
- 更新验证 SQL追加新字段和索引的验证查询
- 更新回滚策略:追加 DROP INDEX 和 ALTER TABLE DROP COLUMN
- _规范: db-docs.md 强制要求_
- [x] 15.2 更新 API 契约 `docs/miniprogram-dev/API-contract.md`
- CHAT 部分路径从 `/api/ai/*` 更新为 `/api/xcx/chat/*`
- 补充 CHAT-1历史列表、CHAT-2a/2b消息查询含 customerId 参数、CHAT-3发送消息端点定义
- 补充 referenceCard 结构定义
- 补充 SSE 事件类型定义message/done/error
- _需求: R1.1, R1.4_
- [x] 15.3 更新后端 API 参考 `apps/backend/docs/API-REFERENCE.md`
- 新增 `xcx_chat` 路由模块文档5 个端点)
- 移除 `xcx_ai_chat` 路由模块文档(已删除)
- _需求: R1.2, R1.3_
- [x] 15.4 更新后端 README `apps/backend/README.md`
- 路由模块摘要中:移除 `xcx_ai_chat`,新增 `xcx_chat`CHAT-1/2/3/4
- 服务层中:新增 `chat_service.py` 说明
- _需求: R1.3_
- [x] 15.5 更新文档地图 `docs/DOCUMENTATION-MAP.md`
- 在 3.1 FastAPI 后端部分新增 RNS1.4 模块xcx_chat.py、chat_service.py、xcx_chat schema
- 在 5.6 Spec 文件表中新增 `rns1-chat-integration` 条目
- 更新"最后更新"日期
- _规范: doc-map.md 归档规则_
- [x] 15.6 更新 RNS1 拆分计划 `docs/prd/Neo_Specs/RNS1-split-plan.md`
- 标注 RNS1.4 状态为"实施中"或"已完成"(视进度)
- _需求: 项目追踪_
- [x] 16. 最终检查点 — 全量验证
- 确保所有测试通过13 个页面均连接真实后端运行,无 mock 数据残留
- 确保 DDL 迁移已合并到主基线BD 手册已同步更新
- 确保 API 契约、后端 README、文档地图均已更新
- 确保 AI 对话落库符合 P5 PRD 规范
- ask the user if questions arise.
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
- 每个任务引用了具体的需求编号R1-R11以确保可追溯性
- 属性测试验证通用正确性属性Property 1-9单元测试验证具体边界条件
- 检查点任务确保增量验证,避免问题累积
- 后端使用 PythonFastAPI + Pydantic前端使用 TypeScript微信小程序
### PRD 合规注意事项
- **P5 PRD 数据写入规则**`docs/prd/specs/P5-miniapp-ai-integration.md`
- 流式返回完成后,完整 assistant 回复写入 `ai_messages`role=assistant
- 用户消息在发送时即写入role=user
- 所有 AI 调用记录写入 `ai_conversations` + `ai_messages`(含 tokens_used 统计)
- 首条消息为页面上下文 JSON`source_page`/`page_context`/`screen_content`/`current_time`
- `app_id` 固定为 `app1_chat`
- **对话复用规则**(用户已确认,覆盖 P5 PRD 的"始终新建"规则):
- `task` 入口:同一 taskId 始终复用同一对话(无时限)
- `customer` / `coach` 入口:最后消息 ≤ 3 天复用,> 3 天新建
- `general` 入口(无参数):始终新建
- `chat-history` 入口:直接打开已有对话(传 historyId
### 文档更新清单
| 文档 | 更新内容 | 任务 |
|------|---------|------|
| `docs/database/ddl/zqyy_app__biz.sql` | 合并 5+1 新字段、2 新索引 | 14.2 |
| `docs/database/BD_Manual_ai_tables.md` | 新字段context_type/context_id/title/last_message/last_message_at/reference_card/索引/兼容性/回滚/验证 SQL | 15.1 |
| `docs/miniprogram-dev/API-contract.md` | CHAT 路径迁移 + 新端点定义 | 15.2 |
| `apps/backend/docs/API-REFERENCE.md` | xcx_chat 路由模块文档 | 15.3 |
| `apps/backend/README.md` | 路由模块 + 服务层更新 | 15.4 |
| `docs/DOCUMENTATION-MAP.md` | RNS1.4 模块 + spec 条目 | 15.5 |
| `docs/prd/Neo_Specs/RNS1-split-plan.md` | RNS1.4 状态更新 | 15.6 |