Files
2026-05-29 13:27:41 +08:00

231 lines
8.6 KiB
Markdown
Raw Permalink 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.
# 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`
```json
{ "status": "ok" }
```
### `POST /api/v1/arena-quote` — 比价擂台(多比比新增)
请求:
```json
{ "title": "iPhone 15 Pro 256GB", "platforms": ["淘宝","京东","拼多多","抖音"] }
```
`platforms` 可省略(默认 4 大电商)。响应:
```json
{
"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 值不值得买(多比比新增)
请求:
```json
{ "title": "iPhone 15 Pro 256GB", "price": 8099.0, "platform": "京东" }
```
响应:
```json
{
"score": 78, "verdict": "buy", "headline": "现在入手划算",
"reasons": ["低于市场常见价约 2%", "距下次大促还有一段时间"],
"typical_price": 8299.0, "best_time": "现在就合适"
}
```
- `score`: 0100,80+ → `buy` / 4079 → `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 推荐)**
```powershell
.\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**
```bash
cd duobibi-server
pip install -e ".[dev]"
uvicorn app.main:app --reload --port 8766
```
首次启动会自动在当前目录创建 `data.db`(SQLite,WAL 模式),无需手动建表。
**验证启动**
```bash
curl http://localhost:8766/health
# {"status":"ok"}
```
**测试**
```bash
pytest -q
```
**烟测核心端点**
```bash
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_PATH` env 可覆盖,默认 `/opt/duobibi-server/data.db`)
- 极光 RSA 私钥不入 git,部署时单独 scp 到 `/opt/duobibi-server/secrets/jverify_rsa_private.pem`
## 待办(占坑期遗留)
- `app/llm_client.py` 的智谱 API key 仍硬编码,上线前迁移到环境变量并替换为多比比自己的 key
- `app/api/auth.py` 的极光 App key / Master Secret 同样硬编码,需迁移到 env
- 接口无鉴权 / 无限流,后续加 `X-Client-Token` 头校验 + IP 限流
- 无 Dockerfile / docker-compose,目前只支持 systemd 部署