# 考研数学教辅 PDF MVP - 技术方案

> **文档状态**：草稿 v1 · 待研发评审
> **PM**：魏晨 · **生成日期**：2026-05-13
> **关联 PRD**：[20260507 考研数学丨支持用户上传教辅PDF MVP](https://jcngysah94q2.feishu.cn/docx/OeeJdziWlor4Ewxm3xUc0BmQnDg)
> **目标上线**：2026-05-20 (内测 1 本免费配额版)

---

## 0. 文档约定

- 本文档只回答"怎么实现"；产品决策与验收口径以 PRD 为准
- 文中所有阈值（5% 命中比例、单题成本上限、等待预算等）**直接引用** PRD 6.4 / 7.4，不再复述
- ⚠️ 标记 = **待核实项**，研发评审时需要补充信息或确认
- 假设默认走扇贝标准技术栈：**Coast (Python 3.11 + Flask + Sea gRPC) + MySQL + Redis + OSS(阿里云) + Kafka 不引入**

---

## 1. 方案概览

### 1.1 服务拓扑

```mermaid
flowchart LR
    Client[App 客户端] -->|HTTP| Upload[Upload Service<br/>三道闸 + 分片 + 哈希]
    Upload --> OSS[(OSS 对象存储)]
    Upload -->|入队| TaskQueue[(processing_task 表<br/>MySQL + worker poll)]
    TaskQueue --> Pipeline[Processing Pipeline<br/>节点0 → 识别 → 节点1 → 抽题]
    Pipeline -->|调外部| AuditSvc[审核服务<br/>4 个 code]
    Pipeline -->|调外部| MinerU[MinerU 识别服务]
    Pipeline -->|调外部| LLMRouter[LLM Router<br/>主力/升级/兜底]
    Pipeline -->|写| Textbook[Textbook Domain<br/>book/chapter/question]
    Client -->|HTTP| Textbook
    Client -->|HTTP| Explanation[Explanation Service]
    Explanation -->|L1/L2 缓存| Redis[(Redis)]
    Explanation -->|未命中| LLMRouter
    Explanation -->|审核| AuditSvc
    Textbook --> DB[(MySQL)]
    Explanation --> DB
```

### 1.2 关键决策一览

| 决策项 | 选择 | 理由 |
|---|---|---|
| 异步任务编排 | **MySQL 任务表 + worker poll**（不引入 Kafka/RabbitMQ） | MVP 流量小、可观测、重入简单 |
| 题库存储模式 | **共享题库 + user_book 引用** | 节省空间、L1 解析复用前提、广播命中天然实现 |
| 文件级缓存 key | **PDF SHA-256** | 工业标准、抗碰撞、与 OSS 通用 |
| 题目级缓存 key（L1） | **正文规范化指纹 v1**（去空白+标点统一+小写+sha256） | MVP 简版，hash_version 字段留升级位 |
| 审核服务接入 | **4 个独立 code**（type / book / item / explanation） | 阈值独立可调；策略不互相影响 |
| 节点 0 实现 | 规则前置驳回 + LLM 全量判定（Doubao-Seed-1.6） | 规则只敢"驳回"，LLM 全量保准确率 |
| LLM Router | **主力 / 升级 / 兜底三档矩阵** | PRD 7.4 锁定档位上限 |
| 客户端等待 | 书加工 → APNs/FCM 推送 + 拉取；解析等待 → HTTP 长轮询（指数退避 1→2→4s） | 不引 WebSocket |
| 节点 2/3 模式 | **先看后审 + 异步审核结果广播 user_book** | 对齐 PRD 6.4 |
| 节点 3 命中处理 | DB + L1 改占位、**不打 blocked 标记**；下次同题再走 LLM | 对齐 PRD 6.4 关键约定 |
| 配额扣减 | **占用名额（加工中）+ 扣免费配额（已就绪）** 两步 | 对齐 PRD 6.2 + 6.3 |

### 1.3 ⚠️ 待核实清单（先标记，研发评审时确认）

1. ⚠️ **Coast 是否有现成 async-task 模块**？若有则复用，否则按 § 5 实现独立任务表
2. ⚠️ **MinerU 是已部署服务还是要新接**？接口形态（HTTP/gRPC）/ 限速 / 1000 页 PDF 平均耗时与成本
3. ⚠️ **OSS 选型**：阿里云 OSS 还是 S3 兼容？分片上传 SDK / Pre-signed URL 是否封装好
4. ⚠️ **精选库 200 本现状**：当前是已入库还是待入库？若已入库，哈希算法是否与本方案对齐
5. ⚠️ **审核服务 4 个新 code 申请流程**：找谁申请、上线前能否就绪
6. ⚠️ **APNs/FCM 推送通道**：扇贝 App 是否已有统一推送 SDK
7. ⚠️ **LLM 调用代理 / Key**：内部 LLM 网关是否已支持 Doubao-Seed-1.6 / DeepSeek-R1 / GPT-4o-mini

---

## 2. 领域模型

### 2.1 ER 图

```mermaid
erDiagram
    book ||--o{ chapter : "1:N"
    chapter ||--o{ question : "1:N"
    question ||--o{ explanation : "1:N (version 多版本)"
    user ||--o{ user_book : "1:N (我有哪些教辅)"
    user_book }o--|| book : "N:1 (引用全局 book)"
    user ||--o{ wrongbook_item : "1:N"
    wrongbook_item }o--|| question : "N:1"
    user_book ||--o{ user_book_item_status : "1:N (per-user item 状态)"
    user_book_item_status }o--|| question : "N:1"
    book ||--o{ processing_task : "1:N (加工历史)"
    book {
        bigint id PK
        string file_hash UK "PDF SHA-256"
        string title
        int chapter_count
        int question_count
        string source "featured | natural"
        string status "processing | ready | rejected"
        json reject_info "驳回原因 + 节点编号"
        timestamp created_at
        timestamp ready_at
    }
    chapter {
        bigint id PK
        bigint book_id FK
        bigint parent_id "章节多级"
        string title
        int order_index
        string visibility "visible | hidden"
        string hidden_reason
    }
    question {
        bigint id PK
        bigint chapter_id FK
        bigint book_id FK
        text stem "题干"
        string normalized_hash "L1 解析复用 key"
        int hash_version "v1"
        string visibility "visible | hidden"
        string hidden_reason
        json knowledge_points
        int order_in_chapter
    }
    explanation {
        bigint id PK
        bigint question_id FK
        text content "JSON: 思路+步骤+知识点"
        string model_used "doubao-seed-1.6 等"
        string audit_status "approved | hidden"
        string source "ai_realtime | featured_prebuilt"
        timestamp created_at
        timestamp audited_at
    }
    user_book {
        bigint id PK
        bigint user_id FK
        bigint book_id FK
        timestamp created_at "命中或上传时刻"
        bool quota_consumed "true=已扣配额"
    }
    user_book_item_status {
        bigint id PK
        bigint user_book_id FK
        bigint question_id FK
        string per_user_state "read | unread"
        timestamp last_view_at
    }
    wrongbook_item {
        bigint id PK
        bigint user_id FK
        bigint book_id FK
        bigint question_id FK
        string source_type "textbook"
        string entry "single_question | explanation"
        timestamp created_at
    }
    processing_task {
        bigint id PK
        bigint book_id FK
        string stage "node0 | recognize | node1 | extract | node2 | done"
        string status "pending | running | succeed | failed"
        int attempt
        text last_error
        json stage_payload
        timestamp updated_at
    }
```

### 2.2 表设计要点

#### `book`（全局唯一）
- **唯一索引**：`file_hash`（PDF SHA-256）—— 上传时哈希查重的核心 key
- `source`: `featured`（精选库 200 本）/ `natural`（用户上传形成的自然缓存）
- `status` 状态机：`processing` → `ready` / `rejected`；rejected 时记录 `reject_info` 含节点编号 + 命中详情
- 命中即在 `user_book` 新建引用，不复制 `book / chapter / question`

#### `question`
- `normalized_hash`：题目正文规范化指纹（详见 § 3.2），L1 跨用户解析库的 key
- `hash_version`：留升级位（v1 = 简版，v2 可升级 LaTeX AST 归一化）
- `visibility = hidden`：节点 1 命中段 / 节点 2 命中题干，**全局所有引用者同时不可见**

#### `user_book_item_status`
- 仅存"per-user 用户态"（已看过 / 未看过等），**不存内容**
- item 内容状态 (visible/hidden) 在 `question.visibility`，对所有引用者一致

#### `wrongbook_item`
- 用户私有，不随 user_book 引用继承（对齐 PRD 8 章）

---

## 3. 核心算法

### 3.1 PDF 文件级指纹

- 算法：`SHA-256(file_bytes)`，输出 64 字符 hex
- 用途：①命中已有 `book` 时跳过加工；②防止用户上传伪造文件绕审
- 客户端在分片上传前预计算，后端 OSS upload complete 后用 OSS SHA 元信息复算并对比，**双方一致才入库**（防中间人篡改）

### 3.2 题目正文规范化指纹 v1（L1 解析复用 key）

```python
def normalize_question_text_v1(stem: str) -> str:
    # 1. 删空白
    s = re.sub(r"\s+", "", stem)
    # 2. 中英文标点统一为半角
    s = s.translate(STR_PUNCT_MAP)  # ，→, ；→; （→( 等
    # 3. 转小写（英文字母）
    s = s.lower()
    # 4. LaTeX 简单归一化（v1 只做最常见）:
    s = s.replace("\\dfrac", "\\frac").replace("\\tfrac", "\\frac")
    s = re.sub(r"\\left\(|\\right\)", "", s)  # 删掉 \left \right
    # 5. SHA-256
    return hashlib.sha256(s.encode()).hexdigest()
```

- `hash_version = "v1"` 写入 `question.hash_version` 字段
- 后续升级到 v2（LaTeX AST 归一化）时双 hash 并行写入，灰度查询

### 3.3 节点 1 命中比例计算

```python
hit_ratio = hit_paragraph_count / total_paragraph_count
if hit_ratio >= 0.05:  # PRD 6.4 锁定 5%
    book.status = "rejected"
    book.reject_info = {"node": 1, "ui": "UI11", "hit_ratio": hit_ratio, ...}
```

- `total_paragraph_count` = MinerU 识别后的段落总数（含被审核的图片，图片按 1 段算）
- 阈值 5% 写入配置表 `audit_config`，运营可调

---

## 4. 服务边界与模块

### 4.1 Upload Service

| API | 方法 | 用途 |
|---|---|---|
| `POST /api/textbook/upload/init` | 客户端发起 | 三道闸校验 + 返回 OSS 上传凭证 |
| `POST /api/textbook/upload/complete` | OSS 上传完成回调 | 计算/校验哈希、查重、入队加工 |
| `GET /api/textbook/upload/{task_id}/status` | 轮询任务进度 | 返回 stage + status |

**三道闸 (PRD 6.2)**：
- 实名：调内部 user 服务查 `phone_bound`
- ugc_switch：查 ConfigCenter 的 `kaoyan.math.textbook.ugc_switch` (整数 0/1/2)
- 单用户配额：查 `user_book WHERE user_id = ? AND book.status IN ('processing', 'ready')` 数量

### 4.2 Processing Pipeline (异步任务编排)

详见 § 5。

### 4.3 Textbook Domain Service

| API | 方法 | 用途 |
|---|---|---|
| `GET /api/textbook/list` | 我的教辅列表 | 返回 user_book + 状态聚合（已就绪/加工中/失败）|
| `GET /api/textbook/{book_id}/chapters` | 章节目录 | 树形结构 + visibility |
| `GET /api/textbook/{book_id}/questions/{q_id}` | 单题详情 | 含 visibility + 申诉状态 |
| `GET /api/textbook/{book_id}/questions/{q_id}/neighbors` | 上一题/下一题导航 | 跳过 hidden |
| `POST /api/textbook/{book_id}/wrongbook` | 加错题本 | 记 user_id + question_id |

权限校验：访问 `book_id` 必须命中 `user_book` 的 `(user_id, book_id)` 行。

### 4.4 Explanation Service

| API | 方法 | 用途 |
|---|---|---|
| `POST /api/explanation/generate` | 用户点解析 | 查 L1/L2 → 命中即返回；未命中入队 LLM 任务 + 返回 task_id |
| `GET /api/explanation/{task_id}` | 轮询解析结果 | 三态：generating / ready / blocked |

详见 § 7 LLM Router + 缓存。

---

## 5. 异步任务编排

### 5.1 任务表设计 (`processing_task`)

详见 § 2.1 ER 图。

**stage 状态机**：

```mermaid
stateDiagram-v2
    [*] --> node0: 上传完成
    node0 --> recognize: 通过
    node0 --> rejected_node0: 不通过
    recognize --> node1: 识别完成
    node1 --> extract: 命中比例<5%
    node1 --> rejected_node1: 命中比例≥5%
    extract --> done: AI 抽题入库
    done --> [*]: book.status=ready
    rejected_node0 --> [*]: book.status=rejected
    rejected_node1 --> [*]: book.status=rejected

    note right of extract: 节点 2 异步审核独立运行<br/>不阻塞主链路
```

### 5.2 Worker 调度

- 一个 worker 进程 poll `processing_task WHERE status='pending' ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED`
- 每 stage 完成后更新 `status='succeed' / 'failed'`、推进下一 stage 或终止
- 失败重试：`attempt < 3` 时回 `status='pending'`；超出则 `status='failed'` 并通知运营
- 幂等：每 stage 用 `book_id + stage` 唯一索引保证不重复执行

### 5.3 节点 2 异步审核子任务

- 独立任务表 `node2_audit_task`，按 `question_id` 粒度
- 不进 `processing_task` 主链路；节点 2 审核完成与否不影响 book ready 状态
- 命中后通过 `question.visibility = 'hidden'` 一处修改，所有引用者下次查询天然反映

---

## 6. 内容审核接入

### 6.1 4 个独立审核 code

| 节点 | code | 触发方 | 同/异步 |
|---|---|---|---|
| 节点 0 | `pg_textbook_type` | Upload pipeline node0 stage | 同步阻塞 |
| 节点 1 | `pg_textbook_paragraph` | Pipeline node1 stage（逐段并发批审）| 同步阻塞 |
| 节点 2 | `pg_textbook_item` | LLM 抽题后异步入队 | 异步放行 |
| 节点 3 | `pg_textbook_explanation` | LLM 解析生成后异步入队 | 异步放行 |

⚠️ 待核实：上述 code 是新申请还是复用真题宝已有的；找运营/审核运维确认。

### 6.2 节点 0 实现（规则 + LLM 双层）

```python
def node0_content_type_check(pdf_text_first_n_pages, pdf_images_sample) -> dict:
    # Layer 1: 规则只敢驳回（高置信"明显不是数学"）
    if math_symbol_density(text) < 0.01 and digit_density(text) < 0.05:
        return {"verdict": "rejected", "reason": "几乎无数学符号"}
    if hit_blacklist_keywords(text, ["小说", "合同", "婚姻", "刑事", ...]):
        return {"verdict": "rejected", "reason": "命中非数学白名单词"}

    # Layer 2: LLM 全量判定（默认路径）
    llm_resp = call_llm(
        model="doubao-seed-1.6",
        prompt=NODE0_CLASSIFY_PROMPT,
        input={"text": text[:5000], "image_count": len(images)},
    )
    return llm_resp  # {verdict, confidence, reason}
```

### 6.3 节点 1 并发批审

- MinerU 输出段落 + 图片各按 batch=50 并发送审
- 收集所有 `hit / not_hit` 结果，计算 `hit_ratio`
- 单段超时 / 异常按 `not_hit` 计入（保守放行，等节点 2 兜底）

---

## 7. AI 解析生成

### 7.1 LLM Router 三档

| 档位 | 主力候选模型（v1 选型） | 升级触发 | 单题成本上限 (PRD 7.4) |
|---|---|---|---|
| 主力 | `doubao-seed-1.6` 关思考关 或 `deepseek-v3.2` | 默认 | ≤ ¥0.02 |
| 升级 | `deepseek-r1` 或 `doubao-1.5-thinking-pro` | LLM-as-judge 评分 < 0.7 或 输出长度 < 200 字 | ≤ ¥0.05 |
| 兜底 | `gpt-4o-mini`（代理） | 主力 + 升级均失败 | ≤ ¥0.05 |

### 7.2 提示词工程

#### System Prompt 模板

```
你是考研数学一对一辅导老师。输出风格：
- 解析思路口语化（"为什么这么想"）
- 不跳步推导（每步独立成行）
- 关联考研大纲知识点
- 公式一律 LaTeX
- 禁止"显然/易得/留作练习"
```

#### Task Prompt + 输出 Schema

```json
{
  "type": "object",
  "required": ["思路", "步骤", "知识点"],
  "properties": {
    "思路": {"type": "string"},
    "步骤": {"type": "array", "items": {
      "type": "object",
      "properties": {"说明": {"type": "string"}, "公式": {"type": "string"}}
    }},
    "知识点": {"type": "array", "items": {
      "type": "object",
      "properties": {"名称": {"type": "string"}, "公式": {"type": "string"}}
    }}
  }
}
```

#### Few-shot

内置 3-5 道考研典型题人工示范，覆盖高数 / 线代 / 概率，独立 jinja2 模板维护。

### 7.3 LLM-as-judge 质量守门

- 用 `gpt-4o`（贵模型）做评分，维度三段齐备 / 公式合法 / 推导可读
- 评分 < 0.7 触发"升级档"重生成
- 抽样 50 题 / 周人工评测，校准 judge 模型

### 7.4 L1 / L2 缓存策略

```
用户点解析 → 查 question.normalized_hash
            ↓
        L1: SELECT explanation FROM explanation
            WHERE normalized_hash=? AND audit_status='approved'
            ORDER BY created_at DESC LIMIT 1
            ↓
        命中 → 直接返回
            ↓
        L2: 仅当 book.source='featured'，查 explanation WHERE
            question_id=? AND source='featured_prebuilt'
            ↓
        命中 → 直接返回
            ↓
        未命中 → 入队 LLM 任务，返回 task_id 给客户端轮询
            ↓
        LLM 生成完成 + 节点 3 审核通过 → 写 explanation 表 audit_status='approved'
            ↓
        节点 3 命中 → 仅改 explanation.audit_status='hidden'，
                     不写入 normalized_hash 作为 blocked 标记
```

**关键约束**：节点 3 命中后**不阻止后续同 question 再次走 LLM**——只是本次的 explanation 记录被标 hidden，下次查询 L1 时不命中（因为 audit_status≠'approved'）→ 自然走新一轮 LLM。对齐 PRD 6.4 关键约定。

### 7.5 A/B 沙盒

- `llm_experiment` 配置表：5% 流量分流到候选模型 × 提示词矩阵
- 不暴露前端；评分维度看 👍/👎 比例 + 单题成本拐点
- 周度复盘选最优组合

---

## 8. 接口设计（精选关键 API）

### 8.1 上传初始化

```http
POST /api/textbook/upload/init
Authorization: Bearer <user_token>
Content-Type: application/json

{
  "file_name": "武忠祥基础.pdf",
  "file_size": 47185920,
  "page_count": 320,
  "client_hash": "ab12cd34..."   // 客户端预计算 SHA-256
}

200 OK
{
  "task_id": "task_xyz",
  "oss_upload": {
    "endpoint": "...",
    "policy": "...",
    "signature": "...",
    "key": "ugc/textbook/2026/05/13/<task_id>.pdf"
  },
  "estimated_processing_seconds": 180   // 命中缓存时为 0
}
```

**三道闸 / 客户端粗筛失败**返回：

```http
403 Forbidden
{
  "error_code": "REALNAME_REQUIRED" / "UGC_SWITCH_OFF" / "QUOTA_EXCEEDED" / "FILE_TOO_LARGE" / "PAGE_TOO_MANY" / "FILE_ENCRYPTED",
  "ui_action": "show_toast" | "redirect_to_realname" | "redirect_to_quota"
}
```

### 8.2 解析生成

```http
POST /api/explanation/generate
{
  "book_id": "book_xyz",
  "question_id": "q_001"
}

200 OK
{
  "cache_hit": "l1",    // l1 | l2 | none
  "task_id": null,      // 命中缓存为 null，未命中返回轮询 task_id
  "explanation": {...}  // 命中时直接返回
}

GET /api/explanation/{task_id}
200 OK
{
  "status": "generating" | "ready" | "blocked",
  "explanation": {...},
  "blocked_info": { "ui": "UI14", "appeal_available": true }
}
```

### 8.3 错题本

```http
POST /api/textbook/{book_id}/wrongbook
{
  "question_id": "q_001",
  "entry": "single_question" | "explanation"
}

GET /api/textbook/{book_id}/wrongbook
{
  "items": [...],
  "total_count": 16,
  "grouped_by_date": {...}
}
```

完整接口清单建研发评审时补全（按 RESTful 风格扩展）。

---

## 9. 前端方案

### 9.1 复用边界

| 复用真题宝 | 新建 |
|---|---|
| 题干 LaTeX 渲染（KaTeX）| 我的教辅卡片（带状态 badge）|
| 解析三段式渲染组件 | 上传引导 + 客户端粗筛 Toast |
| 错题本卡片 | 加工中页（4 步阶段进度）|
| 章节目录卡片 | 占位卡（题/解析两种 × 申诉三态）|
| 单题导航条 | 上传额度 sheet |

### 9.2 路由 / 页面 → 原型 UI 编号

| 路由 | 页面 | UI 编号 |
|---|---|---|
| `/math` | 数学首页（含教辅 banner） | 01 |
| `/math/textbook` | 我的教辅 | 02 |
| `/math/textbook/processing/:taskId` | 加工中 | 03 |
| `/math/textbook/:bookId/chapters` | 章节目录 | 04 |
| `/math/textbook/:bookId/questions/:qId` | 单题 | 05 / 05b |
| `/math/textbook/:bookId/questions/:qId/explanation` | 解析 | 06 |
| `/math/textbook/:bookId/wrongbook` | 错题本 | 08 |

### 9.3 全局状态机

#### book 状态

```
processing → ready (item 立即可见，部分异步审核中)
         → rejected (UI10 / UI11 / UI12)
```

#### item 可见性（章节标题 / 题目）

```
visible / hidden
hidden 时显示占位卡 + 申诉三态按钮
```

#### 申诉三态（前后端共识）

```
A 未申诉  → 红色按钮可点
B 处理中  → 灰色不可点（预计 24h，无自动结案）
C-1 通过 → 入口消失、内容恢复
C-2 不通过 → 灰色划线不可点（终态）
```

### 9.4 等待协议

#### 书加工等待
- 客户端进 UI03 后 **不主动轮询**；离开后通过 **APNs/FCM 推送** 通知
- 用户回到 App 进入 UI02 时 `GET /api/textbook/list` 取最新状态

#### 解析生成等待
- HTTP 长轮询：`GET /api/explanation/{task_id}` 间隔 `1s → 2s → 4s → 8s`（最多 60s）
- 超时显示「网络异常重试」按钮

### 9.5 状态管理

- 复用真题宝的全局 store 方案（⚠️ 待核实：Redux / Zustand / Pinia 哪个）
- 新增 module：`textbookModule`（list / detail / processing / explanation / wrongbook）

---

## 10. 数据埋点接入

埋点字段定义见 [PRD 第 11 章](https://jcngysah94q2.feishu.cn/docx/OeeJdziWlor4Ewxm3xUc0BmQnDg#11)。

实现要点：
- 前端事件统一过神策 SDK
- 后端 `ugc_upload_complete` / `ugc_upload_fail` / `mathTextbook_explanationView.is_cache_hit` 由 Pipeline / Explanation Service 上报
- `cache_source` 字段值：`featured` / `natural` / `none`（对应 PRD 11.4）

---

## 11. 联调清单

### 11.1 前后端联调里程碑

| 阶段 | 联调内容 | 责任方 |
|---|---|---|
| W1 | 上传链路（三道闸 + OSS + 哈希） | 前后端 |
| W1 | 命中缓存路径 | 后端 |
| W2 | 加工中页轮询 + 推送 | 前后端 |
| W2 | 章节 / 单题 / 错题本接口 | 前后端 |
| W3 | 解析生成两级缓存 + 等待协议 | 前后端 |
| W3 | 占位卡 + 申诉三态 | 前后端 |
| W4 | 内容审核 4 节点端到端 | 后端 + 审核 |
| W4 | 埋点全量校验 | 全员 |

### 11.2 联调环境

- Dev：`kaoyan-math-dev.shanbay.com`
- Staging：`kaoyan-math-staging.shanbay.com`
- Prod：`kaoyan.shanbay.com`

⚠️ 待核实：扇贝标准联调环境名 / 域名规范。

### 11.3 审核接入前置依赖

- 4 个新审核 code 申请 ≥ T-7 天
- 节点 0 LLM 内部网关 Key ≥ T-5 天

---

## 12. 关键风险

| 风险 | 缓解 |
|---|---|
| MinerU 单本耗时长尾 → 用户长时间等待 | 推送通知降低焦虑；进度提示不展示百分比避免回测 |
| 节点 1 5% 阈值在某些边缘教辅误命中过高 | 阈值入 `audit_config` 表，运营可调；上线一周高频监控驳回率 |
| L1 缓存命中率达不到 60% PRD 目标 | 哈希算法 v1 偏保守；周度看命中率，必要时升级到 v2 LaTeX AST 归一化 |
| 节点 3 命中不写黑名单 → 反复烧 token | 单题 N 次连续命中后加临时黑名单（V2 加） |
| 自然缓存形成时 item 状态广播延迟 | 节点 2/3 命中走 message broadcast；前端进单题前重新拉一次最新 visibility |

---

## 附录 A · 数据库索引建议（草稿）

```sql
-- book
UNIQUE INDEX idx_book_hash (file_hash);
INDEX idx_book_status (status);

-- question
INDEX idx_question_chapter (chapter_id, order_in_chapter);
INDEX idx_question_book (book_id);
INDEX idx_question_normalized_hash (normalized_hash, hash_version);  -- L1 查询核心

-- explanation
INDEX idx_explanation_question (question_id, audit_status, created_at DESC);
INDEX idx_explanation_normalized (question_id, normalized_hash_ref);

-- user_book
UNIQUE INDEX idx_user_book (user_id, book_id);

-- processing_task
INDEX idx_task_status (status, created_at);
UNIQUE INDEX idx_task_book_stage (book_id, stage);
```

---

## 附录 B · 待研发评审拍板事项

1. ⚠️ Coast async-task 模块是否存在 / 是否复用 → 决定 § 5 方案
2. ⚠️ MinerU 接口形态与 SLA → 决定 § 4.2 调用方式 + § 5 超时配置
3. ⚠️ OSS 选型 + 分片上传 SDK → 决定 § 4.1 凭证生成方式
4. ⚠️ 精选库 200 本现状（已入库？哈希算法？解析预生成？）→ 决定首批上线时是否走 B-1 路径
5. ⚠️ 审核服务 4 个 code 申请流程 → 决定排期
6. ⚠️ 推送 SDK + LLM Router Key → 决定排期
7. 前端框架与状态管理（React / Vue / RN？Redux / Zustand？）→ 决定 § 9.5
8. 节点 0 LLM 选型（Doubao-Seed-1.6 还是 DeepSeek-V3.2？）→ 决定 § 6.2 实测成本

---

> 📌 本技术方案是 PRD v1.1 的实现答卷。研发评审通过后冻结作为开发 baseline；后续 PRD 变更需同步本文档。
