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

@@ -528,7 +528,7 @@ interface CoachTask {
// ─── 最亲密助教 ───
interface FavoriteCoach {
/** 亲密度 emoji如 "💖"、"💛" */
/** 亲密度 emoji如 "💖"、"🧡"、"💛"、"💙" */
emoji: string
/** 助教姓名 */
name: string
@@ -699,7 +699,7 @@ interface CustomerNote {
| 字段 | 类型 | 说明 | 数据源 |
|------|------|------|--------|
| `emoji` | string | 亲密度 emoji💖 = 高亲密度 ≥ 0.7,💛 = 中亲密度 < 0.7 | 后端根据 `relationIndex` 映射 |
| `emoji` | string | 亲密度 emoji💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5),后端 compute_heart_icon() 根据 rs_display 映射 | 后端根据 `relationIndex` 映射 |
| `name` | string | 助教姓名 | `biz.users.nickname` |
| `relationIndex` | string | 关系指数,如 `"0.92"` | 后端根据服务频率/消费金额综合计算 |
| `indexColor` | string | 关系指数对应颜色 | 后端根据 `relationIndex` 阈值映射 |
@@ -759,7 +759,7 @@ interface CustomerNote {
| `settle_type` 过滤 | 消费记录取正向交易 `settle_type IN (1, 3)`,本表无 `is_delete` 字段 |
| 废单排除 | 使用 `dwd_assistant_service_log_ex.is_trash` 排除废单,`dwd_assistant_trash_event` 已废弃 |
| `type` 枚举映射 | `table`(台桌消费)/ `shop`(酒水/商品消费)/ `recharge`(充值),后端根据 `settle_type` 和业务逻辑映射 |
| 亲密度 emoji | 💖 = 高亲密度(关系指数 ≥ 0.7),💛 = 中亲密度(关系指数 < 0.7 |
| 亲密度 emoji | 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5) |
#### 数据源映射表
@@ -867,8 +867,8 @@ interface CoachBoardItem {
skills: Array<{ text: string; cls: string }>
/**
* Top 客户列表(含亲密度 emoji 前缀)
* 示例:['💖 王先生', '💖 李女士', '💛 赵总']
* 💖 = 高亲密度,💛 = 中亲密度
* 示例:['💖 王先生', '🧡 李女士', '💛 赵总', '💙 新客户']
* 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5)
*/
topCustomers: string[]
@@ -922,7 +922,7 @@ interface CoachBoardItem {
| `avatarGradient` | string | 头像渐变色标识(`blue`/`green`/`pink`/`amber`/`violet`/`cyan` | 后端根据 ID 哈希分配 |
| `level` | string | 等级英文 key`star`/`senior`/`middle`/`junior` | `fdw_etl.v_dws_assistant_salary_calc.coach_level` |
| `skills` | `Array<{ text: string, cls: string }>` | 技能标签,`text` 为 emoji 或文字,`cls` 为前端样式类 | `biz.coach_skills` 或配置表 |
| `topCustomers` | `string[]` | Top 客户列表,含亲密度 emoji 前缀(💖/💛 | 后端按亲密度排序取 Top 3拼接 emoji + 姓名 |
| `topCustomers` | `string[]` | Top 客户列表,含亲密度 emoji 前缀(💖/🧡/💛/💙 | 后端按亲密度排序取 Top 3拼接 emoji + 姓名 |
##### perf 维度专属字段4 个)
@@ -978,7 +978,7 @@ interface CoachBoardItem {
| 助教费用拆分DWD-DOC 规则 2 | 工资计算中基础课 = `assistant_pd_money`,激励课 = `assistant_cx_money` |
| `levelClass` 前端计算 | 后端仅返回 `level`(英文 key前端通过 `LEVEL_CLASS` 映射生成样式类 |
| `perfGap` 后端计算 | 后端根据档位阈值计算差距描述字符串,已达标时不返回该字段 |
| `topCustomers` emoji 规则 | 💖 = 高亲密度(关系指数 ≥ 0.7),💛 = 中亲密度(关系指数 < 0.7 |
| `topCustomers` emoji 规则 | 💖(>8.5) / 🧡(>7) / 💛(>5) / 💙(≤5),基于 rs_display 值域 0-10 |
### BOARD-2: 客户看板
@@ -1856,10 +1856,12 @@ interface NoteItem {
---
## 八、对话模块
## 八、对话模块(已实现 ✅ — RNS1.4
> 数据源:`zqyy_app.chat_sessions`(对话会话)、`zqyy_app.chat_messages`(消息
> 路由前缀:`/api/xcx/chat`RNS1.4 从 `/api/ai/*` 迁移,旧路径已移除
> 数据源:`zqyy_app.biz.ai_conversations`(对话会话)、`zqyy_app.biz.ai_messages`(消息)
> 时间字段统一使用 `createdAt`camelCase替代前端 `timestamp` 和旧契约 `created_at`
> 所有端点需 JWTapproved 状态),使用 `require_approved()` 权限检查
### CHAT-1: 对话历史列表
@@ -1867,15 +1869,22 @@ interface NoteItem {
GET /api/xcx/chat/history?page=1&pageSize=20
```
#### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `page` | number (query) | 否 | 页码,默认 1 |
| `pageSize` | number (query) | 否 | 每页条数,默认 20最大 100 |
#### 响应结构
```typescript
interface ChatHistoryResponse {
items: Array<{
id: string // 对话 ID
title: string // 对话标题(新增,需求 5.8.2
customerName?: string // 关联客户姓名(可选
lastMessage: string // 最后一条消息摘要
id: number // 对话 ID(即 chatId
title: string // 对话标题(自定义 > 客户姓名 > 首条消息前20字
customerName?: string // 关联客户姓名(仅 contextType=customer 时有值
lastMessage?: string // 最后一条消息摘要
timestamp: string // 最后消息时间ISO 8601
unreadCount: number // 未读消息数
}>
@@ -1885,33 +1894,55 @@ interface ChatHistoryResponse {
}
```
### CHAT-2: 对话消息
#### 排序规则
> 支持 `customerId` 查询参数(需求 5.8.5):后端根据 `customerId` 自动查找或创建对话,返回对应的 `chatId` 和消息列表
`last_message_at` 倒序(最新对话在前)。
### CHAT-2a: 通过 chatId 查询消息
```
GET /api/xcx/chat/{chatId}/messages?page=1&pageSize=50
GET /api/xcx/chat/messages?customerId={customerId}&page=1&pageSize=50
```
#### 响应结构
#### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `chatId` | number (path) | 是 | 对话 ID |
| `page` | number (query) | 否 | 页码,默认 1 |
| `pageSize` | number (query) | 否 | 每页条数,默认 50最大 100 |
### CHAT-2b: 通过上下文查询消息
> 后端根据 `contextType` + `contextId` 自动查找或创建对话,返回对应的 `chatId` 和消息列表。
> 对话复用规则:`task` 入口同一 taskId 始终复用(无时限);`customer`/`coach` 入口 ≤ 3 天复用、> 3 天新建;`general` 入口始终新建。
```
GET /api/xcx/chat/messages?contextType={type}&contextId={id}&page=1&pageSize=50
```
#### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `contextType` | string (query) | 是 | 上下文类型:`task` / `customer` / `coach` / `general` |
| `contextId` | string (query) | 是 | 上下文 IDtaskId / customerId / coachIdgeneral 时为空) |
| `page` | number (query) | 否 | 页码,默认 1 |
| `pageSize` | number (query) | 否 | 每页条数,默认 50最大 100 |
#### 响应结构CHAT-2a / CHAT-2b 共用)
```typescript
interface ChatMessagesResponse {
chatId: string // 对话 IDcustomerId 入口时由后端返回
chatId: number // 对话 ID上下文入口时由后端返回,供后续发送消息使用
items: Array<{
id: string
id: number
role: 'user' | 'assistant'
content: string
createdAt: string // 统一时间字段名(需求 5.8.1ISO 8601
createdAt: string // 统一时间字段名ISO 8601
// 引用卡片(可选,需求 5.8.3
referenceCard?: {
type: 'customer' | 'record' // 引用类型
title: string // 卡片标题,如 '张伟 — 消费概览'
summary: string // 摘要文字
data: Record<string, string> // 键值对详情,如 { '近30天消费': '¥2,380', '到店次数': '8次' }
}
// 引用卡片(可选,AI 回复涉及特定客户时附带
referenceCard?: ReferenceCard
}>
total: number
page: number
@@ -1919,42 +1950,89 @@ interface ChatMessagesResponse {
}
```
### CHAT-3: 发送消息
#### 排序规则
消息按 `created_at` 正序(最早的消息在前)。
#### referenceCard 结构定义
```typescript
interface ReferenceCard {
type: 'customer' | 'record' // 引用类型
title: string // 卡片标题,如 '张伟 — 消费概览'
summary: string // 摘要文字,如 '余额 ¥5,200近30天消费 ¥2,380'
data: Record<string, string> // 键值对详情
}
```
示例:
```json
{
"type": "customer",
"title": "张伟 — 消费概览",
"summary": "余额 ¥5,200近30天消费 ¥2,380",
"data": {
"余额": "¥5,200",
"近30天消费": "¥2,380",
"到店次数": "8次",
"最近到店": "3天前"
}
}
```
> referenceCard 中金额使用 `items_sum` 口径DWD-DOC 强制规则 1会员信息通过 `member_id` JOIN `dim_member` 获取DWD-DOC 规则 DQ-6
### CHAT-3: 发送消息(同步回复)
```
POST /api/xcx/chat/{chatId}/messages
Content-Type: application/json
Body: { content: string }
```
#### 请求参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `chatId` | number (path) | 是 | 对话 ID归属验证不属于当前用户返回 403 |
| `content` | string (body) | 是 | 消息内容(不能为空) |
#### 响应结构
```typescript
interface SendMessageResponse {
userMessage: {
id: string
id: number
content: string
createdAt: string
createdAt: string // ISO 8601
}
aiReply: {
id: string
id: number
content: string
createdAt: string
createdAt: string // ISO 8601
}
}
```
### CHAT-4: SSE 流式端点(需求 5.8.4
#### AI 失败降级
> 用于 AI 流式回复,前端通过 SSE 接收逐 token 输出
AI 服务调用失败时,用户消息仍保存,`aiReply.content` 返回错误提示消息(如 `"抱歉AI 助手暂时无法回复,请稍后重试"`HTTP 状态码保持 200。
### CHAT-4: SSE 流式端点
> 用于 AI 流式回复,前端通过 SSE 接收逐 token 输出。
> chatId 归属验证在 SSE 流开始前完成,失败时返回普通 HTTP 403非 SSE
```
POST /api/xcx/chat/stream
Content-Type: application/json
Body: { chatId: string, content: string }
Body: { chatId: number, content: string }
Response: text/event-stream
```
#### SSE 事件格式
#### SSE 事件类型定义
三种事件类型:`message`(逐 token`done`(完成)、`error`(错误)。
```
event: message
@@ -1964,24 +2042,40 @@ event: message
data: {"token": "数据分析"}
event: done
data: {"messageId": "msg-xxx", "createdAt": "2026-03-05T14:30:00+08:00"}
data: {"messageId": 123, "createdAt": "2026-03-20T14:30:00+08:00"}
event: error
data: {"message": "AI 服务暂时不可用"}
```
- `event: message` — 逐 token 输出,`data.token` 为文本片段
- `event: done` — 流结束,`data.messageId` 为完整消息 ID
- `event: error` — 错误,`data.message` 为错误描述
| 事件类型 | data 结构 | 说明 |
|----------|----------|------|
| `message` | `{ "token": string }` | 逐 token 输出,`token` 为文本片段 |
| `done` | `{ "messageId": number, "createdAt": string }` | 流结束,返回完整消息 ID 和创建时间 |
| `error` | `{ "message": string }` | 错误,返回错误描述 |
> 注意SSE 端点的响应不经过 ResponseWrapperMiddleware 包装(`text/event-stream` 自动跳过
> 注意SSE 端点的响应不经过 ResponseWrapperMiddleware 包装(`text/event-stream` 自动跳过RNS1.0 已实现)。
#### 对话模块错误码
| HTTP 状态码 | 场景 | 响应示例 |
|:-----------:|------|---------|
| 403 | 用户未通过审核 | `{ "code": 403, "message": "用户未通过审核,无法访问此资源" }` |
| 403 | chatId 不属于当前用户 | `{ "code": 403, "message": "无权访问此对话" }` |
| 404 | 对话不存在 | `{ "code": 404, "message": "对话不存在" }` |
| 422 | 消息内容为空 | `{ "code": 422, "message": "消息内容不能为空" }` |
#### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `CHAT-1.items[].title` | string | 对话标题,新增字段 |
| `CHAT-1.items[].title` | string | 对话标题(自定义 > 客户姓名 > 首条消息前20字 |
| `CHAT-1.items[].timestamp` | string | 最后消息时间ISO 8601 |
| `CHAT-2.items[].createdAt` | string | 统一时间字段名,替代 `created_at``timestamp` |
| `CHAT-2.items[].referenceCard` | object? | 引用卡片,从其他页面跳转时附带的上下文信息 |
| `CHAT-2` `customerId` 参数 | query | 支持通过客户 ID 查找/创建对话 |
| `CHAT-4` | SSE | 流式端点,`text/event-stream` 格式,不经过响应包装 |
| `CHAT-2.items[].referenceCard` | ReferenceCard? | 引用卡片AI 回复涉及客户时附带结构化上下文数据 |
| `CHAT-2b` `contextType` 参数 | query | 上下文类型:`task` / `customer` / `coach` / `general` |
| `CHAT-2b` `contextId` 参数 | query | 上下文 IDtaskId / customerId / coachId |
| `CHAT-4` SSE | text/event-stream | 三种事件:`message`token/ `done`(完成)/ `error`(错误) |
---
@@ -2017,6 +2111,6 @@ Response: {
| board-coach | BOARD-1, CONFIG-1 | P2 |
| board-customer | BOARD-2 | P2 |
| board-finance | BOARD-3 | P2 |
| chat-history | CHAT-1 | P2 |
| chat | CHAT-2, CHAT-3 | P2 |
| chat-history | CHAT-1 | ✅ 后端已实现 |
| chat | CHAT-2a, CHAT-2b, CHAT-3, CHAT-4 | ✅ 后端已实现 |
| my-profile | AUTH-3 | ✅ 已实现(读 globalData |