Sin descripción

liujintao 71b89779fe 增加文档的统一格式和安全性(需要用户名和密码访问) hace 1 mes
app 71b89779fe 增加文档的统一格式和安全性(需要用户名和密码访问) hace 1 mes
.env 79834053ef 优化多进程启动逻辑 hace 1 mes
.env.example 3f4aad15d7 update hace 1 mes
.gitignore f60fe81709 update hace 1 mes
README.md 71b89779fe 增加文档的统一格式和安全性(需要用户名和密码访问) hace 1 mes
pyproject.toml 2b258506bc init hace 1 mes
requirements.txt 79834053ef 优化多进程启动逻辑 hace 1 mes
uv.lock 2b258506bc init hace 1 mes

README.md

Zendesk FAQ Keyword Search

基于 FastAPI 的 Zendesk Help Center FAQ 搜索代理,封装 Zendesk Help Center API,自动维护 FAQ Section 缓存并提供统一的关键词搜索接口。

特性

  • FAQ Section 自动发现:扫描 Zendesk Help Center 找出名字含 faq 且文章数 > 0 的 Section
  • Redis 共享缓存:多 worker 进程共享缓存,避免重复打 Zendesk
  • Leader 选举:仅一个 worker 负责刷新缓存,其余 worker 共享读取
  • 每日定时刷新:可配置时间(默认 0:00)自动刷新 sec_ids
  • 故障自愈:leader 进程崩溃后,其他 worker 自动接管
  • 统一响应格式:所有接口返回 {code, data, msg} 信封
  • 优雅降级:Redis 不可用时返回空 sec_ids,搜索不带 section 过滤继续工作

目录结构

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 选举/心跳/定时刷新

快速开始

1. 安装依赖

pip install -r requirements.txt

2. 准备 Redis

# Docker 一行起
docker run -d -p 6379:6379 --name redis redis:7
# 或用 brew (macOS)
brew install redis && brew services start redis

3. 配置环境

生产环境

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)

4. 启动

# 开发环境(自动重载、单 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": "成功"
}

1. 搜索(GET)

curl "http://localhost:8000/search?query=Can%20I%20Add%20a%20PoE%20Switch"

2. 搜索(POST)

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": "成功"
}

3. 健康检查

curl http://localhost:8000/faq/health
# {"code": 0, "data": {"status": "ok"}, "msg": "成功"}

4. 缓存状态

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": "成功"
}

5. 手动刷新缓存

curl -X POST http://localhost:8000/faq/admin/cache/refresh

6. Swagger UI

打开 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 / 测试账号

多 Worker 与 Leader 机制

启动 N 个 worker 时(uvicorn --workers N):

  1. 每个 worker 独立尝试在 Redis 抢 lock:leader仅一个抢到
  2. Leader:拉 Zendesk 写入 Redis、跑 scheduler_loop(每天定时刷新)、跑 leader_heartbeat(每 LEADER_LOCK_TTL/3 秒续期锁)
  3. Follower:仅读 Redis 缓存,不打 Zendesk
  4. Leader 进程崩溃后,LEADER_LOCK_TTL 秒内剩余 worker 接管

Redis Key

faq_search:cache:data       JSON 缓存数据(共享)
faq_search:lock:leader      leader 选举锁
faq_search:lock:refresh     防并发刷新锁

Redis 故障降级

  • Redis 完全挂掉get_snapshot() 返回空快照,搜索不带 section 过滤继续工作(结果可能包含非 FAQ 文章)
  • Leader 长时间卡死LEADER_LOCK_TTL 后锁过期,其他 worker 抢锁接管
  • 手动刷新POST /faq/admin/cache/refreshforce=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