|
|
1 hónapja | |
|---|---|---|
| app | 1 hónapja | |
| .env | 1 hónapja | |
| .env.example | 1 hónapja | |
| .gitignore | 1 hónapja | |
| README.md | 1 hónapja | |
| pyproject.toml | 1 hónapja | |
| requirements.txt | 1 hónapja | |
| uv.lock | 1 hónapja |
基于 FastAPI 的 Zendesk Help Center FAQ 搜索代理,封装 Zendesk Help Center API,自动维护 FAQ Section 缓存并提供统一的关键词搜索接口。
faq 且文章数 > 0 的 Section{code, data, msg} 信封app/
├── main.py # FastAPI 入口、lifespan、leader 选举、健康检查
├── config.py # 配置加载(按 APP_ENV 切换 .env / .env.dev)
├── schemas.py # 请求/响应 Pydantic 模型
├── response.py # 统一响应类 + 业务错误码 + BusinessError
├── exception_handlers.py # 全局异常处理器
├── routers/
│ └── search.py # /search 接口
└── services/
├── zendesk_client.py # 异步 Zendesk 客户端
├── redis_client.py # Redis 连接 + 分布式锁原语
└── cache.py # 共享缓存读写 + leader 选举/心跳/定时刷新
pip install -r requirements.txt
# Docker 一行起
docker run -d -p 6379:6379 --name redis redis:7
# 或用 brew (macOS)
brew install redis && brew services start redis
cp .env.example .env
# 编辑 .env 填入真实 Zendesk 凭据和 Redis 密码
cp .env.example .env.dev
# 编辑 .env.dev 填入开发凭据
# 注意:开发环境用 db=1 与生产隔离(REDIS_URL=redis://127.0.0.1:6379/1)
# 开发环境(自动重载、单 worker)
APP_ENV=dev uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
# 生产环境(多 worker)
APP_ENV=prod uvicorn app.main:app --host 0.0.0.0 --port 8025 --workers 3
# 或省略 APP_ENV(默认就是 prod)
uvicorn app.main:app --host 0.0.0.0 --port 8025 --workers 3
启动日志中会看到 leader 选举:
[14408-abc] 当选 leader,开始首次刷新
[14408-abc] 开始刷新 FAQ 缓存…
[14409-def] 不是 leader,等待缓存就绪…
[14410-ghi] 不是 leader,等待缓存就绪…
[14408-abc] FAQ 缓存刷新完成:sec_ids=N 条,下一次刷新 ...
[14409-def] 共享缓存已就绪
[14410-ghi] 共享缓存已就绪
所有接口返回统一格式:
{
"code": 0,
"data": {},
"msg": "成功"
}
curl "http://localhost:8000/search?query=Can%20I%20Add%20a%20PoE%20Switch"
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-d '{"query": "Can I Add a PoE Switch", "page": 1, "per_page": 25}'
成功响应:
{
"code": 0,
"data": {
"query": "Can I Add a PoE Switch",
"count": 3,
"page": 1,
"per_page": 25,
"next_page": null,
"sec_ids_used": [48252627506585, 47684283137817],
"results": [{"title": "...", "html_url": "...", "snippet": "..."}]
},
"msg": "成功"
}
curl http://localhost:8000/faq/health
# {"code": 0, "data": {"status": "ok"}, "msg": "成功"}
curl http://localhost:8000/faq/health/cache
{
"code": 0,
"data": {
"sec_ids_count": 30,
"last_updated_at": "2026-06-15T09:12:33",
"next_refresh_at": "2026-06-16T00:00:00"
},
"msg": "成功"
}
curl -X POST http://localhost:8000/faq/admin/cache/refresh
打开 http://localhost:8000/faq/docs。
| code | 含义 | HTTP 状态 |
|---|---|---|
| 0 | 成功 | 200 |
| 1001 | 参数验证失败(如 page=-1) | 422 |
| 1002 | 请求格式不正确 | 400 |
| 1003 | 请求方法不允许 | 405 |
| 1004 | 资源不存在 | 404 |
| 2001 | FAQ 缓存未就绪 | 200 |
| 2002 | 业务资源不存在 | 200 |
| 3001 | 上游服务调用失败(Zendesk) | 200 |
| 3002 | 上游服务响应超时 | 200 |
| 9999 | 服务器内部错误 | 500 |
参数错误响应示例(?query=hi&page=-1):
{
"code": 1001,
"data": [{"loc": ["query", "page"], "msg": "Input should be greater than or equal to 1", "type": "greater_than_equal"}],
"msg": "参数验证失败"
}
通过 APP_ENV 选择加载哪个 env 文件:
APP_ENV |
加载文件 |
|---|---|
dev |
.env.dev |
prod(默认) |
.env |
| 变量 | 默认 | 说明 |
|---|---|---|
ZENDESK_SUBDOMAIN |
— | Zendesk 子域名,例:zositechhelp |
ZENDESK_EMAIL |
— | Zendesk 账号邮箱 |
ZENDESK_API_TOKEN |
— | Zendesk API Token |
DEFAULT_LOCALE |
en-us |
默认语言 |
CACHE_REFRESH_HOUR |
0 |
每日刷新小时(24 小时制,本地时区) |
CACHE_REFRESH_MINUTE |
0 |
每日刷新分钟 |
HTTP_TIMEOUT |
30 |
httpx 超时秒数 |
REDIS_URL |
redis://127.0.0.1:6379/0 |
Redis 连接 URL |
REDIS_PASSWORD |
— | Redis 密码(如有) |
REDIS_USERNAME |
— | Redis 用户名(ACL 模式才需要) |
REDIS_KEY_PREFIX |
faq_search |
所有 Redis Key 的前缀 |
LEADER_LOCK_TTL |
60 |
leader 锁 TTL,决定故障切换延迟 |
CACHE_READY_WAIT |
30 |
follower 等待缓存就绪的最大秒数 |
| 项 | 生产 | 开发 |
|---|---|---|
| Redis db | 0 |
1(避免污染生产数据) |
REDIS_KEY_PREFIX |
faq_search |
faq_search_dev |
| 启动 | --workers 3 |
--reload(单 worker) |
| Zendesk 凭据 | 生产账号 | sandbox / 测试账号 |
启动 N 个 worker 时(uvicorn --workers N):
lock:leader,仅一个抢到scheduler_loop(每天定时刷新)、跑 leader_heartbeat(每 LEADER_LOCK_TTL/3 秒续期锁)LEADER_LOCK_TTL 秒内剩余 worker 接管Redis Key:
faq_search:cache:data JSON 缓存数据(共享)
faq_search:lock:leader leader 选举锁
faq_search:lock:refresh 防并发刷新锁
get_snapshot() 返回空快照,搜索不带 section 过滤继续工作(结果可能包含非 FAQ 文章)LEADER_LOCK_TTL 后锁过期,其他 worker 抢锁接管POST /faq/admin/cache/refresh 用 force=True 跳过去重锁,立即生效# 仅启动开发环境
APP_ENV=dev uvicorn app.main:app --reload
# 用 redis-cli 看缓存
redis-cli -n 1 get faq_search_dev:cache:data | python -m json.tool
# 手动清空开发缓存
redis-cli -n 1 del faq_search_dev:cache:data faq_search_dev:lock:leader
# 查看当前 leader 是哪个 worker
redis-cli -n 1 get faq_search_dev:lock:leader