duobibi-server
多比比(占坑版)后端 — 比价 / 估价服务。
与傻瓜比价后端同一套代码,部署到独立域名
api.duobibi.com(端口 8766,与旧 8765 并存)。 新增两个多比比专属端点:/arena-quote(比价擂台)、/worth-buy(AI 值不值得买)。 占坑期 LLM key 与极光 key 仍硬编码,上线前迁移到环境变量。
技术栈
- Python 3.11+
- FastAPI 0.115+ / uvicorn (standard) / pydantic 2.9+
- 智谱 zai-sdk(
glm-5-turbo,温度 0.0,确定性输出) - SQLite(WAL 模式,单文件)
- cryptography(极光 RSA 解密手机号)
- pytest + pytest-asyncio + httpx(测试)
工程结构
duobibi-server/
├── app/
│ ├── main.py # FastAPI 应用入口,所有 router 注册,启动时建表
│ ├── schemas.py # 全部请求/响应 Pydantic DTO
│ ├── db.py # SQLite 连接管理 + WAL + 表初始化
│ ├── llm_client.py # 智谱客户端,DUOBIBI_MOCK_LLM 开关
│ ├── extractor.py # Android 控件树 → LLM → title/price 提取
│ ├── mock_extractor.py # 占坑期预置商品池(确定性哈希选)
│ └── api/
│ ├── parse.py # POST /api/v1/parse Android 无障碍树识别
│ ├── parse_image.py # POST /api/v1/parse-image iOS 截图 OCR
│ ├── parse_text.py # POST /api/v1/parse-text iOS 文本新建
│ ├── parse_ocr.py # POST /api/v1/parse-ocr OCR 文本识别(补齐缺失)
│ ├── quick_quote.py # POST /api/v1/quick-quote 用户主动比价查询
│ ├── arena.py # POST /api/v1/arena-quote 比价擂台(多比比新增)
│ ├── worth_buy.py # POST /api/v1/worth-buy AI 值不值(多比比新增)
│ ├── protect.py # POST /api/v1/track-price 价保哨兵
│ ├── auth.py # POST /api/v1/auth/jverify-login 极光一键登录
│ └── wish.py # POST /api/v1/wish/{list,upsert} 心愿单同步
├── tests/ # test_health / test_arena_worth / test_parse_image
├── deploy/
│ ├── duobibi-mock.service # systemd unit(生产)
│ └── nginx/api.duobibi.com.conf
├── pyproject.toml
├── run-backend.ps1 # Windows 一键启动脚本
└── data.db # SQLite(开发本地,生产在 /opt/duobibi-server/data.db)
接口
GET /health
{ "status": "ok" }
POST /api/v1/arena-quote — 比价擂台(多比比新增)
请求:
{ "title": "iPhone 15 Pro 256GB", "platforms": ["淘宝","京东","拼多多","抖音"] }
platforms 可省略(默认 4 大电商)。响应:
{
"title": "iPhone 15 Pro 256GB",
"typical_price": 8299.0,
"quotes": [
{"platform": "淘宝", "price": 8099.0, "note": ""},
{"platform": "京东", "price": 8299.0, "note": "自营"},
{"platform": "拼多多", "price": 7888.0, "note": "百亿补贴"},
{"platform": "抖音", "price": 8190.0, "note": ""}
],
"lowest_platform": "拼多多"
}
- LLM 缺平台报价时,按
(title, platform)哈希确定性合成,保证 4 条都齐 - LLM 整体失败返回 422
llm_no_quote,不抛 5xx - 设
DUOBIBI_MOCK_LLM=1可离线测试
POST /api/v1/worth-buy — AI 值不值得买(多比比新增)
请求:
{ "title": "iPhone 15 Pro 256GB", "price": 8099.0, "platform": "京东" }
响应:
{
"score": 78, "verdict": "buy", "headline": "现在入手划算",
"reasons": ["低于市场常见价约 2%", "距下次大促还有一段时间"],
"typical_price": 8299.0, "best_time": "现在就合适"
}
score: 0–100,80+ →buy/ 40–79 →neutral/ <40 →wait- LLM 抽风 / JSON 解析失败也会返回中性结论,不抛 5xx
- Mock 模式按"当前价 / 市场常见价"水位确定性算分
沿用傻瓜比价的端点
| 端点 | 方法 | 用途 |
|---|---|---|
/api/v1/parse |
POST | Android 无障碍树 → title / price / cluster_id |
/api/v1/parse-image |
POST(multipart) | iOS 截图 OCR(图片 ≤ 5MB) |
/api/v1/parse-text |
POST | iOS 文本新建 |
/api/v1/parse-ocr |
POST | OCR 文本识别(本工程补齐 mock 实现) |
/api/v1/quick-quote |
POST | 用户主动比价查询 |
/api/v1/track-price |
POST | 价保哨兵(确定性日抖动模拟降价) |
/api/v1/auth/jverify-login |
POST | 极光 loginToken → 真手机号(RSA 解密) |
/api/v1/wish/list |
POST | 拉取心愿单(含软删) |
/api/v1/wish/upsert |
POST | 批量上传心愿单修改(LWW) |
修复:旧后端
main.py引用app.api.parse_ocr但文件缺失会导致启动ImportError; 本工程补齐了app/api/parse_ocr.py(mock 实现),服务可正常启动。
包名 → 品牌映射
| 包名 | 品牌 |
|---|---|
| com.taobao.taobao | 淘宝 |
| com.jingdong.app.mall | 京东 |
| com.xunmeng.pinduoduo | 拼多多 |
| com.ss.android.ugc.aweme | 抖音 |
| com.sankuai.meituan | 美团 |
| com.sankuai.meituan.takeoutnew | 美团外卖 |
| me.ele | 饿了么 |
非白名单包名返回 source_app: "未知"。
环境变量
| 变量 | 用途 | 默认值 |
|---|---|---|
DUOBIBI_DB_PATH |
SQLite 数据库路径 | /opt/duobibi-server/data.db(生产)/ ./data.db(本地) |
DUOBIBI_MOCK_LLM |
设为非空值 → 跳过真实 LLM,返回确定性 mock 数据 | 空(生产用真实 LLM) |
JG_PRIVATE_KEY_PATH |
极光一键登录 RSA 私钥路径 | /opt/duobibi-server/secrets/jverify_rsa_private.pem |
PYTHONUTF8 |
Windows 控制台 UTF-8(run-backend.ps1 自动设) |
空 |
硬编码待迁移(app/llm_client.py / app/api/auth.py):智谱 API key、极光 App key、极光 Master Secret。
数据存储
SQLite 单文件 + WAL,启动时 IF NOT EXISTS 建表,无 migration 工具。
user — 占坑期登录用户
| 字段 | 类型 | 说明 |
|---|---|---|
phone |
TEXT PK | 用户手机号(占坑期主键) |
created_at |
INTEGER | 创建时间(ms epoch) |
last_seen_at |
INTEGER | 最近一次登录时间(ms epoch) |
wish_item — 心愿单(跨设备同步)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INTEGER PK AUTO | 自增 |
phone |
TEXT FK | 用户手机号 |
title / target_price / note / notify_enabled |
— | 业务字段 |
created_at / updated_at |
INTEGER | ms epoch |
deleted_at |
INTEGER | NULL = 未删,非空 = 软删,防跨设备"复活" |
索引:idx_wish_phone_updated on (phone, updated_at DESC)。
本地启动
环境要求
- Python 3.11+
- 默认 mock 模式无需任何 API key 即可启动(不调真实 LLM,返回确定性占位数据)
方式一:PowerShell 脚本(Windows 推荐)
.\run-backend.ps1 # mock 模式,监听 127.0.0.1:8766
.\run-backend.ps1 -Lan # 监听 0.0.0.0,供真机/局域网访问
.\run-backend.ps1 -Real # 连真实智谱 LLM(需先配好 key)
.\run-backend.ps1 -Reload # 热重载,代码改动自动重启
.\run-backend.ps1 -Port 8888 # 改端口
方式二:手动 pip + uvicorn
cd duobibi-server
pip install -e ".[dev]"
uvicorn app.main:app --reload --port 8766
首次启动会自动在当前目录创建 data.db(SQLite,WAL 模式),无需手动建表。
验证启动
curl http://localhost:8766/health
# {"status":"ok"}
测试
pytest -q
烟测核心端点
curl -X POST http://localhost:8766/api/v1/arena-quote \
-H 'Content-Type: application/json' \
-d '{"title":"iPhone 15 Pro 256GB"}'
curl -X POST http://localhost:8766/api/v1/worth-buy \
-H 'Content-Type: application/json' \
-d '{"title":"iPhone 15 Pro 256GB","price":8099}'
部署(生产)
- 域名:
api.duobibi.com(HTTPS) - 进程:
uvicorn app.main:app --host 127.0.0.1 --port 8766 --workers 1,systemd 守护 (deploy/duobibi-mock.service) - 反代: nginx → 127.0.0.1:8766(
deploy/nginx/api.duobibi.com.conf,client_max_body_size 6MB) - SQLite 单文件(
DUOBIBI_DB_PATHenv 可覆盖,默认/opt/duobibi-server/data.db) - 极光 RSA 私钥不入 git,部署时单独 scp 到
/opt/duobibi-server/secrets/jverify_rsa_private.pem
待办(占坑期遗留)
app/llm_client.py的智谱 API key 仍硬编码,上线前迁移到环境变量并替换为多比比自己的 keyapp/api/auth.py的极光 App key / Master Secret 同样硬编码,需迁移到 env- 接口无鉴权 / 无限流,后续加
X-Client-Token头校验 + IP 限流 - 无 Dockerfile / docker-compose,目前只支持 systemd 部署