231 lines
8.6 KiB
Markdown
231 lines
8.6 KiB
Markdown
# 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`: 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 推荐)**
|
||
|
||
```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 部署
|