Files
geo/docs/ai-brand-monitoring-tech-design-v2.md
T
root 446f865cdf 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.
2026-04-05 17:14:13 +08:00

1349 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI 品牌曝光监测系统技术方案 V2
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档名称 | AI 品牌曝光监测系统技术方案 V2 |
| 文档版本 | V2.0 |
| 文档状态 | 待评审 |
| 创建日期 | 2026-04-05 |
| 适用范围 | 数据追踪模块全部页面 |
| 关联文档 | `docs/geo-platform-prd-v1.md` (PRD 8.4 数据追踪) |
| 关联文档 | `docs/question-driven-monitoring-design-v1.md` (V1 数据模型) |
| 容量目标 | 5 万并发在线用户 |
## 2. 与 V1 设计的关系
本方案基于 V1 (`question-driven-monitoring-design-v1.md`) 的核心结论,保留其"问题驱动 + 日级汇总 + 版本绑定"的数据模型哲学,在此基础上扩展以下能力:
| V1 已定义 | V2 扩展 |
| --- | --- |
| 品牌曝光趋势 + 高频问题 + 引用排行 | 新增:主页总览、平台占比分析、竞争对手分析、业务主题分析、AI 对话问题列表、AI 引用来源排名 |
| 单维度(品牌+关键词+模型) | 多维度:按 AI 平台、按竞品、按主题、按引用来源交叉分析 |
| 汇总表 3 张 | 扩展至 8 张汇总表 + 2 张原始采集表 |
| 未涉及性能架构 | 明确 5 万并发的缓存层、连接池、部署拓扑 |
| 未涉及前端 | 6 个页面 + ECharts 可视化完整方案 |
V1 定义的采集逻辑、版本化策略、去重策略、保留策略 **全部沿用,不再重复**
## 3. 目标页面与数据需求
基于产品截图,数据追踪模块共 6 个页面:
### 3.1 主页(Dashboard
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 曝光趋势图 | 近 7/30 天品牌在所有 AI 平台的综合曝光度日趋势 | `monitoring_brand_daily_overview` |
| 前五竞争对手 | 品牌及竞品的情感得分、曝光度排名 | `monitoring_competitor_daily` |
| 引用来源类型占比 | 编辑类、企业、UGC、参考资料、机构、其他的占比饼图 | `monitoring_citation_source_daily` |
| 前五引用平台 | 引用次数最多的外部平台,含百度权重、引用次数、覆盖度 | `monitoring_citation_source_daily` |
| 最新 AI 对话 | 最近被触发的 AI 问答问题、排名、前三品牌 | `monitoring_question_daily` |
| AI 品牌印象 | 品牌相关的高频词词云 | `monitoring_brand_impression` |
### 3.2 平台占比分析
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 提及次数排名 | 各 AI 平台对品牌的提及总次数横向柱状图 | `monitoring_platform_daily` |
| 各平台排名表现 | 按平台展开:AI 对话问题、提及次数、平均位置、曝光度 | `monitoring_platform_question_daily` |
### 3.3 竞争对手分析
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 竞争对手曝光度排名 | 品牌 + 竞品在各 AI 平台上的曝光度分组柱状图 | `monitoring_competitor_daily` |
| 竞争对手排名情况 | 按品牌/竞品展开:各平台提及次数、平均位置、曝光度 | `monitoring_competitor_platform_daily` |
### 3.4 业务主题分析
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 业务主题 | 主题名称、问题数、曝光度、提及次数 | `monitoring_topic_daily` |
| 引用最高的前五平台 | 外部引用来源平台排名 | `monitoring_citation_source_daily` |
| 引用次数最多的页面 | 具体被引用的外部 URL | `monitoring_citation_page_daily` |
### 3.5 AI 对话问题
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 问题列表 | AI 对话问题、热度指数、曝光度、平均位置、情感倾向、前三品牌 | `monitoring_question_daily` |
| 查看效果 / 查看引用 | 单条问题的详细回答效果和引用来源 | `question_monitor_parse_results` |
### 3.6 AI 引用排名
| 区域 | 数据需求 | 数据来源 |
| --- | --- | --- |
| 最高引用来源平台趋势 | 前 5 引用来源平台的引用次数日趋势折线图 | `monitoring_citation_source_daily` |
| 引用页面详情 | 引用平台、百度权重、引用页面 URL、引用次数、覆盖率 | `monitoring_citation_page_daily` |
## 4. 与现有数据模型的关系
### 4.1 已有表复用
监测系统直接复用以下现有表,**不新建冗余表**:
| 现有表 | 用途 | 关键字段 |
| --- | --- | --- |
| `brands` | 被监测的品牌主体 | `id, tenant_id, name` |
| `brand_keywords` | 监测问题的分类容器 | `id, brand_id, name` |
| `brand_questions` | 监测采集的最小单元 | `id, brand_id, keyword_id, current_version_id` |
| `brand_question_versions` | 保证历史口径稳定 | `id, question_id, question_text, question_hash, version_no` |
| `competitors` | 竞品品牌数据 | `id, brand_id, name, website` |
| `articles` | 被引用的系统文章 | `id, tenant_id, source_type` |
### 4.2 数据流向
```
现有品牌词库 新建监测模块
┌──────────────┐ ┌──────────────────┐
│ brands │──────────────────▶│ 采集调度器 │
│ keywords │ 品牌+关键词+问题 │ (按问题×模型调度) │
│ questions │──────────────────▶│ │
│ question_ │ 绑定问题版本 └────────┬─────────┘
│ versions │ │
│ competitors │──── 竞品清单 ──────▶ │ 调用 AI 平台 API
└──────────────┘ ▼
┌──────────────────┐
现有文章表 │ 原始采集表 │
┌──────────────┐ │ question_monitor_ │
│ articles │◀── 引用检测 ──────│ runs / results │
│ article_ │ └────────┬─────────┘
│ versions │ │ 日级聚合 Job
└──────────────┘ ▼
┌──────────────────┐
│ 汇总表 × 8 │
│ (monitoring_*) │
└────────┬─────────┘
┌──────────────────┐
│ Redis 缓存层 │
└────────┬─────────┘
┌──────────────────┐
│ Dashboard API │
│ (6 个页面) │
└──────────────────┘
```
## 5. 数据库 Schema
### 5.1 参考表
#### `ai_platforms` — AI 平台注册表
```sql
CREATE TABLE ai_platforms (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
name VARCHAR(50) NOT NULL, -- DeepSeek / 通义千问 / 豆包 / 腾讯元宝 / 文心一言
icon VARCHAR(255),
provider_key VARCHAR(50) NOT NULL, -- deepseek / qwen / doubao / hunyuan / ernie
api_endpoint TEXT,
api_model VARCHAR(100),
display_order INT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX uk_ai_platform_tenant_key ON ai_platforms(tenant_id, provider_key);
```
#### `monitoring_topics` — 业务主题分类
```sql
CREATE TABLE monitoring_topics (
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, -- 瑜伽服饰 / 运动生活方式品牌 / 女士运动内衣
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
);
CREATE UNIQUE INDEX uk_topic_tenant_brand_name ON monitoring_topics(tenant_id, brand_id, name) WHERE deleted_at IS NULL;
```
#### `monitoring_topic_questions` — 主题与问题的多对多关系
```sql
CREATE TABLE monitoring_topic_questions (
id BIGSERIAL PRIMARY KEY,
topic_id BIGINT NOT NULL REFERENCES monitoring_topics(id),
question_id BIGINT NOT NULL REFERENCES brand_questions(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_topic_question UNIQUE (topic_id, question_id)
);
```
### 5.2 原始采集表
沿用 V1 设计,增加 `tenant_id` 租户隔离字段。
#### `question_monitor_runs` — 单次采集执行记录
```sql
CREATE TABLE question_monitor_runs (
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),
question_id BIGINT NOT NULL REFERENCES brand_questions(id),
question_version_id BIGINT NOT NULL REFERENCES brand_question_versions(id),
ai_platform_id BIGINT NOT NULL REFERENCES ai_platforms(id),
run_date DATE NOT NULL,
run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
task_batch_id VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/running/completed/failed
raw_answer_key TEXT, -- MinIO 对象存储路径
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (run_date);
CREATE INDEX idx_monitor_runs_tenant_date ON question_monitor_runs(tenant_id, run_date DESC);
CREATE INDEX idx_monitor_runs_batch ON question_monitor_runs(task_batch_id);
```
> **分区策略**:按 `run_date` 月级 Range 分区,自动创建未来 3 个月分区,到期后可 `DROP PARTITION` 清理。
#### `question_monitor_parse_results` — 单次采集解析结果
```sql
CREATE TABLE question_monitor_parse_results (
id BIGSERIAL PRIMARY KEY,
run_id BIGINT NOT NULL, -- 指向 question_monitor_runs.id(分区表不建 FK
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
-- 品牌提及指标
brand_mentioned BOOLEAN NOT NULL DEFAULT false,
brand_position INT, -- 品牌出现位置(1=首位)
top1_mentioned BOOLEAN NOT NULL DEFAULT false,
first_recommended BOOLEAN NOT NULL DEFAULT false,
positive_sentiment BOOLEAN NOT NULL DEFAULT false,
sentiment_label VARCHAR(20), -- excellent/normal/negative
-- 竞品提及
competitor_mentions JSONB, -- [{"competitor_id":1,"name":"Nike","position":2,"sentiment":"positive"}]
-- 引用检测
citation_urls JSONB, -- [{"url":"https://...","platform":"什么值得买","platform_type":"UGC","baidu_weight":7}]
cited_article_ids BIGINT[], -- 系统文章 ID 数组
citation_count INT NOT NULL DEFAULT 0,
-- 品牌印象关键词
brand_keywords_extract JSONB, -- [{"word":"性价比高","weight":5},{"word":"设计时尚","weight":3}]
parser_version VARCHAR(20) NOT NULL DEFAULT 'v1',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_parse_results_run ON question_monitor_parse_results(run_id);
CREATE INDEX idx_parse_results_tenant ON question_monitor_parse_results(tenant_id);
```
### 5.3 汇总表
所有汇总表均由后台聚合 Job 写入,Dashboard 查询 **只读** 汇总表。
#### 1) `monitoring_brand_daily_overview` — 品牌日级总览(主页指标卡 + 趋势图)
```sql
CREATE TABLE monitoring_brand_daily_overview (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
-- 核心指标
total_question_count INT NOT NULL DEFAULT 0, -- 采集的问题总数
total_ask_count INT NOT NULL DEFAULT 0, -- 总提问次数(跨平台)
mention_count INT NOT NULL DEFAULT 0,
top1_mention_count INT NOT NULL DEFAULT 0,
first_recommend_count INT NOT NULL DEFAULT 0,
positive_mention_count INT NOT NULL DEFAULT 0,
citation_count INT NOT NULL DEFAULT 0,
-- 比率
mention_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
top1_mention_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
first_recommend_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
positive_mention_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0, -- 曝光度
-- 情感得分
sentiment_score INT, -- 0~100
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_brand_overview UNIQUE (tenant_id, brand_id, metric_date)
);
```
#### 2) `monitoring_platform_daily` — 按 AI 平台维度的品牌日统计(平台占比分析)
```sql
CREATE TABLE monitoring_platform_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
ai_platform_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
mention_count INT NOT NULL DEFAULT 0,
avg_position NUMERIC(5,2), -- 平均排名位置
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
top1_count INT NOT NULL DEFAULT 0,
question_count INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_daily UNIQUE (tenant_id, brand_id, ai_platform_id, metric_date)
);
```
#### 3) `monitoring_platform_question_daily` — 按平台×问题的明细(平台展开表)
```sql
CREATE TABLE monitoring_platform_question_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
ai_platform_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
question_text TEXT NOT NULL, -- 快照
mention_count INT NOT NULL DEFAULT 0,
avg_position NUMERIC(5,2),
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_platform_question_daily UNIQUE (tenant_id, brand_id, ai_platform_id, question_id, metric_date)
);
```
#### 4) `monitoring_competitor_daily` — 竞品对比日统计(竞争对手分析)
```sql
CREATE TABLE monitoring_competitor_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL, -- 主品牌
compared_brand_name VARCHAR(200) NOT NULL, -- 品牌名或竞品名
is_self BOOLEAN NOT NULL DEFAULT false,
competitor_id BIGINT, -- NULL 表示自身品牌
metric_date DATE NOT NULL,
mention_count INT NOT NULL DEFAULT 0,
avg_position NUMERIC(5,2),
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
sentiment_score INT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_competitor_daily UNIQUE (tenant_id, brand_id, compared_brand_name, metric_date)
);
```
#### 5) `monitoring_competitor_platform_daily` — 竞品×平台交叉统计
```sql
CREATE TABLE monitoring_competitor_platform_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
compared_brand_name VARCHAR(200) NOT NULL,
ai_platform_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
mention_count INT NOT NULL DEFAULT 0,
avg_position NUMERIC(5,2),
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_comp_platform_daily UNIQUE (tenant_id, brand_id, compared_brand_name, ai_platform_id, metric_date)
);
```
#### 6) `monitoring_topic_daily` — 业务主题日统计
```sql
CREATE TABLE monitoring_topic_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
topic_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
question_count INT NOT NULL DEFAULT 0,
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
mention_count INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_topic_daily UNIQUE (tenant_id, brand_id, topic_id, metric_date)
);
```
#### 7) `monitoring_question_daily` — 单问题日统计(AI 对话问题页)
```sql
CREATE TABLE monitoring_question_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
question_version_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
question_text TEXT NOT NULL,
heat_index INT NOT NULL DEFAULT 0, -- 热度指数
exposure_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
avg_position NUMERIC(5,2),
sentiment_label VARCHAR(20), -- excellent/normal/negative
top3_brands JSONB, -- [{"name":"lululemon","position":1},...]
ask_count INT NOT NULL DEFAULT 0,
mention_count INT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_question_daily UNIQUE (tenant_id, brand_id, question_id, metric_date)
);
```
#### 8) `monitoring_citation_source_daily` — 引用来源日统计(AI 引用排名)
```sql
CREATE TABLE monitoring_citation_source_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
source_platform VARCHAR(200) NOT NULL, -- 夸克 / 排行榜123网 / 什么值得买
source_type VARCHAR(50) NOT NULL, -- UGC / 编辑类 / 企业 / 参考资料 / 机构 / 其他
baidu_weight INT, -- 百度权重 0~10
citation_count INT NOT NULL DEFAULT 0,
coverage_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_citation_source_daily UNIQUE (tenant_id, brand_id, source_platform, metric_date)
);
```
#### 9) `monitoring_citation_page_daily` — 引用页面明细日统计
```sql
CREATE TABLE monitoring_citation_page_daily (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
source_platform VARCHAR(200) NOT NULL,
baidu_weight INT,
page_url TEXT NOT NULL,
citation_count INT NOT NULL DEFAULT 0,
coverage_rate NUMERIC(5,4) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_citation_page_daily UNIQUE (tenant_id, brand_id, page_url, metric_date)
);
```
#### 10) `monitoring_brand_impression` — 品牌印象词(词云数据)
```sql
CREATE TABLE monitoring_brand_impression (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
brand_id BIGINT NOT NULL,
metric_date DATE NOT NULL,
word VARCHAR(100) NOT NULL,
weight INT NOT NULL DEFAULT 1, -- 词频权重
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_impression_word UNIQUE (tenant_id, brand_id, word, metric_date)
);
```
### 5.4 分区策略
| 表 | 分区方式 | 保留周期 |
| --- | --- | --- |
| `question_monitor_runs` | RANGE by `run_date`(月级) | 90 天 |
| `question_monitor_parse_results` | 不分区(通过 `run_id` 关联) | 90 天 |
| 所有 `monitoring_*` 汇总表 | 不分区(数据量可控) | 180 天+ |
### 5.5 索引策略
所有汇总表的 UNIQUE 约束自动创建复合索引,天然覆盖 Dashboard 的查询路径(`WHERE tenant_id = ? AND brand_id = ? AND metric_date BETWEEN ? AND ?`)。
额外索引:
```sql
-- 主页趋势图和指标卡的高频查询
CREATE INDEX idx_brand_overview_range ON monitoring_brand_daily_overview(tenant_id, brand_id, metric_date DESC);
-- 竞品分析页的高频查询
CREATE INDEX idx_competitor_range ON monitoring_competitor_daily(tenant_id, brand_id, metric_date DESC);
-- 问题页排序
CREATE INDEX idx_question_daily_heat ON monitoring_question_daily(tenant_id, brand_id, metric_date DESC, heat_index DESC);
```
## 6. 后端 API 设计
### 6.1 路由规划
所有路由挂载在 `tenant` 认证组下,复用现有 JWT + TenantScope 中间件。
```
GET /api/tenant/monitoring/dashboard/overview → 主页指标卡
GET /api/tenant/monitoring/dashboard/exposure-trend → 主页曝光趋势图
GET /api/tenant/monitoring/dashboard/competitors → 主页前五竞争对手
GET /api/tenant/monitoring/dashboard/citation-sources → 主页引用来源类型占比 + 前五引用平台
GET /api/tenant/monitoring/dashboard/recent-questions → 主页最新 AI 对话
GET /api/tenant/monitoring/dashboard/brand-impression → 主页品牌印象词云
GET /api/tenant/monitoring/platforms/stats → 平台占比 - 提及次数排名
GET /api/tenant/monitoring/platforms/:id/questions → 平台占比 - 各平台问题明细
GET /api/tenant/monitoring/competitors/exposure → 竞争对手 - 曝光度排名
GET /api/tenant/monitoring/competitors/detail → 竞争对手 - 排名情况明细
GET /api/tenant/monitoring/topics → 业务主题列表
GET /api/tenant/monitoring/topics/:id/citations → 业务主题 - 引用详情
GET /api/tenant/monitoring/questions → AI 对话问题列表(分页)
GET /api/tenant/monitoring/questions/:id/effect → 问题效果详情
GET /api/tenant/monitoring/questions/:id/citations → 问题引用详情
GET /api/tenant/monitoring/citations/trend → AI 引用排名 - 来源平台趋势
GET /api/tenant/monitoring/citations/pages → AI 引用排名 - 引用页面列表
GET /api/tenant/monitoring/ai-platforms → AI 平台下拉列表
```
### 6.2 通用查询参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `brand_id` | int64 | 是 | - | 品牌 ID |
| `ai_platform_id` | int64 | 否 | 0 (全部) | AI 平台筛选 |
| `days` | int | 否 | 7 | 时间范围:7 / 30 / 90 |
| `page` | int | 否 | 1 | 分页页码 |
| `page_size` | int | 否 | 20 | 分页大小,最大 100 |
### 6.3 响应格式
复用现有 `response.Success` / `response.Error` 统一格式:
```json
{
"code": 0,
"data": { ... },
"request_id": "req_abc123"
}
```
### 6.4 关键 API 响应结构
#### Dashboard Overview
```json
{
"exposure_rate": 0.6333,
"mention_rate": 0.7500,
"top1_mention_rate": 0.4800,
"positive_rate": 0.8200,
"sentiment_score": 83,
"exposure_rate_change": 0.05,
"mention_rate_change": -0.02
}
```
#### Competitors
```json
{
"items": [
{
"rank": 1,
"name": "lululemon",
"is_self": true,
"sentiment_score": 83,
"sentiment_label": "excellent",
"exposure_rate": 0.64
},
{
"rank": 2,
"name": "Nike",
"is_self": false,
"sentiment_score": 69,
"sentiment_label": "normal",
"exposure_rate": 0.47
}
]
}
```
#### Questions (分页)
```json
{
"items": [
{
"question_id": 101,
"question_text": "瑜伽垫品牌推荐",
"heat_index": 5,
"exposure_rate": 1.0,
"avg_position": 1,
"sentiment_label": "excellent",
"top3_brands": ["lululemon", "Alo Yoga", "Nike"]
}
],
"total": 42,
"page": 1,
"page_size": 20
}
```
## 7. 后端架构
### 7.1 服务层设计
新增文件与现有代码的关系:
```
server/internal/tenant/
├── app/
│ ├── monitoring_service.go ← 新建:Dashboard 查询服务
│ ├── monitoring_collector.go ← 新建:AI 平台采集服务
│ ├── monitoring_aggregator.go ← 新建:日级聚合 Job
│ ├── monitoring_parser.go ← 新建:回答解析器
│ ├── brand_service.go ← 已有:品牌 CRUD
│ └── ...
├── transport/
│ ├── monitoring_handler.go ← 新建:监测 API Handler
│ ├── router.go ← 修改:注册监测路由
│ └── ...
└── repository/
└── queries/
├── monitoring.sql ← 新建:监测查询 SQL
└── ...
```
### 7.2 MonitoringService 结构
```go
type MonitoringService struct {
pool *pgxpool.Pool
cache cache.Cache // 复用现有 cache.Cache 接口(Redis
}
// Dashboard 查询方法(全部走缓存 → 汇总表路径)
func (s *MonitoringService) DashboardOverview(ctx, tenantID, brandID, days) DashboardOverviewResp
func (s *MonitoringService) ExposureTrend(ctx, tenantID, brandID, days) []TrendPoint
func (s *MonitoringService) TopCompetitors(ctx, tenantID, brandID, days, limit) []CompetitorItem
func (s *MonitoringService) CitationSources(ctx, tenantID, brandID, days) CitationSourceResp
func (s *MonitoringService) RecentQuestions(ctx, tenantID, brandID, limit) []RecentQuestionItem
func (s *MonitoringService) BrandImpression(ctx, tenantID, brandID, days) []ImpressionWord
func (s *MonitoringService) PlatformStats(ctx, tenantID, brandID, days) []PlatformStatItem
func (s *MonitoringService) PlatformQuestions(ctx, tenantID, brandID, platformID, days, page) PagedQuestions
func (s *MonitoringService) CompetitorExposure(ctx, tenantID, brandID, days) CompetitorExposureResp
func (s *MonitoringService) CompetitorDetail(ctx, tenantID, brandID, days) []CompetitorDetailItem
func (s *MonitoringService) Topics(ctx, tenantID, brandID, platformID, days) []TopicItem
func (s *MonitoringService) Questions(ctx, tenantID, brandID, platformID, days, page, pageSize) PagedQuestions
func (s *MonitoringService) CitationTrend(ctx, tenantID, brandID, platformID, days) []CitationTrendPoint
func (s *MonitoringService) CitationPages(ctx, tenantID, brandID, days, page, pageSize) PagedCitationPages
func (s *MonitoringService) AIPlatforms(ctx, tenantID) []AIPlatform
```
### 7.3 MonitoringCollector 结构
```go
type MonitoringCollector struct {
pool *pgxpool.Pool
llmClients map[string]llm.Client // 每个 AI 平台一个 LLM Client
objectStorage objectstorage.Client // 原始回答存 MinIO
logger *zap.Logger
}
// 采集流程
func (c *MonitoringCollector) RunDailyCollection(ctx, tenantID, brandID) error
func (c *MonitoringCollector) CollectSingle(ctx, question, platform) (*MonitorRun, error)
func (c *MonitoringCollector) ParseAnswer(ctx, run, rawAnswer) (*ParseResult, error)
```
### 7.4 MonitoringAggregator 结构
```go
type MonitoringAggregator struct {
pool *pgxpool.Pool
cache cache.Cache
logger *zap.Logger
}
// 聚合 Job(通常每小时或每天运行一次)
func (a *MonitoringAggregator) AggregateDaily(ctx, tenantID, brandID, date) error
func (a *MonitoringAggregator) InvalidateCache(ctx, tenantID, brandID) error
```
### 7.5 Bootstrap 集成
`bootstrap.go``App` 结构中添加:
```go
type App struct {
// ... 现有字段 ...
MonitoringCollector *app.MonitoringCollector
MonitoringAggregator *app.MonitoringAggregator
}
```
`New()` 中初始化新的 LLM clients(每个 AI 平台一个),注入到 Collector。
### 7.6 配置扩展
`config.yaml` 中新增:
```yaml
monitoring:
# 采集配置
collection:
daily_run_count: 1 # 每题每天默认执行次数
worker_concurrency: 5 # 并发采集 worker 数
question_timeout: 30s # 单次采集超时
batch_size: 50 # 每批提交数量
# 聚合配置
aggregation:
schedule: "0 3 * * *" # 每天凌晨 3 点聚合
lookback_days: 1 # 回溯天数
# AI 平台 API
platforms:
deepseek:
base_url: "https://api.deepseek.com/v1"
api_key: ""
model: "deepseek-chat"
qwen:
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key: ""
model: "qwen-max"
doubao:
base_url: "https://ark.cn-beijing.volces.com/api/v3"
api_key: ""
model: "doubao-seed-2-0-lite-260215"
hunyuan:
base_url: "https://hunyuan.tencentcloudapi.com"
api_key: ""
model: "hunyuan-lite"
ernie:
base_url: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1"
api_key: ""
model: "ernie-4.0-8k"
# 缓存配置
cache:
overview_ttl: 5m
trend_ttl: 15m
competitor_ttl: 15m
topic_ttl: 30m
question_ttl: 10m
citation_ttl: 15m
```
## 8. 缓存策略(5 万并发核心)
### 8.1 缓存架构
```
Browser → CDN (静态资源) → Nginx → Go API → Redis Cache → PostgreSQL
90%+ 命中率
直接返回
```
### 8.2 缓存 Key 设计
复用 V1 的 Key 格式,扩展到新维度:
```
mon:{tenant}:{brand}:overview:{days} → 指标卡
mon:{tenant}:{brand}:trend:{days} → 趋势图
mon:{tenant}:{brand}:competitors:{days} → 竞品排名
mon:{tenant}:{brand}:citation_sources:{days} → 引用来源
mon:{tenant}:{brand}:recent_questions → 最新对话
mon:{tenant}:{brand}:impression:{days} → 品牌印象
mon:{tenant}:{brand}:platform_stats:{days} → 平台统计
mon:{tenant}:{brand}:platform:{pid}:questions:{days}:{page} → 平台问题
mon:{tenant}:{brand}:comp_exposure:{days} → 竞品曝光
mon:{tenant}:{brand}:topics:{pid}:{days} → 业务主题
mon:{tenant}:{brand}:questions:{pid}:{days}:{page} → 对话问题列表
mon:{tenant}:{brand}:citation_trend:{pid}:{days} → 引用趋势
mon:{tenant}:{brand}:citation_pages:{days}:{page} → 引用页面
```
### 8.3 缓存 TTL 矩阵
| 数据类型 | TTL | 原因 |
| --- | --- | --- |
| Overview 指标卡 | 5 min | 高频访问入口,需要相对实时 |
| 趋势图 | 15 min | 日级数据变化慢 |
| 竞品排名 | 15 min | 日级数据 |
| 引用来源 / 引用页面 | 15 min | 日级聚合 |
| 业务主题 | 30 min | 低频变更 |
| 问题列表 | 10 min | 带分页,命中粒度细 |
| 品牌印象 | 30 min | 日级聚合 |
| AI 平台列表 | 1 hour | 几乎不变 |
### 8.4 缓存失效策略
1. **TTL 自动过期**:主要策略,简单可靠。
2. **聚合 Job 主动失效**`MonitoringAggregator.InvalidateCache()` 在每次聚合完成后删除对应品牌的所有缓存 Key。
3. **不做 Cache-Aside Write-Through**:汇总表是离线 Job 写入的,不是用户请求触发的,因此只需被动读取缓存。
### 8.5 50000 并发容量分析
| 层 | 单机容量 | 5 万并发时 QPS 估算 | 需要实例数 |
| --- | --- | --- | --- |
| Go API | 20K+ 并发连接 | ~10K QPS | 3 实例 |
| Redis | 100K+ QPS | ~10K QPS90% 命中) | 1 主实例 |
| PostgreSQL | 5K QPS | ~1K QPS(缓存穿透) | 1 主 + 1 只读副本 |
> 假设:平均用户每 30 秒发起 1 次 API 请求 → 50000/30 ≈ 1700 QPS;峰值 3x → ~5000 QPS。Redis 缓存承接 90%+ → PG 仅 ~500 QPS。
## 9. 前端架构
### 9.1 页面结构
```
apps/admin-web/src/
├── views/monitoring/
│ ├── MonitoringDashboardView.vue ← 主页
│ ├── MonitoringPlatformView.vue ← 平台占比分析
│ ├── MonitoringCompetitorView.vue ← 竞争对手分析
│ ├── MonitoringTopicView.vue ← 业务主题分析
│ ├── MonitoringQuestionsView.vue ← AI 对话问题
│ └── MonitoringCitationView.vue ← AI 引用排名
├── components/monitoring/
│ ├── ExposureTrendChart.vue ← ECharts 折线图
│ ├── CompetitorBarChart.vue ← 分组柱状图
│ ├── PlatformHorizontalBar.vue ← 横向柱状图
│ ├── CitationDonutChart.vue ← 环形图
│ ├── CitationTrendChart.vue ← 引用趋势折线图
│ ├── BrandImpressionCloud.vue ← 词云
│ ├── HeatIndicator.vue ← 热度指示器(IIIII)
│ ├── SentimentBadge.vue ← 情感标签(优秀/一般)
│ ├── CompetitorRankTable.vue ← 竞品排名表格
│ └── MonitoringFilters.vue ← 通用筛选栏(品牌+平台+时间)
└── lib/
└── api.ts ← 修改:新增 monitoringApi
```
### 9.2 路由配置
`router/index.ts` 中新增,替换现有的 `/tracking` FeatureStubView
```typescript
// 数据追踪 - 主路由组
{
path: '/monitoring',
name: 'monitoring',
redirect: '/monitoring/dashboard',
children: [
{ path: 'dashboard', name: 'monitoring-dashboard', component: MonitoringDashboardView },
{ path: 'platforms', name: 'monitoring-platforms', component: MonitoringPlatformView },
{ path: 'competitors', name: 'monitoring-competitors', component: MonitoringCompetitorView },
{ path: 'topics', name: 'monitoring-topics', component: MonitoringTopicView },
{ path: 'questions', name: 'monitoring-questions', component: MonitoringQuestionsView },
{ path: 'citations', name: 'monitoring-citations', component: MonitoringCitationView },
]
}
```
### 9.3 Chart Library
**选用 ECharts + vue-echarts**
- 已在现有 planPhase 18)中确定
- 原生中文支持,社区生态成熟
- 支持所有所需图表类型:折线图、分组柱状图、横向柱状图、环形图
- 词云使用 `echarts-wordcloud` 扩展
依赖添加:
```json
{
"echarts": "^5.5",
"vue-echarts": "^7.0",
"echarts-wordcloud": "^2.1"
}
```
### 9.4 Shared Types
`packages/shared-types/src/index.ts` 中新增接口:
```typescript
// AI 平台
export interface AIPlatform {
id: number
name: string
icon: string
provider_key: string
}
// 主页
export interface MonitoringOverview {
exposure_rate: number
mention_rate: number
top1_mention_rate: number
positive_rate: number
sentiment_score: number
exposure_rate_change: number
mention_rate_change: number
}
export interface TrendPoint {
date: string
exposure_rate: number
brands: Array<{ name: string; exposure_rate: number }>
}
export interface CompetitorRankItem {
rank: number
name: string
is_self: boolean
icon?: string
sentiment_score: number
sentiment_label: string
exposure_rate: number
}
export interface CitationSourceItem {
source_platform: string
source_type: string
baidu_weight: number
citation_count: number
coverage_rate: number
}
export interface MonitoringQuestionItem {
question_id: number
question_text: string
heat_index: number
exposure_rate: number
avg_position: number
sentiment_label: string
top3_brands: string[]
}
export interface CitationPageItem {
source_platform: string
baidu_weight: number
page_url: string
citation_count: number
coverage_rate: number
}
export interface ImpressionWord {
word: string
weight: number
}
export interface PlatformStatItem {
ai_platform_id: number
platform_name: string
platform_icon: string
mention_count: number
avg_position: number
exposure_rate: number
}
export interface TopicItem {
topic_id: number
topic_name: string
question_count: number
exposure_rate: number
mention_count: number
}
```
### 9.5 API Client
```typescript
export const monitoringApi = {
// 主页
dashboardOverview: (brandId: number, days = 7) =>
http.get<MonitoringOverview>('/monitoring/dashboard/overview', { params: { brand_id: brandId, days } }),
exposureTrend: (brandId: number, days = 7) =>
http.get<TrendPoint[]>('/monitoring/dashboard/exposure-trend', { params: { brand_id: brandId, days } }),
topCompetitors: (brandId: number, days = 7) =>
http.get<CompetitorRankItem[]>('/monitoring/dashboard/competitors', { params: { brand_id: brandId, days } }),
citationSources: (brandId: number, days = 7) =>
http.get<{ type_distribution: ..., top_platforms: CitationSourceItem[] }>('/monitoring/dashboard/citation-sources', { params: { brand_id: brandId, days } }),
recentQuestions: (brandId: number) =>
http.get<MonitoringQuestionItem[]>('/monitoring/dashboard/recent-questions', { params: { brand_id: brandId } }),
brandImpression: (brandId: number, days = 7) =>
http.get<ImpressionWord[]>('/monitoring/dashboard/brand-impression', { params: { brand_id: brandId, days } }),
// 平台占比
platformStats: (brandId: number, days = 7) =>
http.get<PlatformStatItem[]>('/monitoring/platforms/stats', { params: { brand_id: brandId, days } }),
platformQuestions: (brandId: number, platformId: number, days = 7, page = 1) =>
http.get<Paged<MonitoringQuestionItem>>(`/monitoring/platforms/${platformId}/questions`, { params: { brand_id: brandId, days, page } }),
// 竞争对手
competitorExposure: (brandId: number, days = 7) =>
http.get('/monitoring/competitors/exposure', { params: { brand_id: brandId, days } }),
competitorDetail: (brandId: number, days = 7) =>
http.get('/monitoring/competitors/detail', { params: { brand_id: brandId, days } }),
// 业务主题
topics: (brandId: number, platformId = 0, days = 7) =>
http.get<TopicItem[]>('/monitoring/topics', { params: { brand_id: brandId, ai_platform_id: platformId, days } }),
// AI 对话问题
questions: (brandId: number, params: { ai_platform_id?: number; days?: number; page?: number; page_size?: number }) =>
http.get<Paged<MonitoringQuestionItem>>('/monitoring/questions', { params: { brand_id: brandId, ...params } }),
// AI 引用排名
citationTrend: (brandId: number, platformId = 0, days = 7) =>
http.get('/monitoring/citations/trend', { params: { brand_id: brandId, ai_platform_id: platformId, days } }),
citationPages: (brandId: number, days = 7, page = 1, pageSize = 20) =>
http.get<Paged<CitationPageItem>>('/monitoring/citations/pages', { params: { brand_id: brandId, days, page, page_size: pageSize } }),
// 通用
aiPlatforms: () => http.get<AIPlatform[]>('/monitoring/ai-platforms'),
}
```
## 10. 采集流水线设计
### 10.1 采集流程
```
┌──────────┐ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐
│ 调度器 │ │ Redis Stream │ │ Collector │ │ Parser │
│ (cron) │────▶│ / 内部队列 │────▶│ Worker ×5 │────▶│ (解析回答) │
│ 生成任务 │ │ │ │ 调用 AI API │ │ 提取指标 │
└──────────┘ └───────────────┘ └──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ MinIO │ │ PostgreSQL │
│ (原始回答) │ │ (解析结果) │
└─────────────┘ └──────────────┘
```
### 10.2 调度策略
```go
// 每天凌晨 1 点执行
func (c *MonitoringCollector) RunDailyCollection(ctx context.Context, tenantID int64) error {
// 1. 查询该租户所有 active 品牌
brands := listActiveBrands(ctx, tenantID)
for _, brand := range brands {
// 2. 查询品牌下所有 active 问题(含 question_hash 去重)
questions := listUniqueActiveQuestions(ctx, brand.ID)
// 3. 查询所有 active AI 平台
platforms := listActivePlatforms(ctx, tenantID)
// 4. 生成采集任务 = questions × platforms
for _, q := range questions {
for _, p := range platforms {
enqueueTask(ctx, brand, q, p)
}
}
}
}
```
### 10.3 回答解析逻辑
```go
func (c *MonitoringCollector) ParseAnswer(ctx context.Context, question, brand string, competitors []string, rawAnswer string) *ParseResult {
// 1. 品牌提及检测:在回答中查找品牌名出现位置
// 2. 首位提及检测:品牌是否出现在推荐列表第一位
// 3. 首选推荐检测:品牌是否被明确推荐
// 4. 情感分析:使用 LLM 对品牌相关段落做情感判断
// 5. 竞品提及检测:检测 competitors 列表中的品牌出现情况
// 6. 引用 URL 提取:从回答中提取所有外部链接
// 7. 系统文章引用匹配:与 articles 表做 URL/标题匹配
// 8. 品牌印象词提取:使用 LLM 从回答中提取品牌相关形容词
}
```
### 10.4 聚合 Job
```go
// 每天凌晨 3 点执行(在采集完成后)
func (a *MonitoringAggregator) AggregateDaily(ctx context.Context, date time.Time) error {
tenants := listAllTenants(ctx)
for _, t := range tenants {
brands := listActiveBrands(ctx, t.ID)
for _, b := range brands {
// 1. 从 parse_results 聚合 → monitoring_brand_daily_overview
a.aggregateBrandOverview(ctx, t.ID, b.ID, date)
// 2. 聚合 → monitoring_platform_daily / monitoring_platform_question_daily
a.aggregatePlatformStats(ctx, t.ID, b.ID, date)
// 3. 聚合 → monitoring_competitor_daily / monitoring_competitor_platform_daily
a.aggregateCompetitorStats(ctx, t.ID, b.ID, date)
// 4. 聚合 → monitoring_topic_daily
a.aggregateTopicStats(ctx, t.ID, b.ID, date)
// 5. 聚合 → monitoring_question_daily
a.aggregateQuestionStats(ctx, t.ID, b.ID, date)
// 6. 聚合 → monitoring_citation_source_daily / monitoring_citation_page_daily
a.aggregateCitationStats(ctx, t.ID, b.ID, date)
// 7. 聚合 → monitoring_brand_impression
a.aggregateBrandImpression(ctx, t.ID, b.ID, date)
// 8. 清除该品牌缓存
a.InvalidateCache(ctx, t.ID, b.ID)
}
}
}
```
## 11. 部署拓扑(5 万并发)
### 11.1 推荐部署架构
```
┌───────────────────────────┐
│ CDN / 云 LB (SLB) │
│ 静态资源 + API 路由 │
└─────────────┬─────────────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ tenant-api│ │ tenant-api│ │ tenant-api│ ×3 (API 服务)
│ Pod/容器 │ │ Pod/容器 │ │ Pod/容器 │
└────┬──────┘ └────┬──────┘ └────┬──────┘
│ │ │
┌────┴─────────────┴─────────────┴────┐
│ Redis 6+ (单主) │ 缓存 + 会话
│ 或 Redis Cluster │
└───────────────────┬──────────────────┘
┌───────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ PG Primary │ │ PG Replica │ │ MinIO │
│ (读写) │────▶│ (只读) │ │ (原始回答) │
└───────────┘ └───────────┘ └───────────┘
╔════════════════════════════════════════════════════════╗
║ 独立进程 / CronJob(不接入 LB
║ ║
║ ┌─────────────────┐ ┌─────────────────┐ ║
║ │ Collector Worker │ │ Aggregator Job │ ║
║ │ (采集 AI 平台) │ │ (日级聚合) │ ║
║ └─────────────────┘ └─────────────────┘ ║
╚════════════════════════════════════════════════════════╝
```
### 11.2 资源估算
| 组件 | 规格建议 | 说明 |
| --- | --- | --- |
| API 实例 ×3 | 2C4G | Go 服务轻量,3 实例可处理 ~60K 并发连接 |
| Redis | 4G 内存 | 缓存数据量不大,主要是 Key 数量 |
| PostgreSQL Primary | 4C16G | 汇总表小,索引少,500 QPS 绰绰有余 |
| PostgreSQL Replica ×1 | 4C16G | 可选,用于聚合 Job 的重查询分流 |
| MinIO | 50G+ | 原始回答存储,按 90 天保留 |
| Collector Worker ×1 | 2C4G | 采集任务非实时,单实例 5 并发即可 |
| Aggregator Job ×1 | 2C4G | 凌晨运行,单实例即可 |
### 11.3 数据库连接池配置
```yaml
database:
max_open_conns: 50 # 从 25 提升到 50(3 个 API 实例共享)
max_idle_conns: 10
```
按 3 个 API 实例计算:每实例 ~17 连接上限,足够应对缓存穿透后的查询。
## 12. 数据量估算
### 12.1 汇总表大小
假设:100 个品牌,每品牌 5 个竞品,5 个 AI 平台,10 个主题,30 天窗口。
| 汇总表 | 行数/天 | 30 天行数 | 估算大小 |
| --- | --- | --- | --- |
| `monitoring_brand_daily_overview` | 100 | 3,000 | < 1 MB |
| `monitoring_platform_daily` | 500 | 15,000 | < 5 MB |
| `monitoring_platform_question_daily` | 50,000 | 1,500,000 | ~200 MB |
| `monitoring_competitor_daily` | 600 | 18,000 | < 5 MB |
| `monitoring_competitor_platform_daily` | 3,000 | 90,000 | ~15 MB |
| `monitoring_topic_daily` | 1,000 | 30,000 | < 5 MB |
| `monitoring_question_daily` | 50,000 | 1,500,000 | ~250 MB |
| `monitoring_citation_source_daily` | 5,000 | 150,000 | ~25 MB |
| `monitoring_citation_page_daily` | 10,000 | 300,000 | ~60 MB |
| `monitoring_brand_impression` | 5,000 | 150,000 | ~20 MB |
**30 天汇总表总计 < 600 MB**PostgreSQL 完全可控。
### 12.2 原始采集表大小
```
100 品牌 × 100 问题 × 5 平台 × 1 次/天 = 50,000 条/天
90 天保留 = 4,500,000 条原始记录
每条约 1KB(不含原始回答文本)→ ~4.5 GB
原始回答文本(~5KB/条)→ MinIO 约 22 GB/90 天
```
## 13. 实施计划
### Phase 15: 数据库迁移 + AI 平台注册表
- 创建所有新表的 migration 文件
- 种子数据:5 个默认 AI 平台
- 估时:1 天
### Phase 16: 后端 - 监测查询服务 + API
- 实现 `MonitoringService` 的所有查询方法
- 实现 `monitoring_handler.go` 的所有路由
- 实现 Redis 缓存层
- 注册路由到 `router.go`
- 估时:3 天
### Phase 17: 后端 - 采集流水线
- 实现 `MonitoringCollector` + `MonitoringParser`
- 对接 5 个 AI 平台 API(复用现有 `llm.Client` 接口)
- 原始回答存储 MinIO
- 估时:3 天
### Phase 18: 后端 - 聚合 Job
- 实现 `MonitoringAggregator` 的 8 个聚合函数
- 实现缓存失效逻辑
- 实现定时调度(可先用 in-process cron
- 估时:2 天
### Phase 19: 种子数据
- 生成 30 天真实结构的 mock 数据,覆盖所有汇总表
- 确保前端开发可立即使用
- 估时:1 天
### Phase 20: 前端 - 6 页面 + ECharts
- 安装 echarts / vue-echarts / echarts-wordcloud
- 实现 6 个页面 + 10 个图表组件
- 实现通用筛选栏组件
- i18n 扩展
- 估时:4 天
### Phase 21: 前端验证 + 集成测试
- `pnpm typecheck:admin && pnpm build:admin` 通过
- `go test ./...` 通过
- 全链路验证:种子数据 → API → 前端图表渲染
- 估时:1 天
**总估时:约 15 个工作日**
## 14. 风险与决策项
### 14.1 待确认
| 序号 | 决策项 | 影响范围 | 建议 |
| --- | --- | --- | --- |
| 1 | 各 AI 平台 API Key 如何获取和管理 | 采集流水线 | 建议 config.yaml 配置,生产环境用环境变量覆盖 |
| 2 | 业务主题的分类方式(人工还是 LLM 自动归类) | 主题分析页 | V1 建议人工创建主题并关联问题,V2 可自动归类 |
| 3 | 百度权重数据来源 | 引用排名页 | 建议接入第三方 SEO API 或定期手动维护 |
| 4 | 情感分析的准确度要求 | 竞品分析 + 问题列表 | 建议先用规则匹配,重要场景用 LLM 二次判断 |
| 5 | 是否需要支持自定义 AI 平台 | 平台管理 | V1 建议固定 5 个平台,用户不可增删 |
| 6 | 部署环境选择(K8s / Docker Compose / 裸机) | 部署架构 | 开发阶段 Docker Compose,生产建议 K8s |
### 14.2 已确认(沿用 V1 决策)
| 决策 | 来源 |
| --- | --- |
| 问题是最小监控采集单元 | V1 核心结论 |
| 原始回答存对象存储,不存 PG | V1 Section 8.2 |
| 每问题每天默认执行 1 次 | V1 Section 8.3 |
| 问题 hash 去重 | V1 Section 8.4 |
| 问题版本化保证历史口径 | V1 Section 9 |
| 页面只查汇总表 | V1 Section 10 |
| 原始记录保留 90 天 | V1 Section 12 |
## 15. 附录
### 15.1 已有表 DDL 参考
```sql
-- brands(已存在)
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',
...
);
-- brand_keywords(已存在)
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,
...
);
-- brand_questions(已存在)
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,
...
);
-- brand_question_versions(已存在)
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,
...
);
-- competitors(已存在)
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,
...
);
```
### 15.2 指标口径定义
| 指标 | 计算公式 | 说明 |
| --- | --- | --- |
| 曝光度 (exposure_rate) | 品牌在 AI 回答中被提及的问题数 / 采集的总问题数 | 反映品牌在 AI 平台的总体可见度 |
| 提及率 (mention_rate) | 品牌被提及的回答次数 / 总提问次数 | 跨平台汇总 |
| 首位提及率 (top1_mention_rate) | 品牌出现在推荐列表首位的次数 / 总提问次数 | 衡量品牌在 AI 推荐中的优先级 |
| 首选推荐率 (first_recommend_rate) | 品牌被明确推荐的次数 / 总提问次数 | 衡量 AI 对品牌的推荐倾向 |
| 正面提及率 (positive_mention_rate) | 正面情感的提及次数 / 总提及次数 | 衡量品牌的 AI 口碑 |
| 情感得分 (sentiment_score) | 正面提及占比 × 100,修正后 0~100 | 综合情感指标 |
| 热度指数 (heat_index) | 近期被触发的 AI 问答频次标准化后 1~5 档 | 问题热度 |
| 覆盖率 (coverage_rate) | 该来源被引用的问题数 / 总问题数 | 引用来源的覆盖广度 |