# 品牌药品推荐 - 后端接口分析与方案（已完成实施）

> **状态：已实施** (2026-07-29)
> 详见 HANDOVER.md 第十六章节。

## 一、需求理解

### 核心流程
```
用户提问 → RAG检索 → LLM生成回答(SSE流式) → 从sources中提取药名 → 
关键词匹配(异步) → 返回匹配的品牌推荐 → 前端展示在回答末尾
```

### 举例
- AI回答中"参考原文"区域出现「布洛芬缓释胶囊」→ 匹配关键词"布洛芬" → 推荐"芬必得布洛芬缓释胶囊"
- AI回答中「感冒灵颗粒」→ 匹配"感冒灵" → 推荐"999感冒灵颗粒"

---

## 二、匹配逻辑分析

### 匹配对象：`buildSources` 返回的 sources 列表

当前 `chatStream` 尾部 `tailFlux` 中的 sources 结构（来自 buildSources line 720+）：

```json
{
  "drug_id": "xxx",
  "name": "布洛芬缓释胶囊",   // ← 匹配目标
  "section": "用法与用量",
  "category": "化学药品",
  "source": "2025版药典二部",
  "excerpt": "..."
}
```

### 匹配规则（按之前的需求）

| 规则 | 说明 |
|---|---|
| 匹配方式 | `sources[i].name` **包含**关键词（忽略大小写） |
| 示例 | 标题"布洛芬缓释胶囊"包含关键词"布洛芬" → 命中 |
| 同一标题多命中 | 去重展示所有匹配的品牌 |
| 无命中 | 不展示推荐区域，不影响正常回答 |
| 时机 | AI回答生成后，meta事件发送前（不增加用户可感知延迟） |

### 在 SSE 流中的注入位置

```
现有流:  ...[token1] [token2] ... [tokenN] → [meta: {sources, intent, cid}] → 完成
改造后: ...[token1] [token2] ... [tokenN] → [brand_recommend: {...}] → [meta: {...}] → 完成
```

新增 SSE 事件类型 `brand_recommend`，携带匹配结果。

---

## 三、需新增的后端内容

### 3.1 数据库表：`brand_recommend_rules`

```sql
CREATE TABLE brand_recommend_rules (
    id SERIAL PRIMARY KEY,
    keyword VARCHAR(100) NOT NULL UNIQUE,      -- 匹配关键词（药名）
    brand_name VARCHAR(200) NOT NULL,          -- 推荐品牌药品名称（必填）
    description VARCHAR(500),                  -- 推荐品牌药品说明（可选）
    is_active BOOLEAN NOT NULL DEFAULT TRUE,   -- 启用/停用开关
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);
```

### 3.2 JPA 实体 + Repository

- `BrandRecommendRule.java` — entity
- `BrandRecommendRuleRepository.java` — 查询启用的规则

### 3.3 品牌推荐 Service：`BrandRecommendService`

```java
@Service
public class BrandRecommendService {
    // 核心方法：根据 sources 列表返回匹配的品牌推荐
    public List<BrandMatchResult> match(List<Map<String, Object>> sources) {
        // 1. 从 sources 提取所有 drug name
        // 2. 查询所有 is_active=true 的规则
        // 3. 对每个 name 做包含匹配（忽略大小写）
        // 4. 去重，返回匹配结果
    }
}
```

### 3.4 管理端 CRUD 接口（AdminKnowledgeController 新增）

| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/api/v1/admin/brand-recommend-rules` | 列表（分页+搜索） |
| `POST` | `/api/v1/admin/brand-recommend-rules` | 新增 |
| `PUT` | `/api/v1/admin/brand-recommend-rules/{id}` | 修改 |
| `DELETE` | `/api/v1/admin/brand-recommend-rules/{id}` | 删除 |
| `POST` | `/api/v1/admin/brand-recommend-rules/import` | Excel 导入 |
| `GET` | `/api/v1/admin/brand-recommend-rules/export` | Excel 导出 |

### 3.5 ChatController 改造

在 `chatStream` 的 `tailFlux` 中，LLM 流结束后、meta 事件前，插入品牌推荐：

```java
Flux<ServerSentEvent<String>> tailFlux = Flux.defer(() -> {
    List<Map<String, Object>> sources = buildSources(docs);
    // 新增：品牌推荐匹配
    List<BrandMatchResult> brandRecs = brandRecommendService.match(sources);
    // 先发 brand_recommend 事件，再发 meta
    return Flux.concat(
        buildBrandRecommendEvent(brandRecs),
        buildMetaEvent(sources, intent, cid)
    );
});
```

---

## 四、需要确认的问题

### Q1：匹配范围
需求原文说匹配对象为「参考原文标题」，即 `sources[].name`。但你说"回答了感冒灵颗粒"—这是匹配 AI 回答正文还是 sources？

- **方案A（推荐）**：只匹配 `sources[].name`（参考原文标题，已结构化）
- **方案B**：同时匹配 AI 回答正文全文（性能开销大，易误匹配）

### Q2：品牌推荐 SSE 事件格式
前端期望的 `brand_recommend` 事件数据格式是什么？建议：

```json
{
  "event": "brand_recommend",
  "data": {
    "recommendations": [
      {"brand_name": "芬必得布洛芬缓释胶囊", "matched_keyword": "布洛芬", "description": "缓解疼痛"},
      {"brand_name": "999感冒灵颗粒", "matched_keyword": "感冒灵", "description": null}
    ]
  }
}
```

### Q3：存量历史对话
需求说"用户翻看历史对话时，历史回答中的品牌推荐正常展示，不因后台规则变更而丢失"。这意味着品牌推荐结果需要**持久化存储**到 messages 表中，而不是每次渲染时重新匹配。

是否在 `messages` 表加一个 `brand_recommendations` JSONB 字段，保存当时匹配的品牌推荐结果？

### Q4：品牌推荐表创建
确认是否现在创建 `brand_recommend_rules` 表和相关 Entity/Repository/Service，还是先只做方案分析？
