Files

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: 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 推荐)

.\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_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 部署