- VideoAnalysis 组件性能优化:使用 memo/useMemo/useCallback,添加详情缓存和虚拟滚动 - 修复 Ant Design Modal/Descriptions/Table 内文字无法复制的问题 - 新增 AntdProvider 组件,解决 layout.tsx 不能加 'use client' 的问题 - 添加云图 API 参数测试,更新 CLAUDE.md 文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
518 lines
15 KiB
Markdown
518 lines
15 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## 交互规范
|
||
|
||
- 开始任务时说:**主人,开始了**
|
||
- 完成任务时说:**主人,我干完了.您看看**
|
||
|
||
## 项目概述
|
||
|
||
KOL Insight 是一个 KOL(关键意见领袖)数据查询与分析工具,用于批量查询达人视频数据并计算预估成本指标。
|
||
|
||
**技术栈**:
|
||
- **前端**: Next.js 14.x (App Router) + React + TypeScript + Tailwind CSS
|
||
- **后端**: Python FastAPI 0.104+ + SQLAlchemy 2.0+ (异步 ORM) + asyncpg
|
||
- **数据库**: PostgreSQL 14.x+
|
||
- **部署**: Docker + Uvicorn (ASGI 服务器)
|
||
|
||
## 常用命令
|
||
|
||
### 前端开发
|
||
|
||
```bash
|
||
# 安装依赖
|
||
pnpm install
|
||
|
||
# 开发模式(热重载)
|
||
pnpm dev
|
||
|
||
# 构建生产版本
|
||
pnpm build
|
||
|
||
# 生产模式运行
|
||
pnpm start
|
||
|
||
# 代码检查
|
||
pnpm lint
|
||
|
||
# 类型检查
|
||
pnpm type-check # 如果配置了此脚本
|
||
```
|
||
|
||
### 后端开发
|
||
|
||
```bash
|
||
# 安装依赖
|
||
pip install -r requirements.txt
|
||
# 或使用 Poetry
|
||
poetry install
|
||
|
||
# 开发模式运行(热重载)
|
||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||
|
||
# 生产模式运行
|
||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||
|
||
# 运行测试(TDD 必须)
|
||
pytest
|
||
|
||
# 运行测试并生成覆盖率报告
|
||
pytest --cov=app --cov-report=html
|
||
|
||
# 运行特定测试文件
|
||
pytest tests/test_query_service.py
|
||
|
||
# 运行特定测试函数
|
||
pytest tests/test_query_service.py::test_query_by_star_id
|
||
```
|
||
|
||
### 数据库操作
|
||
|
||
```bash
|
||
# 连接数据库(使用 .env 中的连接字符串)
|
||
psql "postgresql://user:password@host:5432/yuntu_kol"
|
||
|
||
# 创建迁移
|
||
alembic revision --autogenerate -m "description"
|
||
|
||
# 执行迁移
|
||
alembic upgrade head
|
||
|
||
# 回滚迁移
|
||
alembic downgrade -1
|
||
```
|
||
|
||
### Docker 部署
|
||
|
||
```bash
|
||
# 构建并启动所有服务(前端、后端、数据库)
|
||
docker-compose up -d
|
||
|
||
# 查看日志
|
||
docker-compose logs -f
|
||
|
||
# 停止所有服务
|
||
docker-compose down
|
||
|
||
# 重新构建并启动
|
||
docker-compose up -d --build
|
||
```
|
||
|
||
## 架构设计
|
||
|
||
### 前后端分离架构
|
||
|
||
```
|
||
┌─────────────────────┐
|
||
│ Next.js 前端 │ 端口: 3000
|
||
│ (纯前端渲染) │
|
||
└──────────┬──────────┘
|
||
│ HTTP API 调用
|
||
▼
|
||
┌─────────────────────┐
|
||
│ FastAPI 后端 │ 端口: 8000
|
||
│ (异步 API) │
|
||
└──────────┬──────────┘
|
||
│ asyncpg
|
||
▼
|
||
┌─────────────────────┐
|
||
│ PostgreSQL │ 端口: 5432
|
||
└─────────────────────┘
|
||
```
|
||
|
||
**前后端分离的关键点**:
|
||
- 前端通过 HTTP 调用后端 API(需要配置 CORS)
|
||
- 前端环境变量: `NEXT_PUBLIC_API_URL` 指向后端地址
|
||
- 后端环境变量: `CORS_ORIGINS` 配置允许的前端域名
|
||
- 独立部署:前端可部署到 Vercel,后端部署到 Docker
|
||
|
||
### 核心模块
|
||
|
||
1. **查询模块** (`backend/app/services/query_service.py`)
|
||
- 支持三种查询方式:星图ID(star_id)、达人ID(star_unique_id)、昵称(star_nickname)
|
||
- 星图ID 和达人ID 使用精准匹配(WHERE IN)
|
||
- 昵称使用模糊匹配(WHERE LIKE '%昵称%')
|
||
- 单次查询限制最大 1000 条
|
||
|
||
2. **计算模块** (`backend/app/services/calculator.py`)
|
||
- **预估自然CPM**: `(estimated_video_cost / natural_play_cnt) * 1000`
|
||
- **预估自然看后搜人数**: `(natural_play_cnt / total_play_cnt) * after_view_search_uv`
|
||
- **预估自然看后搜人数成本**: `estimated_video_cost / 预估自然看后搜人数`
|
||
- 除零检查:分母为 0 时返回 `None`
|
||
- 结果保留 2 位小数:使用 `round(value, 2)`
|
||
|
||
3. **品牌API集成模块** (`backend/app/services/brand_api.py`)
|
||
- **批量并发调用**:从查询结果提取唯一 `brand_id`,批量调用品牌API
|
||
- **并发控制**:使用 `asyncio.Semaphore` 限制最大 10 个并发请求
|
||
- **超时设置**:单个请求超时 3 秒
|
||
- **降级策略**:API 调用失败时显示原始 `brand_id`
|
||
- **技术实现**:使用 `httpx.AsyncClient` + `asyncio.gather`
|
||
|
||
4. **导出模块** (`backend/app/services/export_service.py`)
|
||
- 支持 Excel (使用 `openpyxl` 或 `xlsxwriter`) 和 CSV 格式
|
||
- 使用中文列名作为表头
|
||
- 限制单次导出最大 1000 条
|
||
|
||
### 数据流向
|
||
|
||
```
|
||
用户输入查询条件 (前端)
|
||
↓
|
||
POST /api/v1/query (后端)
|
||
↓
|
||
查询数据库 (SQLAlchemy 异步)
|
||
↓
|
||
提取唯一 brand_id → 批量调用品牌API (httpx 异步,并发10)
|
||
↓
|
||
填充品牌名称 → 计算预估指标
|
||
↓
|
||
返回完整数据 (JSON)
|
||
↓
|
||
前端展示结果表格
|
||
↓
|
||
用户点击导出 → GET /api/v1/export → 下载 Excel/CSV
|
||
```
|
||
|
||
## 数据库设计
|
||
|
||
### KolVideo 模型
|
||
|
||
```python
|
||
class KolVideo(Base):
|
||
__tablename__ = "kol_videos"
|
||
|
||
# 主键
|
||
item_id = Column(String, primary_key=True)
|
||
|
||
# 查询字段(必须建立索引)
|
||
star_id = Column(String, nullable=False, index=True) # 星图ID
|
||
star_unique_id = Column(String, nullable=False, index=True) # 达人unique_id
|
||
star_nickname = Column(String, nullable=False, index=True) # 达人昵称
|
||
|
||
# 基础信息
|
||
title = Column(String, nullable=True)
|
||
viral_type = Column(String, nullable=True)
|
||
video_url = Column(String, nullable=True)
|
||
publish_time = Column(DateTime, nullable=True)
|
||
|
||
# 曝光指标(用于计算)
|
||
natural_play_cnt = Column(Integer, default=0) # 自然播放量
|
||
heated_play_cnt = Column(Integer, default=0) # 加热播放量
|
||
total_play_cnt = Column(Integer, default=0) # 总播放量
|
||
|
||
# 互动指标
|
||
total_interact = Column(Integer, default=0)
|
||
like_cnt = Column(Integer, default=0)
|
||
share_cnt = Column(Integer, default=0)
|
||
comment_cnt = Column(Integer, default=0)
|
||
|
||
# 效果指标(用于计算)
|
||
new_a3_rate = Column(Float, nullable=True)
|
||
after_view_search_uv = Column(Integer, default=0) # 看后搜人数
|
||
return_search_cnt = Column(Integer, default=0)
|
||
|
||
# 商业信息
|
||
industry_id = Column(String, nullable=True)
|
||
industry_name = Column(String, nullable=True)
|
||
brand_id = Column(String, nullable=True) # 用于调用品牌API
|
||
estimated_video_cost = Column(Float, default=0) # 预估视频成本(用于计算)
|
||
```
|
||
|
||
**索引策略**:
|
||
- `idx_star_id`: 星图ID 精准查询
|
||
- `idx_star_unique_id`: 达人ID 精准查询
|
||
- `idx_star_nickname`: 昵称模糊查询(需要支持 LIKE)
|
||
|
||
## API 设计
|
||
|
||
### POST /api/v1/query
|
||
|
||
批量查询 KOL 视频数据。
|
||
|
||
**请求体**:
|
||
```python
|
||
{
|
||
"type": "star_id" | "unique_id" | "nickname", # 查询方式
|
||
"values": ["id1", "id2", ...] # 批量ID或单个昵称
|
||
}
|
||
```
|
||
|
||
**响应体**:
|
||
```python
|
||
{
|
||
"success": true,
|
||
"data": [
|
||
{
|
||
"item_id": "...",
|
||
"title": "...",
|
||
# ... 26个字段
|
||
"brand_name": "...", # 后端填充
|
||
"estimated_natural_cpm": 12.34, # 后端计算
|
||
# ...
|
||
}
|
||
],
|
||
"total": 100
|
||
}
|
||
```
|
||
|
||
### GET /api/v1/export
|
||
|
||
导出查询结果。
|
||
|
||
**查询参数**:
|
||
- `format`: `xlsx` 或 `csv`
|
||
|
||
**响应**: 文件下载(`Content-Disposition: attachment`)
|
||
|
||
## 开发规范
|
||
|
||
### TDD(测试驱动开发)要求
|
||
|
||
**这个项目强制使用 TDD,必须先写测试再写实现代码。**
|
||
|
||
1. **单元测试覆盖率要求**: 100%(所有分支覆盖)
|
||
2. **集成测试要求**: 使用真实数据库连接(.env 中的连接字符串)
|
||
3. **测试框架**: pytest + pytest-cov
|
||
4. **测试文件位置**: `backend/tests/`
|
||
|
||
**TDD 流程**:
|
||
```
|
||
1. 编写测试用例 (tests/test_*.py)
|
||
2. 运行测试(应该失败)
|
||
3. 编写最小实现代码
|
||
4. 运行测试(应该通过)
|
||
5. 重构代码(保持测试通过)
|
||
6. 生成覆盖率报告验证 100% 覆盖
|
||
```
|
||
|
||
**测试示例**:
|
||
```python
|
||
# tests/test_calculator.py
|
||
def test_calculate_natural_cpm():
|
||
# 正常情况
|
||
result = calculate_natural_cpm(1000, 50000)
|
||
assert result == 20.0
|
||
|
||
# 除零情况
|
||
result = calculate_natural_cpm(1000, 0)
|
||
assert result is None
|
||
|
||
# tests/test_query_service.py
|
||
@pytest.mark.asyncio
|
||
async def test_query_by_star_id(db_session):
|
||
# 使用真实数据库连接
|
||
result = await query_videos(
|
||
db_session,
|
||
query_type="star_id",
|
||
values=["test_id_1", "test_id_2"]
|
||
)
|
||
assert len(result) > 0
|
||
```
|
||
|
||
### 异步编程规范
|
||
|
||
**后端必须使用异步编程以提升性能。**
|
||
|
||
1. **数据库操作**: 使用 SQLAlchemy 异步 API
|
||
```python
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
async def query_videos(session: AsyncSession, ...):
|
||
stmt = select(KolVideo).where(...)
|
||
result = await session.execute(stmt)
|
||
return result.scalars().all()
|
||
```
|
||
|
||
2. **外部API调用**: 使用 httpx.AsyncClient
|
||
```python
|
||
async with httpx.AsyncClient(timeout=3.0) as client:
|
||
response = await client.get(url)
|
||
```
|
||
|
||
3. **并发控制**: 使用 asyncio.Semaphore
|
||
```python
|
||
semaphore = asyncio.Semaphore(10) # 限制10并发
|
||
async with semaphore:
|
||
# 执行异步任务
|
||
```
|
||
|
||
### 前端实现规范
|
||
|
||
**前端采用"粗略实现"策略:重点在功能可用,样式可简化。**
|
||
|
||
1. **组件化开发**: 使用 React 组件(QueryForm, ResultTable, ExportButton)
|
||
2. **API 调用封装**: 在 `lib/api.ts` 中统一封装
|
||
3. **环境变量**: 使用 `NEXT_PUBLIC_API_URL` 配置后端地址
|
||
4. **状态管理**: 页面状态包括:默认态、输入态、查询中、结果态、空结果态、错误态
|
||
|
||
### 错误处理规范
|
||
|
||
1. **后端**:
|
||
- 所有 API 路由使用 try-except 包裹
|
||
- 数据库连接失败、品牌API超时等场景有降级处理
|
||
- 错误信息记录到日志
|
||
|
||
2. **前端**:
|
||
- 网络错误显示用户友好提示
|
||
- 空结果显示引导文案
|
||
|
||
### 性能要求
|
||
|
||
- 查询响应时间 ≤ 3 秒(100 条数据)
|
||
- 页面加载时间 ≤ 2 秒
|
||
- 导出响应时间 ≤ 5 秒(1000 条数据)
|
||
- 品牌 API 并发限制: 10 个请求,单请求超时 3 秒
|
||
|
||
## 环境变量配置
|
||
|
||
### 前端 (.env.local)
|
||
|
||
```env
|
||
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
|
||
```
|
||
|
||
### 后端 (.env)
|
||
|
||
```env
|
||
DATABASE_URL=postgresql://user:password@host:5432/yuntu_kol
|
||
CORS_ORIGINS=http://localhost:3000,https://your-frontend-domain.com
|
||
BRAND_API_BASE_URL=https://api.internal.intelligrow.cn
|
||
```
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
kol-insight/
|
||
├── frontend/ # 前端项目(Next.js)
|
||
│ ├── src/
|
||
│ │ ├── app/ # App Router 路由
|
||
│ │ ├── components/ # React 组件
|
||
│ │ ├── lib/ # API 调用、工具函数
|
||
│ │ └── types/ # TypeScript 类型定义
|
||
│ └── package.json
|
||
│
|
||
├── backend/ # 后端项目(FastAPI)
|
||
│ ├── app/
|
||
│ │ ├── main.py # FastAPI 应用入口
|
||
│ │ ├── config.py # 配置管理
|
||
│ │ ├── database.py # 数据库连接
|
||
│ │ ├── models/ # SQLAlchemy 模型
|
||
│ │ ├── schemas/ # Pydantic 请求/响应模型
|
||
│ │ ├── api/v1/ # API 路由
|
||
│ │ └── services/ # 业务逻辑
|
||
│ ├── tests/ # 测试文件(TDD 必须)
|
||
│ └── requirements.txt
|
||
│
|
||
├── doc/ # 项目文档
|
||
│ ├── PRD.md # 产品需求文档
|
||
│ ├── FeatureSummary.md # 功能摘要
|
||
│ ├── UIDesign.md # UI 设计
|
||
│ ├── DevelopmentPlan.md # 开发计划
|
||
│ └── tasks.md # 任务列表
|
||
│
|
||
├── docker-compose.yml # Docker 编排
|
||
└── README.md
|
||
```
|
||
|
||
## 关键技术决策
|
||
|
||
### 为什么使用 FastAPI?
|
||
|
||
- 原生支持异步编程(async/await)
|
||
- 自动生成 OpenAPI 文档(Swagger UI)
|
||
- Pydantic 类型验证,类型安全
|
||
- 性能优异(基于 Starlette 和 Pydantic)
|
||
|
||
### 为什么使用前后端分离?
|
||
|
||
- 前端可独立部署到 Vercel 等平台
|
||
- 后端可独立扩展和优化
|
||
- 职责分离:前端专注 UI,后端专注业务逻辑
|
||
- 便于团队协作(前端/后端可并行开发)
|
||
|
||
### 为什么强制 TDD + 100% 覆盖率?
|
||
|
||
- 保证代码质量和可维护性
|
||
- 避免回归问题
|
||
- 文档化代码行为(测试即文档)
|
||
- 便于重构(测试作为安全网)
|
||
|
||
### 为什么品牌API在后端调用?
|
||
|
||
- 避免前端暴露内部API地址
|
||
- 统一错误处理和降级逻辑
|
||
- 利用后端异步能力优化并发性能
|
||
- 减少前端复杂度
|
||
|
||
## 常见问题
|
||
|
||
### Q: 如何运行单个测试?
|
||
```bash
|
||
pytest tests/test_calculator.py::test_calculate_natural_cpm -v
|
||
```
|
||
|
||
### Q: 如何查看测试覆盖率?
|
||
```bash
|
||
pytest --cov=app --cov-report=html
|
||
# 打开 htmlcov/index.html 查看详细报告
|
||
```
|
||
|
||
### Q: 前端如何调用后端API?
|
||
在 `lib/api.ts` 中封装:
|
||
```typescript
|
||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1';
|
||
|
||
export async function queryVideos(request: QueryRequest): Promise<QueryResponse> {
|
||
const response = await fetch(`${API_BASE_URL}/query`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(request),
|
||
});
|
||
return response.json();
|
||
}
|
||
```
|
||
|
||
### Q: 如何调试品牌API批量调用?
|
||
1. 检查后端日志(应该记录API调用状态)
|
||
2. 使用断点调试 `services/brand_api.py`
|
||
3. 验证并发控制和超时设置是否生效
|
||
|
||
### Q: 数据库索引如何验证?
|
||
```sql
|
||
-- 连接数据库后执行
|
||
\d kol_videos
|
||
-- 应该看到 idx_star_id, idx_star_unique_id, idx_star_nickname
|
||
```
|
||
|
||
## 前后端数据结构一致性
|
||
|
||
**重要**:后端返回的 JSON 字段名必须与前端 TypeScript 类型定义完全匹配。
|
||
- 前端类型定义位置:`frontend/src/types/index.ts`
|
||
- 修改后端响应结构时,务必同步检查前端类型定义
|
||
- 常见错误:`Cannot read properties of undefined` 通常是字段名不匹配
|
||
|
||
## 外部 API 参数格式
|
||
|
||
### 云图 API (GetContentMaterialAnalysisInfo)
|
||
- 日期格式:`YYYYMMDD`(不是 `YYYY-MM-DD`)
|
||
- industry_id:数组格式 `["20"]`(不是字符串)
|
||
- Cookie:直接使用 `sessionid=xxx` 格式
|
||
|
||
### 品牌 API
|
||
- URL 格式:`/v1/yuntu/brands?brand_id=xxx`(查询参数,非路径参数)
|
||
- 认证:`Authorization: Bearer {token}`
|
||
|
||
## 前端常见问题
|
||
|
||
- **Next.js 模块错误**:清理缓存 `rm -rf .next node_modules/.cache && pnpm build`
|
||
- **Ant Design Modal 文字无法复制**:需要多层修复:
|
||
1. Modal: `styles={{ body: { userSelect: 'text' } }}`
|
||
2. Descriptions: 添加 `contentStyle={{ userSelect: 'text', cursor: 'text' }}`
|
||
3. globals.css: 添加 `.ant-descriptions td * { user-select: text !important; }`
|
||
- **Next.js layout.tsx 不能加 'use client'**:因为导出 metadata,需创建单独的 Provider 组件(如 AntdProvider.tsx)
|
||
- **Ant Design v6 ConfigProvider**:v6 不支持 `theme.cssVar` 和 `theme.hashed`,直接用 `<ConfigProvider locale={zhCN}>` 即可
|
||
- **前端性能优化**:使用 `useMemo` 包裹 columns、`useCallback` 包裹事件处理器、`memo` 包裹子组件
|
||
- **CORS 400 错误**:检查后端 `CORSMiddleware` 配置的 `allow_origins`
|