feat: add knowledge management functionality with CRUD operations and database schema

- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations.
- Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta.
- Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
This commit is contained in:
2026-04-05 17:14:13 +08:00
parent 134dd063c3
commit 446f865cdf
62 changed files with 9503 additions and 2664 deletions
-348
View File
@@ -1,348 +0,0 @@
# GEO 平台技术架构严格审查报告
> 基于 [geo-platform-ops-admin-tech-architecture-v1.md](file:///d:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md) 对照 `docs/refer` 中 8 个功能截图及两份 PRD 文档进行逐功能审查。
---
## 审查总结
| 评估维度 | 结果 |
| --- | --- |
| 功能覆盖度 | ⚠️ 存在多处缺口 |
| 后端模块划分 | ✅ 基本合理,部分缺失 |
| API 路由设计 | ⚠️ 多条功能链路缺少对应路由 |
| 数据模型覆盖 | ❌ 未提供数据库 Schema |
| 异步链路设计 | ⚠️ 部分 Worker 职责不完整 |
| 前端路由覆盖 | ⚠️ 缺少部分关键页面路由 |
---
## 一、逐功能审查
### 1. 工作台 (工作台.png)
**截图功能点:**
- 模板类型卡片入口(Top X / 产品评测 / 研究报告 / 持续扩展中)
- 右侧数据总览(生成数、发布数、品牌数、媒体平台数)
- 最近生成列表(标题、模板类型、生成状态、发布状态、字数、操作按钮)
- 左下角套餐额度区(免费版、升级入口、额度进度条、重置时间)
- 操作能力:发布、优化、预览、复制、删除
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 `/workspace` | ✅ | 已定义 |
| API `/api/tenant/workspace/*` | ✅ | 已定义 |
| 数据总览统计接口 | ⚠️ | 未在 API 中明确统计聚合端点 |
| 套餐额度展示接口 | ⚠️ | `/api/tenant/account/*` 未明确额度查询子接口 |
| 模板类型配置管理 | ❌ | 技术架构中未定义模板类型配置的存储与下发接口 |
> [!WARNING]
> 工作台首屏需聚合多个数据源(文章统计、品牌数、媒体数、额度),架构中未设计聚合 BFF 层或专用 Dashboard API,可能导致首屏请求过多。
---
### 2. 模板创作 (模板创作.png)
**截图功能点:**
- 筛选区:模板类型、发布状态、生成时间、生成状态、搜索文章
- 操作按钮:批量生成文章、选择模板创作
- 列表:文章标题、模板类型、生成状态、发布状态、字数、操作
- 单次生成、批量任务
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 | ⚠️ | `admin-web` 路由中有 `/articles` 但缺少模板创作专属路由 |
| API `/api/tenant/articles/*` | ✅ | 已定义 |
| 模板定义管理 | ❌ | 后端缺少 `tenant/template` 模块定义 |
| 批量生成任务 | ✅ | `worker-generate` 已定义 |
| 模板输入字段配置 | ❌ | 未定义模板 schema 的存储与解析 |
> [!IMPORTANT]
> 模板创作是核心链路之一,但技术架构中对"模板"概念的后端支撑严重不足。`internal/tenant` 下未见 `template` 子模块;后端模块职责 §5.5.4 仅列出 `article / brand / knowledge`,完全未提及模板管理。
---
### 3. 自定义生成 (自定义生成.png)
**截图功能点:**
- 快捷入口:即时生成内容、定时计划生成
- Tabs:生成文章、即时任务、定时任务、Prompt 规则
- 筛选区:Prompt、定时任务、发布状态、标题、生成状态、生成时间
- 列表:文章标题、规则名称、Prompt、发布平台、生成状态、发布状态、字数
- Prompt 规则管理
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由(自定义生成) | ⚠️ | `/articles` 覆盖但未区分模板创作与自定义生成的路由 |
| Prompt 规则 CRUD | ❌ | 后端完全未定义 `tenant/prompt-rule` 模块 |
| 定时任务管理 | ❌ | 后端未定义定时调度器(Scheduler)组件 |
| 即时任务与定时任务差异化 | ❌ | `worker-generate` 仅定义"处理文章生成任务",未区分即时/定时 |
| 定时任务表 | ❌ | 数据库设计中未提及 `schedule_task` 表 |
> [!CAUTION]
> **定时任务是自定义生成的核心子功能**,但技术架构中完全缺少:
> 1. 定时调度组件(如 cron scheduler 或 delayed message
> 2. 定时任务 CRUD 接口
> 3. Prompt 规则管理接口
>
> 这是**重大架构缺失**。
---
### 4. 文章优化 (文章优化.png)
**截图功能点:**
- 操作入口:导入已生成文章、新建文章优化、未完成优化任务
- 筛选区:发布状态、生成时间、生成状态、搜索文章
- 列表:作品标题/来源、字数、发布状态、最后修改时间、操作
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 | ⚠️ | `/articles` 中未区分文章优化页 |
| 优化任务管理模块 | ❌ | 后端未定义 `tenant/optimization` 子模块 |
| 优化任务 Worker | ❌ | `worker-generate` 未声明支持优化任务类型 |
| 优化版本管理 | ❌ | 未定义 `article_version` 的存储与接口 |
> [!WARNING]
> 文章优化涉及"导入 → 优化 → 版本保存"链路,架构中完全未涉及。PRD 中定义了 `OptimizationTask` 数据对象,但技术架构未对应出后端模块和接口。
---
### 5. 媒体管理 (媒体管理.png)
**截图功能点:**
- 绑定流程引导(安装插件 → 登录 → 授权同步)
- 平台分类 Tabs:UGC 媒体平台、企业自建站点、第三方新闻源
- 搜索与筛选(全部/已连接/未连接)
- 平台卡片:名称、类型、绑定状态、账号昵称、ID、运行状态
- 操作:去绑定、发布内容
- 支持 13+ 平台(知乎、头条号、百家号、搜狐号、企鹅号、网易号、简书、bilibili、稀土掘金、什么值得买、公众号、中关村在线等)
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 `/media` | ✅ | 已定义 |
| API `/api/tenant/media/*` | ✅ | 已定义 |
| 浏览器插件交互接口 | ⚠️ | 仅有 `/api/callback/publish/*`,缺少绑定回调接口 |
| 平台 Adapter 管理 | ✅ | `platform-api` 中有平台接入管理 |
| 平台分类模型 | ❌ | 未定义 UGC / 自建站 / 新闻源的分类架构 |
| 发布链路中的插件通信协议 | ❌ | 架构中未定义插件 ↔ 后端的通信协议和数据格式 |
> [!WARNING]
> 媒体管理涉及浏览器插件的深度交互,但技术架构中未定义:
> - 绑定回调接口(`/api/callback/bind/*`
> - 插件与后端的 WebSocket 或轮询通信机制
> - 13+ 平台的 Adapter 注册与路由分发策略
---
### 6. 品牌词库 (品牌词库.png)
**截图功能点:**
- 品牌卡片区(品牌名称、新建品牌)
- Tab:关键词、竞品库
- 左侧关键词列表(关键词名称、数量、新建)
- 右侧问题集列表(问题内容、创建时间、操作:数据/删除)
- 新建问题集
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 `/brands` | ✅ | 已定义 |
| API `/api/tenant/brands/*` | ✅ | 已定义 |
| 品牌-关键词-问题集层级关系 | ⚠️ | 后端模块 §5.5.4 只笼统提到"品牌资料",未展开数据层级 |
| 竞品库管理 | ❌ | 后端未定义 `competitor` 子模块 |
| 问题集"数据"操作(跳转数据详情) | ❌ | 架构中未定义品牌词→数据追踪的关联查询接口 |
---
### 7. 知识库 (知识库.png)
**截图功能点:**
- 新建内容按钮
- 左侧分组树(默认分组、新建分组)
- 右侧列表:名称、类型、大小、AI 学习状态、上传时间、操作
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 `/knowledge` | ✅ | 已定义 |
| API `/api/tenant/knowledge/*` | ✅ | 已定义 |
| `worker-knowledge` | ✅ | 已定义,负责解析、Embedding、Qdrant 写入 |
| 分组管理接口 | ⚠️ | 未明确分组 CRUD 的子路由 |
| 多来源类型(文档/链接/文本) | ❌ | 架构中未定义不同来源类型的解析策略差异 |
| 对象存储集成 | ✅ | 已定义用于存储原始文档 |
> 知识库是架构覆盖度**最高**的模块。
---
### 8. 数据管理 (数据管理.png)
**截图功能点:**
- 数据概览卡片:总发文数、总引用数、文章引用率、品牌可见率
- 品牌排名数据区(品牌选择、关键词选择、模型选择)
- 指标 Tabs:提及率、首位提及率、首选推荐率、正面提及率
- 趋势图(按日期折线)
- 右侧高频问题列表
- 引用排行榜
**架构覆盖情况:**
| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 前端路由 `/tracking` | ✅ | 已定义 |
| API `/api/tenant/tracking/*` | ✅ | 已定义 |
| `worker-analytics` | ✅ | 已定义,负责统计聚合 |
| 数据采集机制 | ❌ | 架构中未定义 GEO 指标(提及率、引用率等)的采集方式 |
| 品牌×关键词×模型 三维筛选 | ❌ | 后端未定义这种复合维度的查询接口设计 |
| 高频问题聚合逻辑 | ❌ | 未定义高频问题的来源和聚合策略 |
| 引用排行榜数据来源 | ❌ | 未定义引用数据的采集或模拟机制 |
> [!CAUTION]
> 数据管理/追踪是 UI 复杂度最高的功能之一,但技术架构中几乎**仅有一行**提到 `worker-analytics` 负责"统计聚合、分析快照、缓存刷新"。缺少:
> 1. GEO 指标采集服务或爬虫组件的架构定义
> 2. `AnalyticsSnapshot`、`MentionRecord`、`CitationRecord` 的存储方案
> 3. 品牌×关键词×模型的多维查询 API 设计
> 4. 高频问题的采集与聚合方案
---
## 二、跨功能架构问题
### 问题 1:数据库 Schema 完全缺失
> [!CAUTION]
> 技术架构文档 774 行,**没有任何一张数据库表定义**。PRD 中定义了 20+ 数据对象(User、Brand、Article、GenerationTask、ScheduleTask、PlatformAccount、KnowledgeItem 等),但技术架构中仅在 §7.1 笼统列出 PostgreSQL 负责的类别,未给出任何表结构、索引策略或分区方案。
### 问题 2:定时调度器缺失
技术架构定义了 4 类 Worker:
- `worker-generate`
- `worker-knowledge`
- `worker-analytics`
- `worker-publish-callback`
但**没有定义定时调度器**。自定义生成中的"定时计划生成"功能需要 cron 调度能力,但架构中既没有独立的 `cmd/scheduler` 二进制,也没有说明使用 RabbitMQ Delayed Message 或其他延迟队列机制。
### 问题 3:Prompt 规则管理无后端支撑
自定义生成截图中有独立的 "Prompt 规则" TabPRD 中也明确定义了 Prompt 规则的 CRUD。但技术架构中:
- `internal/tenant` 下无 `prompt` 子模块
- API 路由 `/api/tenant/articles/*` 中未体现规则管理
- 平台侧 `platform/content` 提到了"Prompt 规则管理",但租户侧的 Prompt 规则管理被忽略
### 问题 4:浏览器插件通信架构缺失
媒体管理的核心链路依赖浏览器插件,但架构中:
- 未定义插件架构(虽然提到了 `apps/browser-extension`,但标记为"可选"
- 未定义绑定回调接口
- 未定义插件 ↔ 后端的消息协议
- 未定义发布过程中的状态同步(SSE vs WebSocket vs 轮询)
### 问题 5:文章版本管理与优化链路缺失
PRD 中定义了 `ArticleVersion``OptimizationTask` 数据对象,但技术架构中:
- 未出现"版本"概念
- 未设计优化任务的 Worker 或处理流程
- 文章模块仅支持 CRUD,缺少版本历史能力
### 问题 6:SSE 实现细节不足
架构中多次提到 SSEServer-Sent Events),但:
- 仅在 `internal/shared/sse` 中有一行提及
- 未定义 SSE 连接管理、heartbeat、reconnect 策略
- 未说明哪些场景走 SSE vs 轮询
- 未定义 SSE 事件类型与负载格式
### 问题 7:前端路由粒度不足
`admin-web` 路由仅 9 条,但参考截图中的功能至少需要:
```diff
/login
/workspace
-/articles
-/articles/:id
+/articles/template # 模板创作
+/articles/template/create # 模板创建表单
+/articles/custom # 自定义生成
+/articles/custom/instant # 即时任务
+/articles/custom/scheduled # 定时任务
+/articles/custom/prompts # Prompt 规则
+/articles/optimize # 文章优化
+/articles/:id # 文章详情
/brands
+/brands/:id # 品牌详情
/knowledge
/media
-/tracking
+/tracking/overview # 数据概览
+/tracking/detail # 数据详情
/account
```
---
## 三、按模块严重级别排列
### 🔴 严重缺失(阻塞开发)
| # | 问题 | 影响模块 |
| --- | --- | --- |
| 1 | 定时调度器完全未设计 | 自定义生成 |
| 2 | Prompt 规则管理无后端模块 | 自定义生成 |
| 3 | 模板管理后端模块缺失 | 模板创作、工作台 |
| 4 | 数据库 Schema 缺失 | 全部模块 |
| 5 | GEO 指标采集方案未定义 | 数据管理 |
### 🟡 重要缺失(影响功能完整性)
| # | 问题 | 影响模块 |
| --- | --- | --- |
| 6 | 文章优化/版本管理后端缺失 | 文章优化 |
| 7 | 浏览器插件通信协议未定义 | 媒体管理 |
| 8 | 绑定回调接口缺失 | 媒体管理 |
| 9 | 竞品库管理后端缺失 | 品牌词库 |
| 10 | 品牌×关键词×模型 复合查询设计缺失 | 数据管理 |
### 🟢 轻微不足(可迭代补充)
| # | 问题 | 影响模块 |
| --- | --- | --- |
| 11 | 前端路由粒度不足 | 全部前端 |
| 12 | SSE 实现细节不足 | 长任务反馈 |
| 13 | 知识库多来源解析策略未定义 | 知识库 |
| 14 | 工作台 Dashboard 聚合 API 未设计 | 工作台 |
| 15 | 平台分类模型未定义 | 媒体管理 |
---
## 四、审查结论
> [!IMPORTANT]
> **总体评估:技术架构文档覆盖了系统的宏观层面(服务拆分、技术选型、部署方案),但在对照 PRD 和参考截图进行逐功能审查时,发现多处核心业务链路缺少对应的后端模块定义、API 设计和数据模型。**
### 建议补充项(按优先级排序):
1. **补充完整数据库 Schema 设计** — 至少覆盖 PRD 中定义的 20+ 数据对象
2. **补充模板管理模块**`internal/tenant/template`,含模板 CRUD、schema 解析
3. **补充定时调度器设计**`cmd/scheduler` 或基于 RabbitMQ Delayed Message
4. **补充 Prompt 规则管理**`internal/tenant/prompt-rule`,含 CRUD + 关联生成
5. **补充 GEO 数据采集架构** — 明确数据来源、采集频率、存储方案
6. **补充文章优化模块**`internal/tenant/optimization`,含版本管理
7. **补充浏览器插件通信协议** — 绑定/发布回调、状态同步机制
8. **细化前端路由表** — 按实际页面层级展开
9. **补充 SSE 事件定义** — 事件类型、负载格式、连接管理
10. **补充 API 接口清单** — 至少给出每个路由组下的核心端点
@@ -0,0 +1,462 @@
# AI 品牌监测数据抓取可行性与采集方案 V1
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档名称 | AI 品牌监测数据抓取可行性与采集方案 V1 |
| 文档版本 | V1.0 |
| 文档状态 | 待评审 |
| 创建日期 | 2026-04-05 |
| 适用范围 | AI 品牌监测的数据采集、解析、可行性验证 |
| 关联文档 | `docs/ai-brand-monitoring-tech-design-v2.md` |
| 关联文档 | `docs/question-driven-monitoring-design-v1.md` |
| 验证方式 | 本地代码审阅 + 官方文档核验(核验日期:2026-04-05 |
## 2. 目标与非目标
### 2.1 目标
本文回答三个问题:
1. 现有“品牌监测”数据抓取是否技术上可行。
2. 如果可行,主链路应该怎么设计才稳定、可扩展、可验证。
3. 哪些结果可以承诺,哪些结果不能承诺。
### 2.2 非目标
本文不试图解决以下问题:
1. 不承诺“100% 复刻用户在各家 APP / Web 端看到的答案、排序和引用”。
2. 不展开前端页面与汇总查询设计,这部分仍以 `docs/ai-brand-monitoring-tech-design-v2.md` 为主。
3. 不直接替代各平台的商务接入、法务评估和账号申请流程。
## 3. 核心结论
结论先说:
1. **“官方 API 口径下的品牌监测”是可行的。**
2. **“搜索增强 / 引用型品牌监测”是部分可行的,但要按平台能力分层实现。**
3. **“完全等价于终端用户在 APP / Web 里看到的内容”不可承诺,至少 DeepSeek 官方已明确 API 与 APP/Web 版本不同。**
因此,监测目标必须拆成三档:
| 档位 | 定义 | 可行性 | 是否建议纳入主链路 |
| --- | --- | --- | --- |
| A. 标准化 API 监测 | 统一问题、统一温度、统一提示词,通过官方 API 取回答,再做品牌/竞品/引用解析 | 高 | 是 |
| B. 搜索增强监测 | 使用平台官方搜索增强、联网搜索、引文能力,监测“搜索型回答中的品牌曝光” | 中到高 | 是,但只对支持平台开启 |
| C. APP/Web 拟真监测 | 模拟用户在各家产品前台提问,追求与终端用户体验一致 | 低到中 | 否,不建议作为主数据源 |
对 V2 方案的建议是:
1. **主数据源只走官方 API。**
2. **搜索增强能力按平台单独开关,不搞“全平台统一假设”。**
3. **如果业务一定要看终端用户视角,单独做“小样本 UI 观测集”,不要并入核心统计口径。**
## 4. 官方能力核验结果
以下结论基于 2026-04-05 当天核验到的官方材料。
### 4.1 平台能力矩阵
| 平台 | 官方能力核验 | 可用于监测的能力 | 可行性判断 | 备注 |
| --- | --- | --- | --- | --- |
| DeepSeek | 官方 API 与 OpenAI 兼容;官方文档明确 API 对应的 `deepseek-chat` / `deepseek-reasoner` 与 APP/Web 版本不同;官方 rate limit 页面说明高流量下会排队,10 分钟未开始推理会断开 | 标准对话、流式、工具调用入口 | **标准 API 监测可行;终端拟真不可承诺** | 适合作为“基础语言口径”,不适合作为“用户端搜索曝光”唯一来源 |
| 通义千问 / 百炼 | OpenAI 兼容;支持 `tools`;支持 `enable_search``search_options.forced_search`;支持 Batch Chat 与文件批量提交 | 标准对话、函数调用、联网搜索、批量离线推理 | **高可行** | 很适合作为主力采集平台之一 |
| 豆包 / 火山方舟 | 官方文档明确有 `Web Search(联网搜索)``Function Calling``File API`;部署文档区分在线推理与批量推理;常规在线推理说明公共资源池下延迟和并发一般、共享限流 | 标准对话、联网搜索、工具调用、批量推理 | **高可行** | 适合做搜索型监测,但生产要避免完全依赖公共在线推理 |
| 腾讯混元 | OpenAI 兼容;默认 5 并发;支持 `enable_enhancement``force_search_enhancement``search_info``citation` | 标准对话、搜索增强、返回搜索信息、回答角标引文 | **能力强,但默认并发偏低** | 搜索引用监测非常适合,但必须提前做并发扩容或限额申请 |
| 文心一言 / 千帆 | 百度官方产品页确认企业服务由千帆大模型平台提供,含推理服务和工具链,并支持私有资源托管保障并发 | 平台接入可行 | **中可行,需先做 PoC** | 本次核验中,官方可抓取材料足以确认“能接入”,但不足以确认搜索/引文参数细节 |
### 4.2 关键判断
#### 判断 1:能抓到数据,不代表能复刻用户端
DeepSeek 官方文档明确说 API 模型与 APP/Web 版本不同,这意味着:
1. 即使问题文案一样,回答内容也可能不同。
2. 即使回答主题相近,排序、措辞、引用也可能不同。
3. 因此不能把 API 数据直接包装成“终端用户真实看到的内容”。
#### 判断 2:搜索增强和引用能力不是全平台对齐的
各家平台差异很大:
1. 混元已经把 `search_info``citation` 直接定义成参数。
2. 千问已明确支持 `enable_search`,还能强制搜索。
3. 方舟已把 Web Search 作为标准工具能力。
4. DeepSeek 在本次核验的官方材料里,没有看到同等级别的“原生联网搜索 + 引文返回”说明。
5. 百度千帆本次核验只确认了平台接入能力,搜索与引文细节仍需单独 PoC。
因此,**“平台统一采集器 + 平台能力开关”是必须的,不是可选项。**
#### 判断 3:批量离线比凌晨突发跑批更靠谱
监测任务本质是离线任务,不是用户实时请求。对于大批量问题:
1. 千问官方明确提供 Batch Chat,适合“无需实时响应”的场景。
2. 方舟文档体系也区分了在线推理和批量推理。
3. 混元默认只有 5 并发,不适合把所有任务集中在 2 小时窗口内硬跑。
所以应把采集方案设计成:
1. **全天分布式执行。**
2. **优先使用批量接口。**
3. **只有需要搜索增强或即时补采时,才走在线 API。**
## 5. 建议的数据抓取目标口径
### 5.1 统一采集单位
沿用 V1 结论,但扩展成更适合多平台的形式:
`1 question_version x 1 ai_platform x 1 run_mode x 1 run_time = 1 条原始采集记录`
其中 `run_mode` 必须显式记录:
1. `api_standard`
2. `api_search_grounded`
3. `ui_sample`
没有这个字段,后续所有跨平台比较都会失真。
### 5.2 必须采集的原始字段
建议在现有 `question_monitor_runs` / `question_monitor_parse_results` 之外,至少保证以下信息能落盘:
| 类别 | 字段 |
| --- | --- |
| 请求侧 | `provider_key`, `model`, `run_mode`, `prompt_template_version`, `temperature`, `top_p`, `seed`, `search_enabled`, `tool_enabled` |
| 响应侧 | `raw_answer_text`, `raw_response_json_key`, `response_latency_ms`, `finish_reason`, `usage_prompt_tokens`, `usage_completion_tokens` |
| 搜索证据 | `search_hit`, `search_provider`, `search_info_json`, `citation_markers`, `normalized_citations_json` |
| 解析结果 | `brand_mentioned`, `brand_position`, `top1_mentioned`, `first_recommended`, `competitor_mentions`, `sentiment_label`, `brand_keywords_extract` |
| 调试追踪 | `request_id`, `retry_count`, `error_code`, `error_message`, `parser_version`, `extractor_version` |
其中:
1. `raw_answer_text` 可以存对象存储。
2. `raw_response_json_key` 也应该存对象存储,方便 badcase 回放。
3. `search_info_json` 必须保留原始返回,后续才能验证引用解析是否正确。
## 6. 推荐抓取架构
### 6.1 平台适配器,而不是一个通用 LLM Client 硬扛全部平台
当前代码里 `Config` 只有一份全局 `LLMConfig``llm.New()` 也只按单个 provider 初始化客户端,这不足以支撑监测场景的多平台采集。
相关代码:
1. `server/internal/shared/config/config.go`
2. `server/internal/shared/llm/client.go`
建议改成:
```go
type ProviderCapability struct {
Chat bool
Search bool
Citation bool
ToolCalling bool
Batch bool
Streaming bool
Deterministic bool
}
type MonitoringProvider interface {
Key() string
Capability() ProviderCapability
Generate(ctx context.Context, req CollectRequest) (*CollectResponse, error)
SubmitBatch(ctx context.Context, jobs []CollectRequest) (*BatchJob, error)
PollBatch(ctx context.Context, jobID string) (*BatchJobResult, error)
}
```
### 6.2 建议的抓取链路
```text
调度器
-> 任务生成(按品牌 / 问题版本 / 平台 / 模式)
-> 去重与幂等校验
-> 队列
-> Provider Adapter
-> 原始响应落对象存储
-> 标准化解析器
-> 引用规范化
-> 质量采样 / badcase
-> question_monitor_runs / parse_results
-> 日级聚合
```
### 6.3 任务状态机
必须补全任务状态,不建议只保留 `pending/running/completed/failed` 四态。
建议至少支持:
1. `pending`
2. `leased`
3. `running`
4. `succeeded`
5. `retryable_failed`
6. `dead_letter`
7. `cancelled`
8. `skipped_dedup`
原因:
1. 平台 API 容易超时、排队、限流。
2. 搜索增强请求比普通请求更容易波动。
3. 如果没有 `retryable_failed``dead_letter`,PoC 阶段会很难判断是平台问题还是解析问题。
### 6.4 幂等策略
建议幂等 key
`tenant_id + brand_id + question_version_id + ai_platform_id + run_mode + business_date`
这样可以保证:
1. 同一天重复调度不会重复记账。
2. 同题跨平台不会互相覆盖。
3. `api_standard``api_search_grounded` 能并存。
## 7. Prompt 与解析方案
### 7.1 Prompt 原则
抓取时不要用自由发挥的产品提示词,要用固定模板。
建议:
1. 系统提示词固定版本化。
2. 用户问题原文直接使用 `question_version.question_text`
3. 除非平台要求,否则 `temperature` 设为低值。
4. 能设置 `seed` 的平台尽量设置,提升复现性。
5. 搜索增强模式下,显式要求模型给出品牌推荐及可验证来源。
### 7.2 解析顺序
解析不要一上来全交给 LLM。
建议顺序:
1. **规则层**:品牌名、竞品名、URL、角标、序号。
2. **轻语义层**:品牌排名、首位提及、是否明确推荐。
3. **LLM 兜底层**:情感倾向、品牌印象词、复杂引用归因。
原因:
1. 规则层稳定、便宜、可复跑。
2. 复杂语义才需要 LLM。
3. 这样 badcase 也更容易定位。
### 7.3 引用解析策略
引用解析建议拆成三层:
1. **显式链接**:从平台返回的 `search_info`、角标链接、正文 URL 中直接抽取。
2. **正文映射**:把角标 `[1]``[2]` 映射回 `search_info`
3. **规范化**:统一域名、小写 host、去掉追踪参数、提取站点名、生成 `citation_hash`
建议新增规范化后的引用事实结构:
```json
{
"url": "https://example.com/a",
"normalized_url": "https://example.com/a",
"domain": "example.com",
"site_name": "Example",
"citation_type": "editorial",
"position": 1,
"source": "search_info",
"confidence": 0.98
}
```
## 8. 吞吐与调度建议
### 8.1 不要把采集窗口锁死在凌晨 1 点到 3 点
以 V2 的规模假设为例:
`100 品牌 x 100 问题 x 5 平台 x 1 次/天 = 50,000 条/天`
如果把 50,000 条任务都塞进 2 小时窗口:
1. 平均需要 `6.94 req/s`
2. 算上超时、重试、抖动,实际需要按 `15~20 req/s` 设计。
3. 对默认 5 并发的混元,这个窗口过于激进。
因此建议:
1. 日常采集分 6 个时间窗执行,或者全天滚动执行。
2. 重要品牌再补一轮搜索增强采集。
3. 聚合 Job 与采集 Job 解耦,不要求同一夜全部完成。
### 8.2 平台分流策略
建议把平台分成三类:
| 类别 | 平台 | 建议方式 |
| --- | --- | --- |
| 标准对话主链路 | DeepSeek | 在线 API,低温、固定 prompt |
| 搜索增强主链路 | 千问、混元、方舟 | 优先开官方搜索能力 |
| 待验证平台 | 百度千帆 | 先做 PoC,不直接承诺上线指标 |
### 8.3 批量优先级
建议优先级:
1. 能批量就批量。
2. 需要搜索增强或引用时再走在线 API。
3. 需要“接近用户侧”验证时,只做样本观测,不做全量。
## 9. PoC 可行性验证方案
这部分是本方案最核心的落地动作。
### 9.1 PoC 目标
在决定正式开发前,先验证四件事:
1. 各平台能否稳定返回可解析的回答。
2. 搜索增强模式下能否拿到可追溯引用。
3. 同一个问题多次执行时,结果波动是否在可接受范围内。
4. 成本、时延、失败率是否可控。
### 9.2 PoC 样本设计
建议最小样本:
1. 3 个品牌
2. 每品牌 30 个问题
3. 5 个平台
4. 2 种模式:`api_standard``api_search_grounded`
5. 连续跑 3 天
总任务量:
`3 x 30 x 5 x 2 x 3 = 2,700`
这个规模足够看出:
1. 平台成功率
2. 搜索命中率
3. 引用解析效果
4. 回答波动
### 9.3 PoC 验收指标
建议先用下面的阈值:
| 指标 | 目标 |
| --- | --- |
| API 调用成功率 | >= 97% |
| 解析成功率 | >= 99% |
| 搜索增强模式下“存在可用引用”的比例 | >= 60% |
| 品牌提及检测准确率 | >= 95% |
| 引用 URL 规范化成功率 | >= 95% |
| 单任务 P95 延迟 | <= 20s |
| 重试后仍失败率 | <= 2% |
如果某平台低于阈值:
1. 不要强行并入统一统计。
2. 先降级成“样本观测平台”。
### 9.4 PoC 输出物
PoC 结束后必须产出:
1. 平台能力对比表
2. badcase 样本集
3. 成本与时延报告
4. 建议上线的平台白名单
## 10. 建议补充的数据表
V2 当前只覆盖了运行表和解析表,若要把抓取做扎实,建议再补三类表:
### 10.1 `monitoring_collect_tasks`
用途:
1. 存调度任务
2. 管理状态机
3. 支撑重试、死信、优先级
### 10.2 `monitoring_citation_facts`
用途:
1. 保存规范化后的引用事实
2. 便于后续按域名、站点、页面统计
3. 避免每次都从 JSONB 重新解析
### 10.3 `monitoring_parse_badcases`
用途:
1. 保存低置信度或人工标错样本
2. 支撑解析器迭代
3. 给后续模型评测提供数据集
## 11. 实施建议
### Phase A:两周内完成 PoC
1. 先接 3 个最清晰的平台:千问、混元、方舟。
2. DeepSeek 只做 `api_standard`
3. 百度千帆只做可接入性验证,不承诺引用链路。
### Phase B:收敛主链路
1. 形成统一 `MonitoringProvider` 抽象。
2. 完成搜索增强、引用解析、规范化。
3. 上线 badcase 采样与抽检。
### Phase C:再决定是否做 UI 观测
只有当业务方明确要求“终端用户真实视角”时,才考虑单独建设 UI 观测链路;且必须先做:
1. 法务 / ToS 评估
2. 账号稳定性评估
3. 样本规模评估
它不应进入 V1 主数据链路。
## 12. 对现有 V2 方案的直接修正建议
基于本文结论,建议对 `docs/ai-brand-monitoring-tech-design-v2.md` 做以下修正:
1. 把“采集流水线”从通用 `llm.Client` 调整为“平台适配器 + 能力开关”。
2. 增加 `run_mode` 字段,区分标准模式与搜索增强模式。
3. 增加任务表、死信策略、重试退避、幂等 key。
4. 不再默认所有平台都能产出引用来源数据。
5. 把凌晨集中跑批改成“滚动采集 + 批量优先”。
6. 在上线前增加 PoC 验收阶段,而不是直接进入全量开发。
## 13. 官方核验链接
以下均为本次用于判断可行性的官方材料:
1. DeepSeek API Docs: https://api-docs.deepseek.com/
2. DeepSeek Rate Limit: https://api-docs.deepseek.com/quick_start/rate_limit
3. 阿里云百炼 OpenAI 兼容: https://help.aliyun.com/zh/model-studio/compatibility-of-openai-with-dashscope
4. 阿里云百炼 Batch Chat: https://help.aliyun.com/zh/model-studio/openai-compatible-batch-chat
5. 腾讯混元 OpenAI 兼容接口: https://cloud.tencent.com/document/product/1729/111007
6. 火山方舟 Web Search: https://www.volcengine.com/docs/82379/1756990
7. 火山方舟常规在线推理: https://www.volcengine.com/docs/82379/2121998
8. 百度文心一言 / 千帆平台: https://cloud.baidu.com/wenxin.html
## 14. 最终结论
一句话总结:
**这件事能做,但必须把“官方 API 监测”与“终端用户视角监测”分开设计。**
对当前项目最现实的路线是:
1. 先做基于官方 API 的标准化品牌监测。
2. 对具备官方搜索能力的平台,叠加搜索增强与引用监测。
3. 用 PoC 决定百度千帆是否进入第一批上线平台。
4. 不把 APP/Web 前台抓取作为主链路承诺项。
File diff suppressed because it is too large Load Diff
+934
View File
@@ -0,0 +1,934 @@
# AI 品牌曝光监测系统技术方案 V3
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档名称 | AI 品牌曝光监测系统技术方案 V3(容量与口径修订稿) |
| 文档版本 | V3.0 |
| 文档状态 | 待评审 |
| 创建日期 | 2026-04-05 |
| 基线文档 | `docs/ai-brand-monitoring-tech-design-v2.md` |
| 适用范围 | 数据追踪模块全部页面 |
| 关联文档 | `docs/geo-platform-prd-v1.md` (PRD 8.4 数据追踪) |
| 关联文档 | `docs/question-driven-monitoring-design-v1.md` (V1 数据模型) |
| 容量目标 | 5 万并发 **活跃** 用户(同时打开页面、切换筛选、触发突刺流量) |
## 2. V3 修订范围
本文档不再只是 V2 的“容量补丁”。本版同时修正了 V2 与采集可行性方案之间的口径断层,尤其是:
1. `run_mode` 未入模,导致标准回答和搜索增强回答不可比。
2. 问题版本未完整进入汇总表唯一键,历史口径会被覆盖。
3. 采集时间窗与聚合时序冲突,无法同时满足 T+1 发布与一致性。
4. 在线缓存与离线任务队列共用同一 Redis 故障域,风险过高。
读者应先读完 V2 全文,再用本文档替换或补充以下章节:
| V2 章节 | V3 替换 / 补充章节 | 变更原因 |
| --- | --- | --- |
| 5. 数据库 Schema | 2.2 数据口径与 Schema Delta | V2 未建模 `run_mode`、版本键、引用事实 |
| 8. 缓存策略 | 3. 读流量模型与缓存架构 | V2 低估页面 fan-out,缺少防雪崩手段 |
| 10. 采集流水线设计 | 4. 采集流水线扩容 | V2 采集时间窗 / worker 容量不足 |
| 11. 部署拓扑 | 5. 部署拓扑与资源 | V2 连接池计算有误、单点问题 |
| 7. 后端架构 / 7.6 配置扩展 | 4.5、5.3、5.4 | V2 仍按单 provider / 单 Redis / 单 DB 结构假设 |
| (无) | 6. 降级与容灾 | V2 缺失 |
| 13. 实施计划 (Phase 21) | 7. 压测验收标准 | V2 缺失 |
V2 其他章节(页面需求、前端组件、API 响应结构、指标定义)原则上保持不变;但只要与本文中的口径分层、Schema Delta、采集时序冲突,应以 V3 为准。
### 2.1 数据口径分层与 UI 取数规则
为避免把不可比的数据混到同一张榜单中,V3 将监测数据拆成 3 种采集模式:
| `run_mode` | 含义 | 是否进入主统计 | 默认消费页面 |
| --- | --- | --- | --- |
| `api_standard` | 固定 prompt、低温、无搜索增强的标准 API 回答 | 是 | Dashboard、平台占比、竞争对手、业务主题、AI 对话问题、品牌印象 |
| `api_search_grounded` | 开启平台官方搜索 / 引文能力后的回答 | 是,但单独统计 | AI 引用排名;未来可选做“搜索增强视角”专题页 |
| `ui_sample` | 面向 PoC / 验证的前台 UI 采样结果 | 否 | 仅验证报表和 badcase,不进主 Dashboard |
核心规则:
1. **跨平台曝光、竞品、问题排行默认只看 `api_standard`。**
2. **引用相关页面默认只看 `api_search_grounded`。**
3. **不支持搜索增强或引文的平台,在引用页面中标记为“未接入 / 不支持”,不按 0 值并入分母。**
4. **`ui_sample` 只用于验证,不参与 `monitoring_*` 主汇总。**
### 2.2 Schema Delta(覆盖 V2 Schema 的必要修订)
V2 的基础表结构仍可复用,但以下修订是必需项。
#### 2.2.1 `ai_platforms`
增加平台能力字段,避免查询层硬编码:
1. `supports_standard BOOLEAN NOT NULL DEFAULT true`
2. `supports_search_grounded BOOLEAN NOT NULL DEFAULT false`
3. `supports_citation BOOLEAN NOT NULL DEFAULT false`
4. `supports_batch BOOLEAN NOT NULL DEFAULT false`
5. `default_run_mode VARCHAR(30) NOT NULL DEFAULT 'api_standard'`
#### 2.2.2 `question_monitor_runs`
新增以下字段:
1. `business_date DATE NOT NULL`:统计归属日,允许 `run_at` 发生在次日凌晨。
2. `run_mode VARCHAR(30) NOT NULL``api_standard` / `api_search_grounded` / `ui_sample`
3. `provider_model VARCHAR(100) NOT NULL`
4. `prompt_template_version VARCHAR(30) NOT NULL`
5. `search_enabled BOOLEAN NOT NULL DEFAULT false`
6. `request_id VARCHAR(100)`
7. `retry_count INT NOT NULL DEFAULT 0`
建议增加幂等索引:
```sql
CREATE UNIQUE INDEX uk_monitor_run_idempotent
ON question_monitor_runs(tenant_id, brand_id, question_version_id, ai_platform_id, run_mode, business_date);
```
#### 2.2.3 `question_monitor_parse_results`
新增以下字段:
1. `run_mode VARCHAR(30) NOT NULL`
2. `search_hit BOOLEAN NOT NULL DEFAULT false`
3. `search_info_json_key TEXT`
4. `extractor_version VARCHAR(30) NOT NULL DEFAULT 'v1'`
#### 2.2.4 新增任务与质量表
新增下列表:
1. `monitoring_collect_tasks`:调度任务、重试、死信、优先级、租约状态
2. `monitoring_citation_facts`:引用规范化事实表,保存 `normalized_url/domain/citation_hash`
3. `monitoring_parse_badcases`:低置信度样本与人工回标结果
#### 2.2.5 汇总表统一增加 `run_mode`
以下汇总表必须增加 `run_mode`,并进入唯一键:
1. `monitoring_brand_daily_overview`
2. `monitoring_platform_daily`
3. `monitoring_platform_question_daily`
4. `monitoring_competitor_daily`
5. `monitoring_competitor_platform_daily`
6. `monitoring_topic_daily`
7. `monitoring_question_daily`
8. `monitoring_citation_source_daily`
9. `monitoring_citation_page_daily`
10. `monitoring_brand_impression`
#### 2.2.6 问题版本进入汇总主键
以下表必须增加 `question_version_id` 并进入唯一键:
1. `monitoring_platform_question_daily`
2. `monitoring_question_daily`
推荐唯一键修订:
```sql
UNIQUE (tenant_id, brand_id, ai_platform_id, question_id, question_version_id, run_mode, metric_date)
UNIQUE (tenant_id, brand_id, question_id, question_version_id, run_mode, metric_date)
```
#### 2.2.7 引用汇总改为从规范化事实表聚合
`monitoring_citation_source_daily``monitoring_citation_page_daily` 不再直接从 `citation_urls JSONB` 聚合,而是从 `monitoring_citation_facts` 聚合:
1. 页面级汇总按 `normalized_url`
2. 站点级汇总按 `domain` / `site_name`
3. 原始 `page_url` 仅用于回放与调试
---
## 3. 读流量模型与缓存架构
### 3.1 页面 Fan-out 矩阵
V2 错误地按"每人 30 秒 1 个请求"建模。实际上,每个页面打开时会 **并行** 发出多个 API 请求。
| 页面 | 并行请求数 | 对应 API |
| --- | --- | --- |
| 主页 Dashboard | 6 | overview, exposure-trend, competitors, citation-sources, recent-questions, brand-impression |
| 平台占比分析 | 2 | platform-stats, platform-questions |
| 竞争对手分析 | 2 | competitor-exposure, competitor-detail |
| 业务主题分析 | 1 | topics |
| AI 对话问题 | 1 | questions (分页) |
| AI 引用排名 | 2 | citation-trend, citation-pages |
加权平均(按访问占比估算:主页 50%、其他页各 10%):
```
weighted_fan_out = 0.50 × 6 + 0.10 × 2 + 0.10 × 2 + 0.10 × 1 + 0.10 × 1 + 0.10 × 2
= 3.0 + 0.2 + 0.2 + 0.1 + 0.1 + 0.2
= 3.8 次/页面打开
```
### 3.2 QPS 模型
| 场景 | 计算 | QPS |
| --- | --- | --- |
| 稳态(50K 用户,平均 30 秒交互 1 次) | 50000 × 3.8 / 30 | **6,333** |
| 突刺(早会/开盘集中打开,10 秒内 30% 用户首屏加载) | 15000 × 6 / 10 | **9,000** |
| 筛选切换(20% 用户同时切换品牌/时间范围) | 10000 × 3.8 / 5 | **7,600** |
| 设计容量(取 P99 峰值 × 1.5 安全余量) | max(6333, 9000, 7600) × 1.5 | **~13,500** |
**结论**:系统必须在 Redis 层稳定承接 13,500 QPS。穿透到 PG 的流量目标 < 1,350 QPS10% miss rate)。
### 3.3 BFF 聚合层(减少 fan-out 的关键手段)
为降低首页 6 次并行请求的压力,引入 **BFF 聚合接口**
```
GET /api/tenant/monitoring/dashboard/composite
→ 服务端并行查询 6 个数据源,一次返回全部首页数据
→ 默认固定 `scope=standard`
→ 单次 Redis GET 命中完整 composite 缓存即可返回
→ 缓存 miss 时服务端内部 goroutine 并行查 6 个汇总表
```
前端首屏只发 **1 个请求**,极端情况下用户手动刷新某个区块再发单接口。
| 方案 | 首页 QPS (50K 用户) | Redis 压力 |
| --- | --- | --- |
| 6 个独立接口 | ~10,000 | 60K ops/s |
| composite 聚合 | ~1,667 | 1,667 ops/s |
其他页面 fan-out ≤ 2,不需要聚合。
### 3.4 缓存防雪崩机制
V2 仅使用 TTL + 聚合后全量删除,存在雪崩风险。V3 引入 4 层防护:
#### 3.4.1 Singleflight
同一 cache key 的并发 miss 请求,只放行 1 个打到 PG,其余等待结果。
```go
import "golang.org/x/sync/singleflight"
var sf singleflight.Group
func (s *MonitoringService) getWithCache(ctx context.Context, key string, ttl time.Duration, loader func() (any, error)) (any, error) {
// 1. 尝试 Redis GET
if cached, err := s.cache.Get(ctx, key); err == nil {
return cached, nil
}
// 2. Singleflight: 同 key 只执行一次 loader
result, err, _ := sf.Do(key, func() (any, error) {
data, err := loader()
if err != nil {
return nil, err
}
// 写回 RedisTTL 加随机抖动
jitteredTTL := ttl + time.Duration(rand.Int63n(int64(ttl/5)))
_ = s.cache.Set(ctx, key, data, jitteredTTL)
return data, nil
})
return result, err
}
```
#### 3.4.2 TTL 随机抖动
防止大量 key 同时过期导致集中穿透。
```
实际 TTL = 基础 TTL + random(0, 基础 TTL × 20%)
```
例如 5 分钟基础 TTL → 实际 300~360 秒。
#### 3.4.3 Stale-While-Revalidate
缓存过期后,先返回旧数据,后台异步刷新。用户不感知延迟。
```go
// Redis 存储两个 key:
// mon:{key} → 数据 (TTL = base_ttl)
// mon:{key}:stale → 数据 (TTL = base_ttl × 3,作为兜底)
//
// 读取流程:
// 1. GET mon:{key} → 命中 → 返回
// 2. miss → GET mon:{key}:stale → 命中 → 返回 stale 数据 + 异步触发刷新
// 3. 都 miss → 同步查 PG + singleflight
```
#### 3.4.4 热点保护(Hot Key
对高频访问的品牌(如首页默认品牌),使用 **进程内 L1 缓存**Go sync.Map 或 groupcache):
```
请求 → L1 进程缓存 (TTL 10s) → Redis (TTL 5min) → PG
```
L1 缓存的极短 TTL 保证数据不会太旧,但能挡住 Redis 的瞬时压力。
### 3.5 缓存 Key 修订
修复 V2 的分页缓存 key 缺少 `page_size` 的问题,同时将统计口径 `scope` 纳入缓存维度。
约定:
1. `scope=standard` 对应 `run_mode=api_standard`
2. `scope=search` 对应 `run_mode=api_search_grounded`
3. `ui_sample` 不进入线上缓存
```
# 分页接口必须包含 scope + page + page_size
mon:{tenant}:{brand}:platform:{pid}:questions:{days}:{scope}:{page}:{pageSize}
mon:{tenant}:{brand}:questions:{pid}:{days}:{scope}:{page}:{pageSize}
mon:{tenant}:{brand}:citation_pages:{days}:{scope}:{page}:{pageSize}
# composite 聚合接口
mon:{tenant}:{brand}:dashboard_composite:{days}:{scope}
# 非分页接口也加 scope
mon:{tenant}:{brand}:overview:{days}:{scope}
mon:{tenant}:{brand}:trend:{days}:{scope}
mon:{tenant}:{brand}:competitors:{days}:{scope}
...
```
### 3.6 缓存失效策略修订
V2 的"聚合后删除品牌全部缓存"策略改为 **渐进式失效**
| 时机 | 操作 | 原因 |
| --- | --- | --- |
| 聚合 Job 完成 | **不删除** 现有缓存 | 避免 5 万用户同时穿透 |
| 聚合 Job 完成 | 异步预热 Top 20 热门品牌的 composite 缓存 | 用新数据替换旧缓存 |
| 聚合 Job 完成 | 设置全局标记 `mon:agg_version:{tenant}` +1 | 通知各实例 L1 缓存过期 |
| 自然过期 | TTL 到期后正常 singleflight 回源 | 冷数据自然刷新 |
### 3.7 修订后的 TTL 矩阵
| 数据类型 | Redis TTL | Stale TTL | L1 TTL | 说明 |
| --- | --- | --- | --- | --- |
| Dashboard composite | 5 min | 15 min | 10 s | 首页入口,高频访问 |
| Overview 指标卡 | 5 min | 15 min | 10 s | 同上 |
| 趋势图 | 15 min | 45 min | 30 s | 日级数据,变化慢 |
| 竞品排名 | 15 min | 45 min | 30 s | 同上 |
| 引用来源 | 15 min | 45 min | 30 s | 同上 |
| 业务主题 | 30 min | 90 min | 60 s | 低频变更 |
| 问题列表(分页) | 10 min | 30 min | 不缓存 | 粒度太细,L1 命中率低 |
| 品牌印象 | 30 min | 90 min | 60 s | 日级聚合 |
| AI 平台列表 | 1 hour | 3 hour | 5 min | 几乎不变 |
---
## 4. 采集流水线扩容
### 4.1 V2 瓶颈分析
V2 的问题:
- 5 worker、50,000 任务 → 每任务仅 0.72 秒,不现实
- 凌晨 1 点采集、3 点聚合 → 2 小时时间窗太紧
- Collector ×1、Aggregator ×1 → 单点故障
### 4.2 任务量重新估算
```
标准模式:
100 品牌 × 100 问题/品牌 × 5 AI 平台 = 50,000 任务/天
搜索增强模式(首批仅 3 平台: qwen / doubao / hunyuan:
100 品牌 × 100 问题/品牌 × 3 AI 平台 = 30,000 任务/天
合计 = 80,000 任务/天
# 但问题 hash 去重后(V1 设计)
假设去重率 30% → 实际 56,000 唯一任务
# 每次 AI API 调用 + 解析 + 入库:
平均耗时: 8 秒 (含 AI 生成 5s + 解析 2s + 入库 1s)
P99 耗时: 20 秒 (含重试)
```
### 4.3 Worker 池设计
```
所需总处理时间 = 56,000 × 8s = 448,000 秒
可用采集时间窗 = 6 小时 = 21,600 秒 (0:00 ~ 6:00)
所需并发 worker = 448,000 / 21,600 ≈ 21
安全系数 1.4 → 30 worker
```
**架构**
```
┌─────────────────────────────────────────────────────┐
│ 采集调度器 (Scheduler) │
│ • 凌晨 0:00 启动 │
│ • 生成 business_date = yesterday 的全部任务 │
│ • 任务维度: 品牌 × 去重问题 × 平台 × run_mode │
│ • 写入 Queue Redis Stream: mon:collect:tasks │
└───────────────────────┬─────────────────────────────┘
┌─────────────┼─────────────┬─────────────┐
▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ Collector │ │ Collector │ │ Collector │ │ Collector │
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │ │ standby HPA│
│ worker ×10 │ │ worker ×10 │ │ worker ×10 │ │ 按需扩容 │
└──────┬─────┘ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────┐
│ Queue Redis Stream │
│ mon:collect:tasks (pending → ack) │
│ Consumer Group: collectors │
└─────────────────────────────────────────┘
```
**为什么用 Redis Stream 而不是内存队列**
- 多 Pod 竞争消费,自动负载均衡
- 消息持久化,Pod 重启不丢任务
- Consumer Group 的 XACK 机制提供 at-least-once 保证
- 但必须使用 **独立 Queue Redis 故障域**,不能和在线缓存共用实例
### 4.4 时间窗修订
```
0:00 调度器生成 business_date = yesterday 的全部采集任务 → Queue Redis Stream
0:00 ~ 6:00 Collector workers 消费任务,调用 AI API + 解析 + 写 PG/MinIO
6:00 检查未完成任务数量,告警如果 > 5%
6:10 将未完成任务转入 backfill 队列,不阻塞晨间主聚合
6:30 Aggregator 聚合 business_date = yesterday 且 status = succeeded 的数据
7:00 聚合完成 → 预热热门品牌缓存
7:30 T+1 数据上线,用户访问“昨日完整快照”
```
说明:
1. `run_at` 是实际执行时间,可能发生在次日凌晨。
2. `business_date` 是统计归属日,Dashboard 晨间看到的是 T+1 快照。
3. 晚到任务走 backfill,单独触发品牌级重聚合,不污染主发布窗口。
预留 6 小时采集窗口(vs V2 的 2 小时),即使 P99 延迟也有足够 buffer。
### 4.5 采集重试与限流
```go
// 每个 AI 平台独立的限流器
type PlatformRateLimiter struct {
limiters map[string]*rate.Limiter // provider_key → limiter
}
// 默认限流配置
var defaultLimits = map[string]rate.Limit{
"deepseek": 10, // 10 QPS
"qwen": 10,
"doubao": 10,
"hunyuan": 5,
"ernie": 5,
}
// 重试策略
const (
maxRetries = 3
retryBaseWait = 2 * time.Second // 指数退避: 2s, 4s, 8s
)
```
此外,Collector 必须基于 `run_mode` 和平台能力做任务路由:
1. 不支持 `api_search_grounded` 的平台不生成搜索增强任务。
2. `ui_sample` 任务只进入 PoC / badcase 流水线,不进入主聚合队列。
3. `api_standard``api_search_grounded` 使用不同的 prompt 模板版本。
### 4.6 采集容错
```go
// 单条任务失败不阻塞批次
func (w *CollectorWorker) processTask(ctx context.Context, task CollectTask) {
for attempt := 1; attempt <= maxRetries; attempt++ {
err := w.collect(ctx, task)
if err == nil {
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
return
}
if isRateLimited(err) {
time.Sleep(retryBaseWait * time.Duration(1<<(attempt-1)))
continue
}
// 非限流错误直接标记失败
w.markFailed(ctx, task, err)
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
return
}
w.markFailed(ctx, task, fmt.Errorf("max retries exceeded"))
w.stream.XAck(ctx, "mon:collect:tasks", "collectors", task.ID)
}
```
### 4.7 聚合 Job 扩容
```
┌───────────────────────────────────────────────────────┐
│ Aggregator Job │
│ • 6:30 启动(聚合 business_date = yesterday
│ • 按租户 × 品牌分片,每片独立事务 │
│ • 并发聚合 goroutine: 10 │
│ • 聚合完一个品牌 → 异步预热该品牌缓存 │
│ • 全部完成后 → 全局 agg_version +1 │
└───────────────────────────────────────────────────────┘
```
聚合 Job 部署 **2 个实例**(主备),使用 **PG advisory lock** 选主,只有一个实例实际运行,另一个待命。
---
## 5. 部署拓扑与资源
### 5.1 修正:连接池是 per-process 的
V2 错误地把 `max_open_conns: 50` 当作"3 个实例共享"。实际上 `bootstrap.go``pgxpool.Pool` 是每个进程独立初始化的。
**修正后的连接池规划**
```yaml
# 每个 API 实例
database:
max_open_conns: 20 # 每实例 20
max_idle_conns: 5
# 3 个 API 实例 → PG 总连接: 60
# 3 个 Collector 实例 → PG 总连接: 30 (各 10)
# 1 个 Aggregator 实例 → PG 总连接: 20
# 合计: 110 连接
# PostgreSQL 需配置:
# max_connections = 160 (留 50 余量给运维和监控)
```
### 5.1.1 配置模型 Delta
V3 不能继续沿用当前项目的单 `database`、单 `redis`、单 `llm` 配置模型,必须扩展为以下结构:
```yaml
database:
primary:
host: pg-primary
...
replica:
host: pg-replica
...
redis:
cache:
sentinel:
master_name: geo-cache-master
addrs: ["cache-sentinel-1:26379", "cache-sentinel-2:26379", "cache-sentinel-3:26379"]
db: 0
queue:
sentinel:
master_name: geo-queue-master
addrs: ["queue-sentinel-1:26379", "queue-sentinel-2:26379", "queue-sentinel-3:26379"]
db: 0
monitoring:
providers:
deepseek:
enabled: true
supports_standard: true
supports_search_grounded: false
qwen:
enabled: true
supports_standard: true
supports_search_grounded: true
doubao:
enabled: true
supports_standard: true
supports_search_grounded: true
hunyuan:
enabled: true
supports_standard: true
supports_search_grounded: true
ernie:
enabled: false # PoC 通过后再开启
```
这意味着:
1. `server/internal/shared/config/config.go` 必须扩展结构体,不是“小改动”。
2. `bootstrap.go` 必须初始化双 DB pool、双 Redis client、provider registry。
3. 监测模块必须摆脱全局单 provider `llm.Client` 的限制。
### 5.2 部署架构
```
┌─────────────────────────────┐
│ CDN (静态资源) │
│ admin-web 打包后的 JS/CSS │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ Nginx / 云 LB (SLB/ALB) │
│ API 路由 + 健康检查 │
│ 限流: 每 IP 200 req/s │
└──────────┬───────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ tenant-api │ │ tenant-api │ │ tenant-api │
│ Pod #1 │ │ Pod #2 │ │ Pod #3 │
│ API 20conn │ │ API 20conn │ │ API 20conn │
│ L1 cache │ │ L1 cache │ │ L1 cache │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌──────┴─────────────────────┴─────────────────────┴──────┐
│ Cache Redis 6+ (主从 Sentinel) │
│ 用途: API 缓存 + JWT 会话 │
└────────────────────────┬─────────────────────────────────┘
┌────────────────────────┼────────────────────────┬────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PG Primary │────────▶│ PG Replica │ │ MinIO │ │ Queue Redis │
│ (读写) │ 流复制 │ (只读) │ │ (原始回答) │ │ Streams │
│ 160 conns │ │ 50 conns │ │ S3 兼容 │ │ 独立故障域 │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
▲ ▲ │
│ │ │
│ ┌────────────────────┘ Aggregator 的重查询走 Replica │
│ │ │
┌──────┴───┴──────────────────────────────────────────────┐
│ 独立进程(不接入 LB) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ Collector Pod #1 │ │ Collector Pod #2 │ │ Collector Pod #3 │ ×3 Pod
│ │ worker ×10 │ │ worker ×10 │ │ worker ×10 │ (竞争消费)
│ │ PG conn: 10 │ │ PG conn: 10 │ │ PG conn: 10 │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │ │
│ └────────────────────┴────────────────────┘
│ │
│ ▼
│ Queue Redis Stream
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Aggregator #1 │ │ Aggregator #2 │ 主备(锁选主)│
│ │ (active) │ │ (standby) │ │
│ │ PG conn: 20 │ │ PG conn: 0 │ │
│ │ 读走 Replica │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
╚══════════════════════════════════════════════════════════╝
```
### 5.3 Redis 高可用与故障域隔离
V2 使用 Redis 单点,且在线缓存与离线任务共用同一实例。V3 修正为 **双 Redis 故障域**
1. `redis.cache`:面向 API 读流量,承接缓存与会话
2. `redis.queue`:面向离线采集,承接 Stream 队列
推荐配置:
```yaml
redis:
cache:
sentinel:
master_name: "geo-cache-master"
addrs:
- "cache-sentinel-1:26379"
- "cache-sentinel-2:26379"
- "cache-sentinel-3:26379"
db: 0
queue:
sentinel:
master_name: "geo-queue-master"
addrs:
- "queue-sentinel-1:26379"
- "queue-sentinel-2:26379"
- "queue-sentinel-3:26379"
db: 0
```
Go 客户端(go-redis v9)原生支持 Sentinel,但这里需要初始化 **两个** client
```go
cacheRDB := newRedisClient(cfg.Redis.Cache)
queueRDB := newRedisClient(cfg.Redis.Queue)
```
### 5.4 PostgreSQL 读写分离
API 服务的监测查询走 **PG Replica**。通过在 `MonitoringService` 中注入两个 pool
```go
type MonitoringService struct {
writePool *pgxpool.Pool // Primary,仅供 aggregator 写入
readPool *pgxpool.Pool // ReplicaDashboard 查询
cache cache.Cache
sf singleflight.Group
}
```
`bootstrap.go` 中初始化时,如果配置了 `database.replica`,则创建第二个连接池。
### 5.5 资源规格修订
| 组件 | 实例数 | 规格 | PG 连接 | 说明 |
| --- | --- | --- | --- | --- |
| **tenant-api** | 3 | 4C8G | 20/实例 | API 含 L1 缓存、singleflight |
| **Cache Redis Sentinel** | 1主1从3哨兵 | 8G 内存 | - | API 缓存 + JWT 会话 |
| **Queue Redis Sentinel** | 1主1从3哨兵 | 4G 内存 | - | Stream 队列,独立故障域 |
| **PG Primary** | 1 | 8C32G | max 160 | SSD,汇总表 + 原始采集表 |
| **PG Replica** | 1 | 4C16G | max 50 | SSDDashboard 只读查询 |
| **MinIO** | 1 | 4C8G, 100G SSD | - | 原始回答对象存储 |
| **Collector** | 3 Pod | 2C4G | 10/Pod | Queue Redis Stream 竞争消费 |
| **Aggregator** | 2 Pod (主备) | 4C8G | 20 (active only) | 分布式锁选主 |
| **CDN** | - | - | - | 静态资源 + Nginx 限流 |
---
## 6. 降级与容灾
### 6.1 降级策略
| 故障场景 | 降级措施 | 用户感知 |
| --- | --- | --- |
| **Cache Redis 主节点故障** | Sentinel 自动切换从节点为主(< 30s) | 短暂超时后恢复 |
| **Cache Redis 完全不可用** | API 直接查 PG,跳过缓存;Nginx 层限流降至 50 req/s/IP | 响应变慢但可用 |
| **Queue Redis 故障** | 采集调度与 Collector 暂停;线上查询不受影响 | 当天数据延迟,但 Dashboard 继续服务旧数据 |
| **PG Primary 故障** | Dashboard 查询走 Replica 不受影响;采集/聚合暂停 | Dashboard 可用,数据冻结在最后聚合时间 |
| **PG Replica 故障** | Dashboard 查询回退到 Primary | 无感知,Primary 负载上升 |
| **单个 AI 平台 API 超时** | 该平台任务标记 failed,其他平台正常采集 | 该平台在对应模式下标记“数据缺失”,不强制展示为 0 |
| **全部 AI 平台不可用** | 当天采集任务全部失败,Dashboard 展示前一天数据 | 显示"数据更新时间:昨日" |
| **Collector Pod 全挂** | Queue Redis 消息堆积,等待 Pod 恢复后继续消费 | 当天数据延迟 |
| **Aggregator 异常** | 另一个 Aggregator Pod 抢锁接管 | 无感知 |
### 6.2 熔断器
对外部 AI 平台 API 使用熔断器(circuit breaker):
```go
// 每个 AI 平台独立的熔断器
type PlatformBreaker struct {
breakers map[string]*circuitbreaker.CircuitBreaker
}
// 配置
// 连续 5 次失败 → 打开熔断 → 30 秒后半开 → 1 次成功 → 关闭
```
### 6.3 监控告警
| 指标 | 告警阈值 | 通知方式 |
| --- | --- | --- |
| API P99 延迟 | > 2s 持续 5 分钟 | 钉钉/飞书 |
| Cache Redis 命中率 | < 80% 持续 10 分钟 | 钉钉/飞书 |
| Queue Redis backlog | > 10,000 持续 10 分钟 | 钉钉/飞书 |
| PG 连接池使用率 | > 80% | 钉钉/飞书 |
| 采集任务失败率 | > 10% 单平台 | 钉钉/飞书 |
| 聚合 Job 未在 8:00 前完成 | 超时 | 电话 + 钉钉 |
| Cache/Queue Redis Sentinel 主从切换 | 任何切换事件 | 电话 + 钉钉 |
### 6.4 数据一致性保障
| 场景 | 措施 |
| --- | --- |
| 聚合 Job 被中断 | 每个品牌的聚合是独立事务,失败后可重跑该品牌 |
| 采集与聚合时间窗重叠 | 聚合只处理 `business_date = yesterday AND status = succeeded` 的数据 |
| 缓存与 PG 不一致 | stale-while-revalidate 保证最终一致,不保证强一致 |
| 原始回答写 MinIO 失败 | 标记该 run 为 failed,解析结果不写入 |
| 晚到补采任务 | 进入 backfill 队列,只重聚合受影响品牌,不改写已发布快照的全局口径 |
---
## 7. 压测验收标准
### 7.1 压测场景模型
| 场景编号 | 场景名称 | 虚拟用户数 | 行为 | 持续时间 |
| --- | --- | --- | --- | --- |
| S1 | 稳态浏览 | 50,000 | 每 30 秒打开 1 个页面(加权 fan-out 3.8 | 30 min |
| S2 | 早高峰突刺 | 50,000 | 0~60 秒内全部打开首页(composite 接口) | 1 min ramp-up |
| S3 | 筛选风暴 | 20,000 | 每 5 秒切换品牌/时间范围 | 10 min |
| S4 | 缓存失效 | 50,000 | 手动清空 Cache Redis 后观察恢复 | 5 min |
| S5 | Cache Redis 故障 | 50,000 | kill Cache Redis master,观察 Sentinel 切换 | 2 min |
| S6 | Queue Redis 故障 | 0 | kill Queue Redis master,观察采集暂停与恢复 | 30 min |
### 7.2 性能目标(SLA
| 指标 | 目标值 | 说明 |
| --- | --- | --- |
| API P50 延迟 | < 50 ms | 稳态,Redis 命中 |
| API P95 延迟 | < 200 ms | 稳态 |
| API P99 延迟 | < 500 ms | 含缓存 miss + PG 查询 |
| API P99 延迟(突刺) | < 2 s | S2 场景 |
| 错误率 | < 0.1% | 5xx 错误 |
| Cache Redis 命中率 | > 90% | 稳态 |
| PG QPS(穿透) | < 1,500 | 10% miss rate |
| 采集完成率 | > 95% | 日采集任务成功率 |
| 聚合 SLA | 8:00 前完成 | 用户上班前数据可用 |
| Cache Redis 故障恢复 | < 30 s | Sentinel 自动切换 |
| Queue Redis 恢复后 backlog 清空 | < 60 min | 采集可恢复性 |
### 7.3 压测工具与脚本
```
# 推荐工具: k6 (grafana/k6)
# 脚本位置: tests/load/
tests/load/
├── k6.config.js # 共享配置
├── scenarios/
│ ├── s1_steady_browse.js # 稳态浏览
│ ├── s2_peak_burst.js # 早高峰突刺
│ ├── s3_filter_storm.js # 筛选风暴
│ ├── s4_cache_flush.js # 缓存失效
│ └── s5_cache_redis_failover.js # Cache Redis 故障
├── helpers/
│ ├── auth.js # 登录获取 token
│ └── brands.js # 随机品牌选择
└── README.md # 运行说明(S6 Queue Redis 故障为运维演练,无 k6 脚本)
```
### 7.4 压测前置条件
1. **数据准备**:100 品牌 × 100 问题 × 5 平台 × 30 天汇总数据已填充
2. **用户池**:50,000 个预创建的测试用户 token
3. **环境**:与生产同规格的独立压测环境
4. **监控**Prometheus + Grafana 大盘实时观察
### 7.5 验收流程
```
Phase 1: 基线测试
→ 5,000 用户稳态,确认 P99 < 100ms
→ 记录基线指标
Phase 2: 容量爬坡
→ 逐步提升: 10K → 20K → 30K → 40K → 50K
→ 每级停留 5 分钟,观察指标是否劣化
→ 记录每级的 P50/P95/P99、错误率、Redis/PG 指标
Phase 3: 极限测试
→ 50K 稳态 30 分钟
→ 中间插入 S2 突刺、S3 筛选风暴
→ 确认所有 SLA 指标达标
Phase 4: 容灾演练
→ S4 缓存失效:确认 singleflight 生效,PG 不被打爆
→ S5 Cache Redis 故障:确认 Sentinel 切换 < 30sAPI 降级可用
→ S6 Queue Redis 故障:确认在线 API 不受影响,采集恢复后 backlog 可消化
Phase 5: 报告
→ 输出压测报告,包含:
- 每个场景的延迟分布图
- 资源使用率(CPU/内存/连接池/磁盘IO)
- 瓶颈分析与调优建议
- 通过/未通过结论
```
---
## 8. 实施计划修订
在 V2 原有 Phase 15~21 的基础上,调整以下 Phase:
### Phase 15 修订:DB 迁移 + 数据口径
新增:
- `run_mode``business_date`、平台能力字段 migration
- `question_version_id` 进入问题级汇总唯一键
- 新增 `monitoring_collect_tasks` / `monitoring_citation_facts` / `monitoring_parse_badcases`
- 引用聚合改为从规范化事实表出发
### Phase 16 修订:后端查询服务
新增:
- `getWithCache()` 通用缓存方法 + singleflight + stale-while-revalidate
- Dashboard composite 聚合接口
- `MonitoringService` 注入 readPool/writePool
- L1 进程内缓存层
- 所有查询默认显式带 `run_mode` / `scope`
### Phase 17 修订:采集流水线
新增:
- Queue Redis Stream 任务队列替代内存队列
- `MonitoringProvider` 平台适配器注册表
- `PlatformRateLimiter` 每平台独立限流
- `PlatformBreaker` 熔断器
- 多 Pod 竞争消费架构
- 重试 + 容错逻辑
- `business_date` + backfill 流程
- `api_standard` / `api_search_grounded` 分流调度
### Phase 18 修订:聚合 Job
新增:
- 分布式锁选主
- 聚合后预热热门品牌缓存(替代全量删除)
- 并发 goroutine 聚合(10 并发)
- 聚合读走 PG Replica
- 晚到任务品牌级重聚合
- 引用事实表 → 日汇总表的聚合逻辑
### 新增 Phase 22: 基础设施
- Cache Redis Sentinel 部署配置
- Queue Redis Sentinel 部署配置
- PG 流复制 + Replica 配置
- Nginx 限流配置
- 监控告警配置(Prometheus + Grafana
- 估时:3 天
### 新增 Phase 23: 压测与验收
- 编写 k6 压测脚本(5 个压测场景)+ 1 个运维演练场景
- 数据准备脚本(100 品牌 × 30 天种子数据)
- 执行 Phase 1~4 验收流程
- 输出压测报告
- 估时:3 天
### 新增 Phase 24: 平台 PoC 与口径验收
- 3 品牌 × 30 问题 × 3 天的平台 PoC
- 验证搜索增强引用质量与波动
- 决定百度千帆是否进入首批上线白名单
- 估时:4 天
### 修订后总估时
| Phase | 内容 | V2 估时 | V3 估时 | 增量 |
| --- | --- | --- | --- | --- |
| 15 | DB 迁移 + 数据口径 | 1 天 | 3 天 | +2run_mode、business_date、任务/引用事实表) |
| 16 | 查询服务 + 缓存层 | 3 天 | 6 天 | +3singleflight、composite、L1、读写分离、scope 过滤) |
| 17 | 采集流水线 | 3 天 | 7 天 | +4Provider Registry、双 run_mode、Queue Redis、多 Pod、限流熔断) |
| 18 | 聚合 Job | 2 天 | 4 天 | +2(分布式锁、预热、并发聚合、backfill) |
| 19 | 种子数据 | 1 天 | 1 天 | 0 |
| 20 | 前端 6 页面 + ECharts | 4 天 | 4 天 | 0 |
| 21 | 前端验证 + 集成测试 | 1 天 | 1 天 | 0 |
| 22 | 基础设施(新增) | - | 3 天 | +3 |
| 23 | 压测与验收(新增) | - | 3 天 | +3 |
| 24 | 平台 PoC 与口径验收(新增) | - | 4 天 | +4 |
| **合计** | | **15 天** | **32 天** | **+17 天** |
---
## 9. V2 → V3 变更汇总
| 问题编号 | V2 问题 | V3 解决方案 |
| --- | --- | --- |
| **H0** | `run_mode` 未入模,标准回答与搜索增强回答混算 | 主统计按 `api_standard` / `api_search_grounded` 分层,默认页面显式取数规则 |
| **H1** | 读流量按 1 req/30s 估算,实际首页 6 个并行请求 | 重新建模 fan-out,引入 BFF composite 接口将首页降为 1 请求 |
| **H2** | TTL + 聚合后全删缓存,无防雪崩 | 4 层防护:singleflight + TTL 抖动 + stale-while-revalidate + L1 热点缓存 |
| **H3** | 5 worker、2 小时时间窗,采集跑不完 | 30 worker、6 小时窗口、Queue Redis 多 Pod 竞争消费 |
| **H4** | 问题版本未进入全部汇总唯一键 | `question_version_id` 进入问题级汇总唯一键,避免历史口径覆盖 |
| **H5** | 日采集与日聚合时序矛盾 | 使用 `business_date = yesterday` 的 T+1 发布模型 + backfill |
| **M1** | max_open_conns 50 被当作共享,实际 per-process | 显式规划每进程连接数,PG max_connections=160 |
| **M2** | 缺少压测验收 | 6 个场景模型 + SLA 指标 + k6 脚本 + 4 阶段验收流程 |
| **M3** | 分页缓存 key 缺 page_size | 所有分页 key 加入 `scope + page_size` 维度 |
| **M4** | 引用按原始 URL 聚合,易重复计数 | 引入 `monitoring_citation_facts`,按 `normalized_url/domain` 聚合 |
| **M5** | 在线缓存与离线队列共用 Redis 故障域 | Cache Redis / Queue Redis 拆分,分别高可用 |
| **新增** | 无降级容灾策略 | 双 Redis Sentinel + PG 读写分离 + 熔断器 + 降级措施矩阵 |
| **新增** | 无监控告警 | 7 项核心告警指标 + 阈值 |
-215
View File
@@ -1,215 +0,0 @@
# GEO 平台技术架构 V1 二次审查报告
> 基于更新后的 [geo-platform-ops-admin-tech-architecture-v1.md](file:///d:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md)1257 行),对照 `docs/refer` 中 8 个功能截图、两份 PRD 及 5 万并发用户目标进行审查。
---
## 一、前次缺失修复确认
| # | 前次严重缺失 | 修复状态 | 说明 |
| --- | --- | --- | --- |
| 1 | 定时调度器完全未设计 | ✅ 已修复 | §13.7.2 新增 `cmd/scheduler`,数据库到期扫描 + 行级锁防重 |
| 2 | Prompt 规则管理无后端模块 | ✅ 已修复 | §13.5.3 新增 `tenant/prompt-rule`,§13.11.2 新增 `prompt_rules` 表 |
| 3 | 模板管理后端模块缺失 | ✅ 已修复 | §13.5.3 新增 `tenant/template`,§13.11.2 新增 `article_templates` 表 |
| 4 | 数据库 Schema 缺失 | ✅ 已修复 | §13.11 提供 30+ 表的最小 Schema,含关键字段和索引 |
| 5 | GEO 指标采集方案未定义 | ✅ 已修复 | §13.9 定义完整的问题驱动监控模型 |
| 6 | 文章优化/版本管理缺失 | ✅ 已修复 | 新增 `optimization_tasks` + `article_versions` 表 |
| 7 | 浏览器插件通信协议缺失 | ✅ 已修复 | §13.8 定义插件会话模型 + 回调协议 |
| 8 | 绑定回调接口缺失 | ✅ 已修复 | §13.6.3 新增 `/api/callback/plugin/bind` |
| 9 | 竞品库管理缺失 | ✅ 已修复 | §13.11.3 新增 `competitors` 表 |
| 10 | 复合维度查询缺失 | ✅ 已修复 | §13.9 + §13.11.6 定义 `keyword_model_daily_metrics` 多维聚合 |
| 11 | 前端路由粒度不足 | ✅ 已修复 | §13.4 展开了完整路由 |
| 12 | SSE 细节不足 | ✅ 已修复 | §13.10 定义事件类型、负载格式、连接策略 |
> [!TIP]
> **前次审查中指出的 15 项问题已全部修复。** 文档从 774 行扩展到 1257 行,新增的 §13 闭环补齐方案质量很高。
---
## 二、功能完整性复查(对照 docs/refer 截图)
| 功能页 | 路由 | API | 数据模型 | 异步组件 | 评价 |
| --- | --- | --- | --- | --- | --- |
| 工作台 | ✅ | ✅ 聚合 API | ✅ | ✅ | **完备** |
| 模板创作 | ✅ | ✅ | ✅ `article_templates` | ✅ `worker-generate` | **完备** |
| 自定义生成 | ✅ | ✅ | ✅ `prompt_rules` `schedule_tasks` | ✅ `scheduler` + `worker-generate` | **完备** |
| 文章优化 | ✅ | ✅ | ✅ `optimization_tasks` `article_versions` | ✅ `worker-generate` | **完备** |
| 媒体管理 | ✅ | ✅ + 回调 | ✅ `plugin_sessions` `publish_records` | ✅ 插件 + `worker-publish-callback` | **完备** |
| 品牌词库 | ✅ | ✅ | ✅ 含问题版本 + 竞品 | ✅ `worker-analytics` | **完备** |
| 知识库 | ✅ | ✅ | ✅ 分组 + 多来源 | ✅ `worker-knowledge` | **完备** |
| 数据追踪 | ✅ | ✅ | ✅ 5 张聚合表 | ✅ `scheduler` + `worker-analytics` | **完备** |
> **结论:8 个功能页全部达到"页面 → 路由 → API → 对象 → 任务 → 存储 → 状态回传"的完整闭环。**
---
## 三、面向 5 万并发用户的技术问题
以下是基于"5 万人同时使用 + 足够简单"目标的严格审查:
### 🔴 必须解决的问题
#### 1. Gateway 单点瓶颈 — 缺少水平扩展方案
文档中 `cmd/gateway` 是唯一入口,但未说明:
- Gateway 是否无状态可水平扩展(多实例部署)
- 前端连接的负载均衡策略(L4/L7)
- Gateway 与 `tenant-api` / `platform-api` 之间的通信方式(直连 or 服务发现)
> 5 万并发用户假设平均每人每 10 秒发一次请求 = **5000 QPS**。单个 Go 进程可以处理,但 SSE 长连接会占用大量连接数。
**建议**:明确 Gateway 无状态 + Kubernetes HPA 策略 + Nginx/Ingress L7 负载均衡。
#### 2. SSE 连接数问题 — 5 万长连接的资源消耗
§13.10 设计了 SSE 推送,但 5 万用户全部保持 SSE 长连接意味着:
- **5 万个 TCP 连接同时保活**
- 每个连接 15s heartbeat = **3333 条/秒** 仅心跳
- 内存占用:每连接约 10-50KB → 约 **500MB - 2.5GB** 仅连接缓冲
**建议**
- V1 默认使用 **轮询**(10s 间隔),仅在执行长任务时临时升级为 SSE
- 或者只在活跃操作页面(文章生成中、发布中)才建立 SSE,非活跃页面走轮询
- 明确 SSE 连接上限和优雅关闭策略
#### 3. Scheduler 单实例风险
§13.7.2 描述 Scheduler 使用数据库行级锁防重复派发,但:
- 单 Scheduler 实例是单点故障
- 行级锁在高并发下有锁竞争风险
**建议**
- V1 可接受单实例 + 进程级健康检查自动重启
- 但需要明确:如果 Scheduler 宕机 5 分钟,到期任务是否会堆积?重启后是否能自动追回?
- 文档中应补充 `Scheduler 故障恢复策略`
#### 4. PostgreSQL 单库承压
30+ 张表全部在一个 PostgreSQL 实例中,5 万用户场景下:
- `articles` 表增长最快(每用户每天可能生成 5-20 篇)
- `question_monitor_runs``问题数 × 模型数 × 天数` 增长
- `audit_logs` 写入量大,查询范围广
**建议**
- 审计日志表按月分区(`PARTITION BY RANGE (created_at)`
- 监控数据表按日期分区
- `articles` 添加 `tenant_id` 分区索引或按 tenant 分片策略
- 明确 V1 阶段的数据量估算和归档策略
#### 5. 缺少限流与熔断设计
5 万用户中如果某个租户突发大量请求(如批量生成 1000 篇文章),可能打爆队列。文档只在 §7.2 提到 Redis 限流计数,但缺乏系统性设计:
**建议**
- 在 Gateway 层增加租户级限流(令牌桶/滑动窗口)
- 在 Worker 层增加模型调用限频(保护下游 LLM API)
- 在 MQ 层设置队列容量上限 + 拒绝策略
- 生成任务提交时校验排队深度,超阈值直接拒绝并提示
---
### 🟡 建议优化的问题
#### 6. 对象存储缺少 CDN 加速策略
知识库的原始文档和图片素材通过对象存储管理(§7.5),但 5 万用户同时访问文章预览中的图片时:
- 需要 CDN 分发(阿里云 OSS 天然支持,MinIO 需手动配 Nginx/CDN
- 建议文档中明确:线上版走 OSS CDN,私有化部署走 Nginx 反代 MinIO
#### 7. Redis 缓存雪崩风险
§13.9.2 提到 Redis 缓存 30 天摘要,但未设计:
- 缓存预热策略
- 缓存过期时间错开(避免集体失效)
- 缓存穿透保护(布隆过滤器或空值缓存)
**建议**:关键缓存(工作台统计、趋势图)使用 `TTL + 异步刷新` 模式,避免大量请求击穿到 PostgreSQL。
#### 8. `worker-generate` 职责过重
§13.5.2 中 `worker-generate` 同时承载:模板生成、Prompt 生成、批量生成、文章优化。这意味着:
- 一个 Worker 进程要处理 4 种不同的任务类型
- 批量生成可能堵塞优化任务
**建议**
- 队列内使用 `routing key``priority` 区分任务类型
- 或拆分为 `q.generate.interactive`(即时任务,高优先级)和 `q.generate.batch`(批量/定时任务,低优先级)
- V1 在代码层面保持一个 Worker 二进制,但消费端可部署多实例分配不同 routing key
#### 9. Sponge 框架兼容性未评估
§2.2 提到使用 [sponge](https://github.com/go-dev-frame/sponge) 框架,但文档中的分层架构(transport → app → domain → repository)与 sponge 的代码生成模式是否兼容未说明:
- sponge 生成的代码结构是否支持 `internal/tenant` + `internal/platform` 的双域拆分?
- sponge 的 handler/service/dao 分层与文档中的 transport/app/domain/repository 如何映射?
**建议**:明确 sponge 的使用范围(仅脚手架?还是深度依赖代码生成?),避免框架约束与架构设计冲突。
我的意见:优先架构设计为先,sponge 只是为了快速生成代码,但是一定架构设计为先
#### 10. 缺少全局配置管理方案
文档提到 `internal/shared/config`,但未说明:
- 配置从哪里加载(文件?环境变量?配置中心?)
- 运行时配置变更是否支持热加载
- 多个服务(gateway / tenant-api / platform-api / scheduler / workers)的配置如何统一管理
**建议**V1 使用 `YAML 配置文件 + 环境变量覆盖`,明确配置层级。
是用动态配置加载技术
---
### 🟢 文档层面的小问题
#### 11. §5.5.9 `platform/audit` 内容空缺
第 406-407 行 `platform/audit` 只有标题没有内容,而 §13 中才补齐。应删除或填充。
#### 12. §5.6.3 两服务协作原则有"悬挂列表"
第 436-439 行的 4 个日志类型列表与上文语义断裂,像是从 `platform/audit` 模块复制过来的遗留。
#### 13. `viewer` 角色在 §6.2.2 被删除
原版有 `viewer`(只读成员),更新版替换为 `kol`。但 PRD 中明确定义了"只读成员"角色,建议保留 `viewer` 并额外增加 `kol`
#### 14. Mermaid 图中缺少 `scheduler` 节点
§8 交互图(第 553-599 行)未包含 `scheduler`,虽然 §13.5.1 补了修订图,但原图应同步更新或标注以修订图为准。
#### 15. 后端结构图(§9)缺少 `scheduler` 节点
第 603-691 行的 Mermaid 图中 cmd 子图也缺少 scheduler 二进制。
---
## 四、面向"足够简单"目标的评估
| 维度 | 评价 | 风险点 |
| --- | --- | --- |
| 服务数量 | ✅ **合理** — 8 个进程 | 足够收敛,V1 可控 |
| 中间件数量 | ⚠️ **偏多** — PG + Redis + RabbitMQ + Qdrant + OSS | 但都是必需的,无冗余 |
| 数据库表 | ⚠️ **30+ 张** | 但每张表职责单一,不算过度 |
| 框架选型 | ⚠️ Sponge 框架 | 需确认不会引入额外复杂度 |
| 部署复杂度 | ✅ Docker Compose 可覆盖本地开发 | 生产环境需 K8s |
| 前端应用数 | ✅ 2 + 1 插件 | 合理 |
> [!IMPORTANT]
> **总体评价:架构已从"宏观方案"升级为"可开发闭环方案"。功能覆盖度 100%,数据模型完整。主要残留问题集中在 5 万并发的性能架构层面(SSE 连接管理、数据库分区、限流熔断)和文档一致性上。**
---
## 五、修改优先级建议
| 优先级 | 修改项 | 工作量 |
| --- | --- | --- |
| P0 | 补充 Gateway 水平扩展和负载均衡方案 | 0.5h |
| P0 | 修订 SSE 策略:默认轮询、长任务页面才开 SSE | 0.5h |
| P0 | 补充租户级限流 + 队列容量保护设计 | 1h |
| P1 | 补充 PostgreSQL 分区策略(审计/监控表) | 0.5h |
| P1 | 恢复 `viewer` 角色 + 增加 `kol` | 5min |
| P1 | 评估 sponge 框架与双域分层的兼容性 | 1h |
| P2 | 统一旧图与新图(§8/§9 补 scheduler | 15min |
| P2 | 修复 §5.5.9 空内容 + §5.6.3 悬挂列表 | 10min |
| P2 | 补充配置管理方案 | 0.5h |
| P2 | 补充对象存储 CDN 方案 | 15min |
-318
View File
@@ -1,318 +0,0 @@
# GEO 平台技术架构深度审查报告(第三轮)
> 在前两轮审查(功能覆盖度 + 5 万并发初审)基础上,交叉审查全部 6 份文档(共 ~4700 行),聚焦**跨文档一致性、数据流完整性、实施风险**三个维度。
---
## 一、跨文档一致性审查
现有文档体系:
| 文档 | 行数 | 定位 |
| --- | --- | --- |
| [geo-platform-ops-admin-tech-architecture-v1.md](file:///d:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md) | 1257 | 🏗️ 主架构 |
| [geo-platform-prd-v1.md](file:///d:/project/geo/docs/geo-platform-prd-v1.md) | 1079 | 📋 租户端 PRD |
| [geo-platform-ops-admin-prd-v1.md](file:///d:/project/geo/docs/geo-platform-ops-admin-prd-v1.md) | 652 | 📋 平台管理端 PRD |
| [media-binding-and-publish-v1.md](file:///d:/project/geo/docs/media-binding-and-publish-v1.md) | 562 | 🔌 插件方案 |
| [qdrant-rag-architecture-v1.md](file:///d:/project/geo/docs/qdrant-rag-architecture-v1.md) | 651 | 🧠 RAG 方案 |
| [question-driven-monitoring-design-v1.md](file:///d:/project/geo/docs/question-driven-monitoring-design-v1.md) | 495 | 📊 数据监控方案 |
### 问题 1:RAG 组件未纳入主架构图和交互图
[qdrant-rag-architecture-v1.md](file:///d:/project/geo/docs/qdrant-rag-architecture-v1.md) 定义了完整的检索链路:`query rewrite → filter → Qdrant 检索 → rerank → 上下文拼装 → 提交模型`。但主架构中:
- §8 交互图中 `worker-generate → Qdrant` 的箭头缺失(只有 `worker-knowledge → QD`
- 文章生成链路(§10.1)只提到"调用模型"**没有 RAG 检索步骤**
- 后端模块职责中无 `shared/rag``shared/retrieval` 模块
> [!WARNING]
> **影响**`worker-generate` 生成文章时需要从 Qdrant 检索品牌知识,这条数据流在主架构中完全不可见。开发时容易遗漏 RAG 集成。
>
> **建议**
> 1. 交互图中为 `worker-generate → Qdrant` 增加箭头
> 2. §10.1 生成链路在步骤 3 和 4 之间插入 "RAG 检索品牌知识上下文"
> 3. 后端目录中增加 `internal/shared/retrieval` 模块
### 问题 2media-binding 插件协议与主架构定义存在分歧
| 对比项 | media-binding-v1.md | 主架构 §13.8 |
| --- | --- | --- |
| 绑定落库 | 前端调用后端 API 保存 | 插件**双通道**:回传前端 + 回调 API 落库 |
| 会话管理 | 无 `plugin_session` 概念 | 有 `plugin_sessions` 表 + `session_token` |
| 回调路径 | `POST /api/platform-accounts/bind` | `POST /api/callback/plugin/bind` |
| 发布记录 | `POST /api/publish-records` | `POST /api/callback/plugin/publish` |
> **结论**:主架构 §13.8 的设计更完善(双通道 + 会话令牌 + 审计),但 media-binding-v1.md 还是旧方案。**两份文档需要统一**,否则前端、插件、后端开发人员会看不同的接口文档。
### 问题 3:数据追踪的数据模型字段不一致
| 对比项 | question-driven-monitoring-v1.md | 主架构 §13.11.6 |
| --- | --- | --- |
| 汇总表名 | `question_model_daily_metrics` | 未出现此表(但有 `keyword_model_daily_metrics` |
| `question_monitor_runs` 字段 | 有 `run_at``task_batch_id` | 缺少 `run_at``task_batch_id` |
| `question_monitor_parse_results` 字段 | 有 `citation_platform_count``parser_version` | 缺少这两个字段 |
| 保留策略 | 明确 30/90/180 天分级 | 未提及保留策略 |
> **建议**:主架构 §13.11.6 的 Schema 应与 [question-driven-monitoring-design-v1.md](file:///d:/project/geo/docs/question-driven-monitoring-design-v1.md) 完全对齐,特别是补充 `question_model_daily_metrics` 表和缺失字段。
### 问题 4RAG 中的 `knowledge_chunks_meta` 表未出现在主架构 Schema
[qdrant-rag-architecture-v1.md](file:///d:/project/geo/docs/qdrant-rag-architecture-v1.md) §6.1 提到 PostgreSQL 需保存 `knowledge_chunks_meta`(chunk 元数据和状态),但主架构 §13.11.4 知识库 Schema 中**没有这张表**。这意味着:
- 每个文档被切成多少 chunk、每个 chunk 的 token 数、status 在 PostgreSQL 中无处存储
- chunk 重建/版本化时没有元数据支持
> **建议**:在 §13.11.4 增加 `knowledge_chunks_meta` 表:
> ```
> | knowledge_chunks_meta | id, knowledge_item_id, chunk_index, token_count, qdrant_point_id, item_version, status |
> ```
### 问题 5RAG 中的 `retrieval_logs` 和 `rag_eval_cases` 未纳入主架构
RAG 文档要求记录每次检索的 query/filter/rerank 结果用于 badcase 分析,但主架构没有对应表和模块。V1 如果不记录,后续无法评估 RAG 精准度。
> **建议**:如果目标是"足够简单",V1 可以先将检索日志写入**文件日志**而非数据库表,后续再升级。在架构中明确这一策略。
### 问题 6PRD 中定义了 `kol` 角色但无实际权限描述
主架构 §6.2.2 新增 `kol`(用于某个专家的 prompt 从底层共享),但:
- 租户端 PRD 只定义了 管理员/编辑/只读 三种角色
- `kol` 角色的具体权限范围(能看什么、能做什么)未在任何文档中定义
- PRD 中的 `viewer`(只读成员)角色被删除
> **建议**:保留 `viewer`、额外增加 `kol`,并在权限矩阵中补充 `kol` 的能力边界。
### 问题 7`article_templates` 表的 `tenant_id` 字段暗示模板是租户级的
主架构 §13.11.2 中 `article_templates``tenant_id`。但:
- 平台端 PRD §8.5 明确有"系统模板管理"功能
- 平台级模板应 `tenant_id = NULL` 或专门的 `scope` 字段
- 参考截图中工作台的模板卡片(Top X / 产品评测 / 研究报告)看起来是**平台级**预设
> **建议**`article_templates` 表增加 `scope` 字段(`platform` / `tenant`),平台级模板 `tenant_id = NULL`,租户可在此基础上自定义。
### 问题 8ops-admin-web PRD 中的"平台接入管理"路由在主架构 §13.4.2 中已有但 §5.3.3 中缺失
§13.4.2 ops-admin-web 路由有 `/platform/integrations/*`,§13.5.4 模块也有 `platform/integration`,但 §5.3.3 原始 API 路由列表中没有 `/api/platform/integrations/*`
> **影响较小**,因为 §13.6.2 覆盖了。但前后不一致容易造成混乱。
---
## 二、关键数据流深度审查(面向 5 万并发 + 简单)
### 数据流 1:文章生成全链路
```
用户 → admin-web → gateway → tenant-api(额度校验 + 任务入队)
→ RabbitMQ → worker-generateRAG 检索 + 模型调用 + 结果存储)
→ PostgreSQL + Redis → SSE/轮询 → 前端更新
```
**关键问题:LLM 调用超时与成本**
- 一篇文章生成可能调用 LLM 3-20 秒(取决于文章长度和模型)
- 5 万用户中假设 1% 同时在生成 = **500 并发生成任务**
- 每个任务占用 1 个 Worker goroutine + 1 个 LLM API 连接
- 如果 LLM 上游限频(如 DeepSeek 300 RPM),Worker 会排队堆积
> [!CAUTION]
> **架构中缺少**
> 1. LLM 调用超时策略(多久算超时?超时后任务状态如何?)
> 2. LLM 上游限流适配(令牌桶/重试退避)
> 3. 生成任务的优先级控制(VIP 租户优先?)
> 4. Worker 对 LLM 的并发连接池设计
### 数据流 2:知识库入库全链路
```
用户上传 → tenant-api → 对象存储 → RabbitMQ
→ worker-knowledge(提取 → 清洗 → 切片 → Embedding → Qdrant 写入 + PG 元数据)
```
**关键问题:Embedding 调用成本**
- 每个文档可能切出 50-200 个 chunk
- 每个 chunk 需要一次 Embedding API 调用
- 50 万租户 × 平均 10 个知识文档 = 500 万文档 → 潜在 2.5-10 亿 chunk
> 但 V1 阶段用户量有限,这主要是演进方向的考量。当前建议在架构中明确 **Embedding 批量调用**策略(一次请求多个 chunk),减少 API round-trip。
### 数据流 3:定时任务调度全链路
```
scheduler 每分钟扫描 → schedule_tasks(到期的) → 生成 generation_tasks → 入队 RabbitMQ
→ 生成 analytics 任务 → 入队 RabbitMQ
```
**关键问题:调度雷群效应**
- 很多租户的定时任务可能设在同一时刻(如每天 08:00)
- 某分钟可能扫描出数千个到期任务
- 全部同时入队可能瞬间打爆 `q.generate` 队列
> **建议**
> - Scheduler 支持 **平滑派发**(将同时到期的任务分散到 5-10 分钟窗口内入队)
> - 或租户创建定时任务时自动 **加随机偏移量**(±15 分钟)
### 数据流 4:数据追踪采集全链路
```
scheduler → 为每个启用的 question_version × model 生成采集任务 → RabbitMQ
→ worker-analytics(调用模型 → 解析回答 → 存解析结果)
→ 日级聚合 → 刷新缓存
```
**关键问题:采集任务与生成任务共享模型调用**
- 数据追踪的采集本质上也是"调用 LLM 获取回答"
- 与文章生成共用 LLM 额度/限频
- 但采集任务优先级应**低于**用户直接触发的生成任务
> **建议**
> - 采集任务使用独立的 LLM 通道/频次限制
> - 或在共享通道中标记优先级,用户直接任务优先
### 数据流 5:插件发布全链路
```
admin-web → 创建 plugin_session → browser-extension 执行发布
→ 同时:回传前端(即时反馈)+ 回调 /api/callback/plugin/publish(可靠落库)
→ tenant-api 存储 publish_records → SSE 广播
```
**问题:插件离线时的降级处理**
- 如果用户没装插件、或插件版本过低,点击"发布"会发生什么?
- 主架构中未定义**插件检测**机制(前端如何知道插件是否可用?)
- media-binding-v1.md §12 提到只注入本系统后台域名,但没定义版本检测协议
> **建议**:前端在页面加载时检测插件安装状态和版本,未安装或版本过低时灰掉发布按钮并引导安装。在 `packages/shared-types` 中定义最低兼容插件版本常量。
---
## 三、实施风险与工程注意事项
### 风险 1`worker-generate` 中 LLM 超时导致 goroutine 泄漏
Go 中如果 HTTP 请求到 LLM API 设置了很长的超时(如 60s),大量排队任务会占用大量 goroutine。如果 LLM 侧故障,可能导致 goroutine 数量爆炸。
> **建议**
> - 对 LLM 调用设置 `context.WithTimeout`(建议 30s
> - Worker 设置最大并发 goroutine 数(如基于 `semaphore` 限制并发 50
> - 超时任务标记为 `failed`,进入重试队列
### 风险 2PostgreSQL 事务边界不清晰
生成任务涉及多表写入:`articles` + `article_versions` + `generation_tasks` + `tenant_quota_ledgers`。如果不在同一事务中:
- 额度扣了但文章没写入 → 额度泄漏
- 文章写了但额度没扣 → 超额使用
> **建议**:文档中明确关键事务边界——至少"额度扣减 + 任务创建"必须在同一 PostgreSQL 事务中。Worker 侧的"结果写入 + 额度确认"也应事务保护。
### 风险 3migration 策略缺失
30+ 张表的 Schema 定义好了,但没有 migration 策略:
- 如何管理 Schema 版本?(golang-migrate / Atlas / sponge 内置?)
- 线上 DDL 变更如何无停机执行?
- 是否有 down migration
> **建议**:明确使用 `golang-migrate` 或 `Atlas`,并在 `server/migrations/` 目录约定命名规则。
### 风险 4`tenant_id` 数据隔离的执行保障
5 万用户意味着大量租户。所有租户数据共库共表,`tenant_id` 过滤是唯一隔离手段。如果某个查询漏加 `tenant_id`,就是数据泄露。
> **建议**
> - 在 repository 层封装 `TenantScope(ctx)` 自动注入 `WHERE tenant_id = ?`
> - 在 CI 中运行静态分析检查所有查询是否带 `tenant_id`
> - 文档中将此列为**强制编码规范**
### 风险 5:对象存储的清理策略
知识库文档、原始模型回答、发布请求/响应 payload 持续写入对象存储。无清理策略意味着存储成本线性增长。
> **建议**
> - 已删除知识文档的对象存储文件应在 T+7 天后清理
> - 原始模型回答按 `question-driven-monitoring` 建议保留 30-90 天
> - 定期运行 GC 任务扫描"孤儿文件"
### 风险 6Sponge 框架的 code-gen 与手写代码冲突
Sponge 的核心卖点是代码生成(从 protobuf/sql 生成 handler/service/dao)。但主架构的分层是自定义的 `transport/app/domain/repository`。如果使用 sponge 的代码生成:
- 生成的代码放在哪个层?
- 手写的 domain 逻辑如何与生成的 dao 共存?
- 重新生成时是否覆盖手写代码?
> **建议**:明确 sponge 仅用于**脚手架初始化**(项目初始化、数据库连接、中间件配置),**不使用**其默认的 handler/service/dao 代码生成,由团队按自定义分层手写业务代码。
### 风险 7SSE 与 Kubernetes HPA 的兼容性
Kubernetes 默认负载均衡(基于 Service → Pod 的 round-robin)会将新请求分配到不同 Pod。但 SSE 是长连接,如果用户的 SSE 连接在 Pod A,但任务结果写入后通知只发到 Pod B,用户收不到。
> **建议**
> - SSE 推送使用 **Redis Pub/Sub** 作为跨 Pod 广播层
> - 或使用 **Sticky Session**Ingress `nginx.ingress.kubernetes.io/affinity: cookie`
> - 文档中需要明确选择哪种方案
### 风险 8`brand_question_versions` 的碎片化
每次用户修改问题文案就新增一个版本。如果用户频繁微调(改错字、加时间等),版本表会快速碎片化。监控计划如果绑定 `question_version_id`,可能产生大量只使用 1-2 天的版本。
> **建议**
> - 对问题修改增加 **debounce**(如 10 分钟内多次修改只产生一个版本)
> - 或者只在"发布/启用"时才生成新版本,编辑中间态不算版本
### 风险 9:批量生成的额度预扣与回滚
用户提交"批量生成 100 篇"时:
- 是预扣 100 次额度?
- 还是每篇生成成功时扣 1 次?
- 如果预扣后有 30 篇生成失败,如何退回额度?
> **建议**:文档应明确**额度策略**:推荐"提交时预扣 + 失败后按条退还 + 定期对账"模式。
### 风险 10`viewer` 角色缺失导致 PRD 不满足
PRD 明确定义了"只读成员"角色,但主架构删除了 `viewer` 替换为 `kol`。如果按当前架构开发:
- "只读成员"无法创建——PRD 交付不通过
- 前端权限配置缺少对应的角色枚举
---
## 四、"足够简单"维度的优化建议
| 可简化选项 | 当前方案 | 简化建议 | 风险 |
| --- | --- | --- | --- |
| SSE 推送 | 全场景 SSE | **V1 全部用轮询**10s),V1.5 引入 SSE | 延迟略高但架构极度简化 |
| Gateway 独立进程 | `cmd/gateway` | **V1 把网关逻辑合并到 tenant-api 的中间件**,省一个进程 | 后续拆分需重构 |
| Rerank 模块 | cross-encoder reranker | **V1 跳过 rerank**,只用 dense + filter + top5 | RAG 精度降低 |
| 独立 Scheduler | `cmd/scheduler` | **用 cron job 或 pg_cron** 代替独立进程 | 灵活性降低 |
| `question_model_daily_metrics` | 问题级汇总表 | **V1 只保留 `keyword_model_daily_metrics`** | 丢失问题级穿透能力 |
> [!NOTE]
> 以上是**可选简化**,不是必须。如果团队人力充足,保持当前设计无问题。但如果"足够简单"是硬约束,可以选择部分简化。
---
## 五、修改建议汇总
### P0(必须修复)
| # | 问题 | 预计工作量 |
| --- | --- | --- |
| 1 | 主架构中补充 RAG 检索步骤(交互图 + 生成链路 + 模块定义) | 30min |
| 2 | 统一 media-binding-v1.md 与主架构 §13.8 的接口定义 | 1h |
| 3 | 补充 LLM 调用超时/限流/并发池设计 | 30min |
| 4 | 明确关键事务边界(额度扣减 + 任务创建 + 结果写入) | 15min |
| 5 | 恢复 `viewer` 角色 + 定义 `kol` 角色权限 | 15min |
### P1(重要补充)
| # | 问题 | 预计工作量 |
| --- | --- | --- |
| 6 | §13.11.4 补充 `knowledge_chunks_meta` 表 | 10min |
| 7 | §13.11.6 与 question-driven-monitoring 对齐字段 | 20min |
| 8 | `article_templates` 增加 `scope` 字段区分平台/租户模板 | 5min |
| 9 | 补充 SSE 跨 Pod 广播方案(Redis Pub/Sub 或 Sticky Session | 20min |
| 10 | 补充额度预扣/退还/对账策略 | 15min |
-242
View File
@@ -1,242 +0,0 @@
# GEO 平台技术架构 V1 综合审查报告(第四轮)
> 以大厂技术架构师视角,综合前三轮 Claude 审查结论,对 [geo-platform-ops-admin-tech-architecture-v1.md](file:///d:/project/geo/docs/geo-platform-ops-admin-tech-architecture-v1.md) 进行终审,聚焦 **可实施性** 和 **运维简单性**。
---
## 一、前三轮审查问题收敛确认
### 第一轮 → 第二轮(功能闭环修复)
| # | 问题 | 状态 |
| --- | --- | --- |
| 1 | 定时调度器缺失 | ✅ 已补齐 `cmd/scheduler` |
| 2 | Prompt 规则管理缺失 | ✅ 已补齐模块 + 表 |
| 3 | 模板管理缺失 | ✅ 已补齐 `article_templates` |
| 4 | 数据库 Schema 缺失 | ✅ 已补齐 30+ 张表 |
| 5 | GEO 指标采集方案缺失 | ✅ 已补齐问题驱动监控 |
| 6-12 | 版本管理 / 插件协议 / SSE / 路由 | ✅ 全部修复 |
### 第二轮 → 第三轮(5 万并发 + 一致性修复)
| # | 问题 | 状态 |
| --- | --- | --- |
| 1 | Gateway 扩展方案 | ✅ §13.13.1 补充了 HPA + L7 负载均衡 |
| 2 | SSE 连接策略 | ✅ §13.10.3 改为默认轮询 + 按需 SSE |
| 3 | Scheduler 故障恢复 | ✅ §13.13.2 补充了恢复策略 |
| 4 | PostgreSQL 分区 | ✅ §13.13.3 补充了审计/监控表分区 |
| 5 | 限流 + 熔断 | ✅ §13.13.4 补充了租户级令牌桶 + Worker 并发池 |
| 6 | 配置管理方案 | ✅ §13.13.6 补充了三级配置 |
| 7 | Sponge 使用原则 | ✅ §13.13.7 明确仅作脚手架 |
| 8 | Migration 策略 | ✅ §13.13.8 补充了 golang-migrate |
| 9 | 租户隔离保障 | ✅ §13.13.9 补充了 TenantScope |
### 第三轮提出的残留问题
| # | 问题 | 当前状态 |
| --- | --- | --- |
| 1 | RAG 检索未纳入主架构图 | ✅ §13.5.1 修订图已补 `WG → QD` 箭头;§5.5.2 新增 `shared/retrieval` |
| 2 | media-binding 接口不一致 | ⚠️ **仍残留** — media-binding-v1.md 未同步更新 |
| 3 | 数据追踪 Schema 字段不一致 | ✅ §13.11.6 已与监控方案对齐 |
| 4 | `knowledge_chunks_meta` 表缺失 | ✅ §13.11.4 已补充 |
| 5 | `retrieval_logs` 落地策略 | ✅ §13.11.4 末尾明确 V1 写文件日志 |
| 6 | `kol` 角色权限不清 | ✅ §6.2.3 补充了权限矩阵 |
| 7 | `article_templates.scope` | ✅ §13.11.2 已补充 scope 字段 |
| 8 | LLM 超时 / 限流 / 并发池 | ✅ §13.13.4 明确 30s 超时 + semaphore 50 |
| 9 | 事务边界 | ✅ §13.13.9 明确了关键事务边界 |
| 10 | 额度预扣/退还策略 | ✅ §13.7.3 明确了预扣 + 失败退还 + 对账 |
> [!TIP]
> **三轮审查共计约 40 项问题,当前仅剩 1 项残留(media-binding 文档未同步)。架构整体完成度非常高。**
---
## 二、当前架构总体评价
### 2.1 架构复杂度评级
| 维度 | 现状 | 评价 |
| --- | --- | --- |
| 进程数量 | gateway + 2 API + scheduler + 4 worker = **8 个** | ⚠️ 偏多但合理 |
| 中间件 | PG + Redis + RabbitMQ + Qdrant + OSS = **5 个** | ⚠️ 每个都必要,无冗余 |
| 数据库表 | **30+ 张** | ✅ 职责单一,不算过度 |
| 前端应用 | 2 后台 + 1 插件 | ✅ 合理 |
| 部署形态 | Docker Compose(开发)+ K8s(生产) | ✅ 标准 |
### 2.2 可实施性判断
> [!IMPORTANT]
> **结论:可实施。** 文档已覆盖从页面路由 → API → 数据模型 → 异步链路 → 部署的完整闭环。8 个功能页全部具备"可开发"条件。
**但有以下前提条件:**
1. 团队至少需要 2 名 Go 后端 + 1 名前端 + 1 名 DevOps
2. LLM/Embedding 供应商的限频和成本需提前确认
3. 首版上线建议先控制在 **1000 并发** 内验证,而非直接冲 5 万
---
## 三、本轮发现的问题与建议
以下是前三轮未充分覆盖、但作为架构师我认为必须关注的问题:
### 🔴 必须修正(影响运维简单性或可实施性)
#### 问题 1V1 阶段 Gateway 存在过度设计的嫌疑
当前方案 `cmd/gateway` 作为独立进程做路由分发。但 V1 只有 2 个下游(tenant-api、platform-api),且 gateway 只做鉴权 + 路由转发。
**运维影响**:多一个进程 = 多一套部署/监控/日志/重启策略。
**建议**
- **V1 简化方案**:将 gateway 的鉴权中间件合并到 `tenant-api``platform-api` 中,由 Nginx/Ingress 按 `/api/tenant/*``/api/platform/*` 做 L7 路由分发
- 省掉 1 个进程,运维直接减负
- V1.5 如果需要统一限流/审计注入,再独立出 gateway
> [!WARNING]
> 如果选择保留 gateway,则必须确保它"足够薄"——只做路由 + 鉴权 + 限流,**绝不放业务逻辑**。否则它会变成系统的"上帝对象"。
我的想法是保留的,但第一版,只做路由 + 鉴权 + 限流
#### 问题 2:8 个进程的运维负担
生产环境需要管理 8 个独立二进制(gateway + tenant-api + platform-api + scheduler + 4 worker),每个都需要:
- 独立的 Dockerfile
- 独立的 K8s Deployment + Service
- 独立的日志采集
- 独立的健康检查
**建议**
- 4 个 Worker 在 V1 阶段考虑合并为 **2 个 Worker 二进制**
- `worker-main`:消费 generate + knowledge + analytics 队列(按 routing key 区分)
- `worker-callback`:消费 publish-callback 队列
- 代码层面保持模块隔离(`internal/jobs/` 下仍然分独立 package),只是 `cmd/` 入口合并
- 部署从 8 个进程降为 **5 个**tenant-api + platform-api + scheduler + worker-main + worker-callback
审核员想法: 不采纳,合并后系统用户体验会变差会更慢
#### 问题 3`viewer` 角色仍未恢复
§6.2.2 中只有 `tenant_admin` / `editor` / `kol`。PRD 明确定义了"只读成员"。
审核员意图:viewer 角色没用,删除;kol其实也只是用户,但是这个角色比普通用户要更会写 prompt,别人可以直接使用她的分享码,使用她分享的prompt(其中主要信息,比如公司名啊,地址啊,还是得用户填)
---
### 🟡 建议优化(提升运维效率)
#### 问题 4:缺少统一的错误码规范
文档定义了 40+ 个 API 接口,但没有统一的错误码体系。前后端联调时会出现:
- 后端返回 `{"error": "xxx"}` 格式不统一
- 前端无法区分"额度不足"和"LLM 超时"和"参数错误"
**建议**:定义统一错误响应和错误码段:
```
{
"code": 40301,
"message": "quota_insufficient",
"detail": "生成额度不足,请升级套餐"
}
```
| 错误码段 | 含义 |
| --- | --- |
| 400xx | 参数校验 |
| 401xx | 认证 |
| 403xx | 权限/额度 |
| 404xx | 资源不存在 |
| 409xx | 状态冲突 |
| 500xx | 内部错误 |
| 503xx | 下游不可用(LLM/Qdrant |
#### 问题 5:缺少日志规范与链路追踪
8 个进程的分布式架构,单个请求可能跨 gateway → API → MQ → Worker。排查问题需要:
- 统一的 `request_id` / `trace_id` 贯穿全链路
- 结构化日志(JSON 格式)
- 日志级别规范
`internal/shared/observability` 模块已定义在目录结构中,但文档中没有展开。
**建议**
- 所有进程使用统一日志库(如 `zap`
- gateway 生成 `request_id`,全链路透传
- MQ 消息 header 中携带 `trace_id`
- 使用 Jaeger
#### 问题 6Docker Compose 本地开发的启动顺序和依赖
5 个中间件 + 8 个服务的本地开发环境,`docker-compose up` 时:
- PG 未就绪时 API 连接失败
- RabbitMQ 未初始化队列时 Worker 报错
- Qdrant 启动慢导致知识库功能不可用
**建议**
- `docker-compose.yml` 中只放中间件(PG/Redis/RabbitMQ/Qdrant/MinIO
- Go 服务本地 `go run` 开发,用 `Makefile``Task` 统一启动
- 提供一键初始化脚本:`make dev-init`(创建队列、运行 migration、创建 bucket
#### 问题 7media-binding-v1.md 仍与主架构不一致
第三轮审查已指出,至今未同步。
**建议**:以主架构 §13.8 的插件会话模型为准,更新 media-binding-v1.md 的接口定义。
---
### 🟢 小问题(不阻塞但建议修正)
| # | 问题 | 说明 |
| --- | --- | --- |
| 1 | §8 旧交互图与 §13.5.1 修订图并存 | 建议删除旧图或标注"以 §13 为准" |
| 2 | §9 后端结构图缺 scheduler 节点 | 与实际设计不符 |
| 3 | §5.5.9 `platform/audit` 只有标题无内容 | 已在 §13 补齐,建议填充或删除旧节 |
| 4 | Vite 8 版本号存疑 | §2.2 提到 `Vite 8`,截至 2026.3 Vite 最新稳定版需确认 |
---
## 四、运维简单性终审
### 4.1 运维维度打分
| 维度 | 评分 | 说明 |
| --- | --- | --- |
| 部署复杂度 | ⭐⭐⭐ | 8 个进程偏多,建议合并 Worker |
| 监控难度 | ⭐⭐⭐ | 有 observability 模块但未展开 |
| 故障排查 | ⭐⭐ | 缺少 trace_id + 结构化日志规范 |
| 扩容简易度 | ⭐⭐⭐⭐ | gateway/API 无状态,HPA 就绪 |
| 配置管理 | ⭐⭐⭐⭐ | 三级配置合理 |
| 数据库运维 | ⭐⭐⭐ | 分区策略已定义,缺归档自动化 |
### 4.2 V1 运维简化建议清单
| # | 简化措施 | 效果 |
| --- | --- | --- |
| 1 | 合并 4 Worker 为 2 个 | 减少 2 个部署单元 |
| 2 | 合并 Gateway 到 Ingress 路由 | 减少 1 个部署单元 |
| 3 | 统一日志 + request_id | 大幅降低排查成本 |
| 4 | 提供 `make dev-init` | 新人 10 分钟启动开发环境 |
| 5 | 自动化 migration CI | 避免手动 DDL 事故 |
| 6 | 审计/监控表自动归档脚本 | 避免磁盘满 |
---
## 五、最终结论
> [!IMPORTANT]
> ### 可实施性:✅ 通过
>
> 文档已从"宏观方案"演进为可直接进入开发的"闭环方案"。8 个功能页全部具备路由 → API → 模型 → 异步 → 存储 → 状态回传的完整链路。
>
> ### 运维简单性:⚠️ 有优化空间
>
> 当前 **8 个进程 + 5 个中间件** 对小团队偏重。建议 V1 合并 Worker、考虑去掉独立 Gateway,将运维实体控制在 **5 个进程** 以内。
### 如果只改三件事:
1. **合并 Worker**:4 个 Worker 二进制合并为 2 个(-2 部署单元)
2. **统一日志与 trace**:所有进程使用 `zap` + `request_id` 贯穿全链路
3. **恢复 `viewer` 角色**PRD 强制要求,不能漏
其余问题(错误码、旧图清理、media-binding 同步等)可在开发过程中逐步完善。
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

@@ -1,675 +0,0 @@
# GEO admin-web 后端核心链路设计
## 1. 概述
### 1.1 目标
搭建 GEO 平台 tenant-api 后端核心链路,为 admin-web 前端提供可用的 API 服务。第一阶段聚焦四个核心模块:认证、工作台、文章/模板、品牌词库,让前端能跑通内容生成主流程。
### 1.2 决策摘要
| 决策项 | 选择 | 理由 |
|--------|------|------|
| 进程模型 | 单进程 tenant-api | 当前只有 tenant 侧,gateway 无分发对象;鉴权中间件写在 shared 层,后续零成本拆出 |
| 脚手架 | 手工初始化 | sponge 生成的 handler/dao/service 命名与本项目 transport/app/domain/repository 四层结构不符,手工初始化成本更低 |
| Web 框架 | Gin | 架构文档指定 |
| DB 访问 | pgx + sqlc | 类型安全、性能最优、SQL 控制力强 |
| Migration | golang-migrate | 架构文档 §13.13.8 推荐 |
| 认证 | JWT (access + refresh) | 架构文档 §6 指定 |
| 日志 | zap 结构化 JSON | 架构文档 §13.13.10 推荐 |
| 配置 | YAML + env 覆盖 (viper) | 架构文档 §13.13.6 指定 |
| 错误码 | 统一格式 `{code, message, detail, request_id}` | 架构文档 §13.6.4 |
### 1.3 不包含
第一阶段明确不做:
- gateway 独立进程
- platform-api
- scheduler / worker-*
- prompt-rule、schedule、optimization、knowledge、media、tracking、account 模块
- RabbitMQ / Qdrant / 对象存储集成
- SSE 推送
这些在后续迭代中加入。
## 2. 项目结构
### 2.1 项目初始化
手工初始化 Go 项目(放弃 sponge),按架构文档 §5.2 的分层要求直接建目录结构:
- `transport` — HTTP handler、路由注册
- `app` — 业务逻辑 service
- `domain` — 领域模型(纯 Go 结构体,无框架依赖)
- `repository` — 数据访问(pgx + sqlc
### 2.2 目录结构
```
server/
├── cmd/
│ └── tenant-api/
│ └── main.go # 入口:初始化依赖、注册路由、启动 HTTP
├── internal/
│ ├── bootstrap/
│ │ └── bootstrap.go # 依赖容器:DB/Redis/Config 初始化
│ ├── shared/
│ │ ├── auth/
│ │ │ ├── jwt.go # JWT 颁发 (access 15min + refresh 7day)
│ │ │ ├── middleware.go # Gin 中间件:Token 校验 + 身份注入 ctx
│ │ │ └── claims.go # JWT Claims 结构体
│ │ ├── config/
│ │ │ └── config.go # Viper 配置加载
│ │ ├── middleware/
│ │ │ ├── cors.go
│ │ │ ├── request_id.go # 生成/复用 request_id
│ │ │ ├── recovery.go
│ │ │ ├── logger.go # 请求日志 (zap)
│ │ │ └── tenant_scope.go # 从 JWT 提取 tenant_id 注入 ctx
│ │ ├── response/
│ │ │ ├── response.go # 统一响应封装 Success/Error
│ │ │ └── errors.go # 错误码枚举与 AppError 类型
│ │ ├── observability/
│ │ │ └── logger.go # zap 初始化
│ │ └── repository/
│ │ ├── postgres/
│ │ │ └── postgres.go # pgx 连接池
│ │ └── redis/
│ │ └── redis.go # Redis 连接
│ ├── tenant/
│ │ ├── transport/
│ │ │ ├── router.go # 注册所有 tenant 路由组
│ │ │ ├── auth_handler.go # /api/auth/* handlers
│ │ │ ├── workspace_handler.go
│ │ │ ├── template_handler.go
│ │ │ ├── article_handler.go
│ │ │ └── brand_handler.go
│ │ ├── app/
│ │ │ ├── auth_service.go
│ │ │ ├── workspace_service.go
│ │ │ ├── template_service.go
│ │ │ ├── article_service.go
│ │ │ └── brand_service.go
│ │ ├── domain/
│ │ │ ├── user.go
│ │ │ ├── tenant.go
│ │ │ ├── article.go
│ │ │ ├── article_template.go
│ │ │ ├── article_version.go
│ │ │ ├── generation_task.go
│ │ │ ├── brand.go
│ │ │ ├── brand_keyword.go
│ │ │ ├── brand_question.go
│ │ │ └── competitor.go
│ │ └── repository/
│ │ ├── queries/ # sqlc SQL 文件
│ │ │ ├── user.sql
│ │ │ ├── tenant.sql
│ │ │ ├── article.sql
│ │ │ ├── article_template.sql
│ │ │ ├── brand.sql
│ │ │ └── ...
│ │ ├── sqlc.yaml # sqlc 配置
│ │ ├── generated/ # sqlc 生成的 Go 代码
│ │ ├── user_repo.go # Repository 接口 + 实现
│ │ ├── article_repo.go
│ │ ├── template_repo.go
│ │ └── brand_repo.go
│ └── gateway/ # 预留空目录
├── migrations/ # golang-migrate SQL 文件
│ ├── 20260331100000_create_tenants.up.sql
│ ├── 20260331100000_create_tenants.down.sql
│ ├── 20260331100001_create_users.up.sql
│ ├── ...
├── configs/
│ ├── config.yaml # 默认配置
│ └── config.local.yaml # 本地开发覆盖 (gitignore)
├── go.mod
├── go.sum
├── Makefile # dev-init, dev-api, migrate-up, sqlc-generate
└── docker-compose.yaml # PostgreSQL + Redis (本地开发)
```
## 3. 数据库 Schema
### 3.1 Migration 文件规划
按架构文档 §13.11,第一阶段需要以下表。命名规则:`YYYYMMDDHHMMSS_description.up.sql`
#### 3.1.1 身份与租户
```sql
-- 20260331100000_create_tenants.up.sql
CREATE TABLE tenants (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, frozen
frozen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_tenant_name UNIQUE (name)
);
-- 20260331100001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
phone VARCHAR(20),
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
avatar_url TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_users_email UNIQUE (email)
);
-- 20260331100002_create_tenant_memberships.up.sql
CREATE TABLE tenant_memberships (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
user_id BIGINT NOT NULL REFERENCES users(id),
tenant_role VARCHAR(20) NOT NULL, -- tenant_admin, editor, kol
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_tenant_user UNIQUE (tenant_id, user_id)
);
-- 20260331100003_create_platform_user_roles.up.sql
CREATE TABLE platform_user_roles (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
platform_role VARCHAR(20) NOT NULL, -- super_admin, ops, sre
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_platform_user_role UNIQUE (user_id, platform_role)
);
-- 20260331100004_create_plans.up.sql
CREATE TABLE plans (
id BIGSERIAL PRIMARY KEY,
plan_code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
quota_policy_json JSONB NOT NULL DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_plan_code UNIQUE (plan_code)
);
-- 20260331100005_create_tenant_plan_subscriptions.up.sql
CREATE TABLE tenant_plan_subscriptions (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
plan_id BIGINT NOT NULL REFERENCES plans(id),
start_at TIMESTAMPTZ NOT NULL,
end_at TIMESTAMPTZ NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX idx_tenant_plan_active ON tenant_plan_subscriptions(tenant_id) WHERE status = 'active'; -- 同一时间只有一个活跃订阅
-- 20260331100006_create_tenant_quota_ledgers.up.sql
CREATE TABLE tenant_quota_ledgers (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
quota_type VARCHAR(50) NOT NULL, -- article_generation
delta INT NOT NULL,
balance_after INT NOT NULL,
reason VARCHAR(255),
reference_type VARCHAR(50),
reference_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tenant_quota_tenant_type_created ON tenant_quota_ledgers(tenant_id, quota_type, created_at DESC);
-- 20260331100007_create_quota_reservations.up.sql
CREATE TABLE quota_reservations (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
quota_type VARCHAR(50) NOT NULL,
resource_type VARCHAR(50) NOT NULL,
resource_id BIGINT NOT NULL,
reserved_amount INT NOT NULL,
consumed_amount INT NOT NULL DEFAULT 0,
refunded_amount INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, confirmed, refunded, expired
expire_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quota_reservation_tenant_status ON quota_reservations(tenant_id, status);
```
#### 3.1.2 模板与文章
```sql
-- 20260331100010_create_article_templates.up.sql
CREATE TABLE article_templates (
id BIGSERIAL PRIMARY KEY,
scope VARCHAR(20) NOT NULL, -- platform, tenant
tenant_id BIGINT REFERENCES tenants(id),
origin_type VARCHAR(20) NOT NULL DEFAULT 'platform', -- platform, tenant_custom, kol_share
share_code_id BIGINT,
template_key VARCHAR(100) NOT NULL,
template_name VARCHAR(200) NOT NULL,
schema_json JSONB NOT NULL DEFAULT '{}',
prompt_template TEXT,
prompt_visibility VARCHAR(20) NOT NULL DEFAULT 'visible', -- visible, sealed
protected_prompt_asset_key TEXT,
card_config_json JSONB NOT NULL DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'active',
version_no INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_template_scope_key_version UNIQUE (scope, tenant_id, template_key, version_no)
);
-- 20260331100011_create_articles.up.sql
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
source_type VARCHAR(30) NOT NULL, -- template, prompt_rule, optimization
template_id BIGINT REFERENCES article_templates(id),
prompt_rule_id BIGINT,
current_version_id BIGINT,
generate_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, generating, completed, failed
publish_status VARCHAR(20) NOT NULL DEFAULT 'unpublished', -- unpublished, publishing, success, failed, pending_review
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_article_tenant_created ON articles(tenant_id, created_at DESC);
CREATE INDEX idx_article_tenant_status ON articles(tenant_id, generate_status, publish_status, created_at DESC);
-- 20260331100012_create_article_versions.up.sql
CREATE TABLE article_versions (
id BIGSERIAL PRIMARY KEY,
article_id BIGINT NOT NULL REFERENCES articles(id),
version_no INT NOT NULL,
title VARCHAR(500),
html_content TEXT,
markdown_content TEXT,
word_count INT NOT NULL DEFAULT 0,
source_label VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_article_version_no UNIQUE (article_id, version_no)
);
-- 20260331100013_create_generation_tasks.up.sql
CREATE TABLE generation_tasks (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
article_id BIGINT REFERENCES articles(id),
task_batch_id VARCHAR(100),
quota_reservation_id BIGINT REFERENCES quota_reservations(id),
task_type VARCHAR(50) NOT NULL, -- template, prompt, batch_item, schedule_item, optimize
request_hash VARCHAR(64),
input_params_json JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'queued', -- queued, running, completed, failed
error_message TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_generation_task_status_created ON generation_tasks(status, created_at);
CREATE INDEX idx_generation_task_tenant ON generation_tasks(tenant_id, created_at DESC);
```
#### 3.1.3 品牌
```sql
-- 20260331100020_create_brands.up.sql
CREATE TABLE brands (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
name VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_brand_tenant_name UNIQUE (tenant_id, name)
);
-- 20260331100021_create_brand_keywords.up.sql
CREATE TABLE brand_keywords (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
brand_id BIGINT NOT NULL REFERENCES brands(id),
name VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uk_brand_keyword_name UNIQUE (brand_id, name)
);
-- 20260331100022_create_brand_questions.up.sql
CREATE TABLE brand_questions (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
brand_id BIGINT NOT NULL REFERENCES brands(id),
keyword_id BIGINT NOT NULL REFERENCES brand_keywords(id),
current_version_id BIGINT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_brand_questions_keyword ON brand_questions(keyword_id);
-- 20260331100023_create_brand_question_versions.up.sql
CREATE TABLE brand_question_versions (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES brand_questions(id),
question_text TEXT NOT NULL,
question_hash VARCHAR(64) NOT NULL,
version_no INT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_question_version_no UNIQUE (question_id, version_no)
);
CREATE INDEX idx_question_hash ON brand_question_versions(question_hash);
-- 20260331100024_create_competitors.up.sql
CREATE TABLE competitors (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
brand_id BIGINT NOT NULL REFERENCES brands(id),
name VARCHAR(200) NOT NULL,
website TEXT,
description TEXT,
product_lines_json JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_competitor_brand ON competitors(brand_id);
```
#### 3.1.4 基础设施
```sql
-- 20260331100030_create_task_records.up.sql
CREATE TABLE task_records (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
task_type VARCHAR(50) NOT NULL,
resource_type VARCHAR(50) NOT NULL,
resource_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
retry_count INT NOT NULL DEFAULT 0,
task_batch_id VARCHAR(100),
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_task_record_status_type ON task_records(status, task_type);
-- 20260331100031_create_audit_logs.up.sql
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
operator_id BIGINT NOT NULL REFERENCES users(id),
tenant_id BIGINT,
module VARCHAR(50) NOT NULL,
action VARCHAR(50) NOT NULL,
before_json JSONB,
after_json JSONB,
result VARCHAR(20),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_module_created ON audit_logs(module, created_at DESC);
```
### 3.2 种子数据
`make dev-init` 需要写入最小种子数据:
- 1 个默认租户
- 1 个 tenant_admin 用户 (admin@geo.local / 默认密码)
- 1 个 tenant_membership
- 1 个免费套餐 (plan)
- 1 个订阅关系
- 初始额度记录
- 3-4 个平台预设模板 (Top X 文章、产品评测文章、研究报告文章、品牌词搜索扩写)
## 4. API 设计
### 4.1 统一响应格式
所有 API 返回:
```json
// 成功
{
"code": 0,
"message": "ok",
"data": { ... },
"request_id": "req_xxx"
}
// 失败
{
"code": 40301,
"message": "quota_insufficient",
"detail": "生成额度不足,请升级套餐或等待额度重置",
"request_id": "req_xxx"
}
```
错误码段 (§13.6.4)`400xx` 参数错误, `401xx` 认证失败, `403xx` 权限/额度不足, `404xx` 资源不存在, `409xx` 状态冲突, `429xx` 频率限制, `500xx` 内部错误, `503xx` 下游不可用。
### 4.2 分页格式
请求:`?page=1&page_size=20&sort=created_at&order=desc`
响应 data 字段:
```json
{
"items": [...],
"total": 100,
"page": 1,
"page_size": 20
}
```
### 4.3 Auth API
| Method | Path | 说明 |
|--------|------|------|
| POST | `/api/auth/login` | 邮箱+密码登录,返回 access_token + refresh_token |
| POST | `/api/auth/refresh` | 刷新 access_token |
| GET | `/api/auth/me` | 返回当前用户信息、tenant_id、tenant_role、permissions[] |
| POST | `/api/auth/logout` | 登出 (可选:加入 token 黑名单) |
**JWT Claims**
```go
type Claims struct {
UserID int64 `json:"user_id"`
TenantID int64 `json:"tenant_id"`
Role string `json:"role"` // tenant_admin, editor
jwt.RegisteredClaims
}
```
- Access token TTL: 15 分钟
- Refresh token TTL: 7 天
- Refresh token 存 Redis,支持主动失效
### 4.4 Workspace API
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/tenant/workspace/overview` | 聚合统计:文章数、发布数、品牌数、媒体平台数 |
| GET | `/api/tenant/workspace/recent-articles` | 最近 10 篇文章 (带生成状态/发布状态) |
| GET | `/api/tenant/workspace/quota-summary` | 当前套餐额度:已用/总量/重置时间 |
| GET | `/api/tenant/workspace/template-cards` | 可用模板卡片列表 (平台预设 + 租户自建) |
workspace_service 聚合多个 repository 数据,单次返回,避免前端首屏发起过多散请求 (§13.2)。
### 4.5 Template API
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/tenant/templates` | 模板列表 (scope=platform 或 tenant_id 匹配) |
| GET | `/api/tenant/templates/{id}` | 模板详情 (prompt_visibility=sealed 时不返回 prompt_template) |
| GET | `/api/tenant/templates/{id}/schema` | 模板表单 schema (JSON) |
| POST | `/api/tenant/templates/{id}/generate` | 提交生成任务 |
**生成流程** (§10.1)
1. 校验租户身份、额度、参数
2. 在单个 PG 事务内:额度预扣 + generation_tasks 创建 + articles 占位记录
3. 返回 task_id 和 article_id
4. (V1 第一阶段:任务状态模拟为同步完成或标记为 queued 等待 worker)
### 4.6 Article API
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/tenant/articles` | 文章列表 (分页、筛选:generate_status, publish_status, source_type, template_id, keyword, date_range) |
| GET | `/api/tenant/articles/{id}` | 文章详情 (含当前版本内容) |
| GET | `/api/tenant/articles/{id}/versions` | 文章版本列表 |
| DELETE | `/api/tenant/articles/{id}` | 删除文章 |
### 4.7 Brand API
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/tenant/brands` | 品牌列表 |
| POST | `/api/tenant/brands` | 创建品牌 |
| GET | `/api/tenant/brands/{id}` | 品牌详情 |
| PUT | `/api/tenant/brands/{id}` | 更新品牌 |
| DELETE | `/api/tenant/brands/{id}` | 删除品牌 |
| GET | `/api/tenant/brands/{id}/keywords` | 关键词列表 |
| POST | `/api/tenant/brands/{id}/keywords` | 创建关键词 |
| PUT | `/api/tenant/brands/{id}/keywords/{kid}` | 更新关键词 |
| DELETE | `/api/tenant/brands/{id}/keywords/{kid}` | 删除关键词 |
| GET | `/api/tenant/brands/{id}/questions` | 问题列表 (按关键词筛选) |
| POST | `/api/tenant/brands/{id}/questions` | 创建问题 (自动创建 v1 版本) |
| GET | `/api/tenant/brands/{id}/competitors` | 竞品列表 |
| POST | `/api/tenant/brands/{id}/competitors` | 创建竞品 |
| PUT | `/api/tenant/brands/{id}/competitors/{cid}` | 更新竞品 |
| DELETE | `/api/tenant/brands/{id}/competitors/{cid}` | 删除竞品 |
### 4.8 Health Check
| Method | Path | 说明 |
|--------|------|------|
| GET | `/api/health/live` | 存活检查 |
| GET | `/api/health/ready` | 就绪检查 (DB + Redis 可达) |
## 5. 中间件链
请求处理顺序 (§6.3)
1. Recovery (panic 恢复)
2. RequestID (生成/复用 request_id)
3. Logger (请求日志)
4. CORS
5. Auth (JWT 校验 + 身份注入 ctx) — `/api/auth/login``/api/health/*` 除外
6. TenantScope (从 JWT 提取 tenant_id 注入 ctx)
7. Handler
### 5.1 TenantScope 实现
```go
// 从 context 中获取 tenant_idrepository 层必须调用
func TenantIDFromCtx(ctx context.Context) int64
```
所有 tenant repository 方法的 SQL 必须包含 `WHERE tenant_id = $tenant_id`。sqlc 生成的查询默认带上此参数。
## 6. 本地开发
### 6.1 docker-compose.yaml
仅启动基础设施:
```yaml
services:
postgres:
image: postgres:16
ports: ["5432:5432"]
environment:
POSTGRES_DB: geo
POSTGRES_USER: geo
POSTGRES_PASSWORD: geo_dev
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports: ["6379:6379"]
volumes:
pgdata:
```
### 6.2 Makefile 命令
```makefile
dev-init: # docker compose up -d + migrate up + seed data
dev-api: # go run ./server/cmd/tenant-api
migrate-up: # golang-migrate up
migrate-down: # golang-migrate down 1
migrate-create: # 创建新 migration 文件
sqlc-generate: # sqlc generate
lint: # golangci-lint run
test: # go test ./...
```
### 6.3 配置文件
```yaml
# configs/config.yaml
server:
port: 8080
mode: debug # debug / release
database:
host: localhost
port: 5432
user: geo
password: geo_dev
dbname: geo
sslmode: disable
max_open_conns: 25
max_idle_conns: 5
redis:
addr: localhost:6379
db: 0
jwt:
secret: "${JWT_SECRET:dev-secret-change-in-production}"
access_ttl: 15m
refresh_ttl: 168h # 7 days
log:
level: debug
format: json
```
## 7. 验证方式
1. `make dev-init` 启动基础设施并初始化数据库
2. `make dev-api` 启动 tenant-api
3. `POST /api/auth/login` 登录获取 token
4. `GET /api/auth/me` 验证身份
5. `GET /api/tenant/workspace/overview` 验证工作台数据
6. `GET /api/tenant/templates` 查看预设模板
7. `POST /api/tenant/brands` 创建品牌
8. `GET /api/tenant/brands/{id}/keywords` 查看关键词
9. `make test` 所有测试通过
10. `make lint` 无 lint 错误