实现下单任务网关与本地 worker
按 docs/order-gateway.md 落地:第三个部署单元 app.gateway(:31109)承担任务队列 + 状态镜像;本地 worker 在 app.trading.worker 内,按 RAKUTEN_ORDER_GATEWAY_URL 决定是否启动。规格 §5 最关键约束已守:租约过期绝不自动重投,恢复只能 reclaim, worker 收到 lease_count>1 时先核对站点订单。 站点交互(加购/下单/付款/订单列表反查)按规格 §10 留接口缝,site_interact.py 全部 NotImplementedError,verify.py 恒返回 unknown——等真实账号实测后再填, 不写猜测的提交逻辑。 310 个测试全绿,覆盖规格 §9 验收清单 12 条;架构测试守住三方互不 import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,36 +9,42 @@
|
||||
|
||||
两站均支持**搜索**、**商品详情**、**商家信息**与**商家名下商品**。
|
||||
|
||||
## 两个部署单元
|
||||
## 三个部署单元
|
||||
|
||||
同一个仓库出**两个服务**,分进程运行:
|
||||
同一个仓库出**三个服务**,分进程运行:
|
||||
|
||||
| | 抓取服务 `app.scraping` | 交易服务 `app.trading` |
|
||||
| --- | --- | --- |
|
||||
| 启动 | `python -m app.scraping.main`(:31107) | `python -m app.trading.main`(:31108) |
|
||||
| 账号 | 全程匿名 | 必须带登录态 cookie |
|
||||
| 状态 | 无状态,请求-响应 | 有状态:订单、页面证据、付款进度 |
|
||||
| 失败重试 | 幂等,重试无代价 | **不可逆**,重复提交即重复下单 |
|
||||
| 实例数 | 想开几个开几个 | **只能一个**(或按账号分片) |
|
||||
| 出口 IP | 被限速换掉即可 | 频繁漂移会触发风控 |
|
||||
| | 抓取服务 `app.scraping` | 交易服务 `app.trading` | 下单任务网关 `app.gateway` |
|
||||
| --- | --- | --- | --- |
|
||||
| 启动 | `python -m app.scraping.main`(:31107) | `python -m app.trading.main`(:31108) | `python -m app.gateway.main`(:31109) |
|
||||
| 部署位置 | 服务器 | 本地(NAT 后) | 服务器 |
|
||||
| 账号 | 全程匿名 | 必须带登录态 cookie | 不接触账号 |
|
||||
| 状态 | 无状态,请求-响应 | 有状态:订单、页面证据、付款进度 | 有状态:任务队列 + 状态镜像 |
|
||||
| 失败重试 | 幂等,重试无代价 | **不可逆**,重复提交即重复下单 | 任务编排侧,**不自动重投** |
|
||||
| 实例数 | 想开几个开几个 | **只能一个**(或按账号分片) | **只能一个**(SQLite + 全局并发度 1) |
|
||||
| 出口 IP | 被限速换掉即可 | 频繁漂移会触发风控 | 不出站到站点 |
|
||||
|
||||
拆开的决定性理由是「实例数」那一行,而不是「要不要登录」:登录态 cookie 全局唯一、
|
||||
订单监控是常驻轮询,一旦与抓取同进程,抓取横向扩容就会把登录态和轮询任务复制 N 份,
|
||||
让同一个账号被多个进程并发操作。
|
||||
抓取与交易拆开的决定性理由是「实例数」那一行,而不是「要不要登录」:登录态 cookie
|
||||
全局唯一、订单监控是常驻轮询,一旦与抓取同进程,抓取横向扩容就会把登录态和轮询任务
|
||||
复制 N 份,让同一个账号被多个进程并发操作。
|
||||
|
||||
依赖方向固定为 `scraping → shared`、`trading → shared`,两侧互不 import
|
||||
(`tests/test_architecture.py` 会守着)。交易侧需要商品信息时,走抓取服务的 HTTP 接口——
|
||||
下单要用的 `purchase` 块本来就是抓取服务的对外契约。
|
||||
网关单独成第三个部署单元,是因为它**有状态**(任务队列),不能塞进可多开的抓取服务,
|
||||
也不能塞进在本地、零入站端口的交易服务——本地通过出站长轮询从这里领任务。
|
||||
完整规格见 [docs/order-gateway.md](docs/order-gateway.md)。
|
||||
|
||||
依赖方向固定为 `scraping → shared`、`trading → shared`、`gateway → shared`,三方互不 import
|
||||
(`tests/test_architecture.py` 会守着)。交易侧需要商品信息时走抓取服务的 HTTP 接口,
|
||||
需要任务调度时走网关的 HTTP 接口——下单要用的 `purchase` 块本来就是抓取服务的对外契约。
|
||||
|
||||
抓取服务部署在服务器,交易服务部署在本地(便于管理账号、排查支付问题),本地在 NAT 后
|
||||
没有公网入口,因此下单请求不是推进来的,而是由本地长轮询主动领取。
|
||||
任务网关与本地 worker 的规格见 [docs/order-gateway.md](docs/order-gateway.md)(待实现)。
|
||||
任务网关与本地 worker 的规格见 [docs/order-gateway.md](docs/order-gateway.md)。
|
||||
|
||||
```
|
||||
app/
|
||||
shared/ 配置、错误码、日志、响应信封与鉴权(两侧共用,不认识两侧)
|
||||
shared/ 配置、错误码、日志、响应信封与鉴权、任务/订单状态枚举(三方共用,不认识任何一侧)
|
||||
scraping/ 站点常量 / 会话 / 解析器 / 抓取路由(本 README 的绝大部分)
|
||||
trading/ 登录态、(在建)加购、下单、付款与订单监控
|
||||
trading/ 登录态、加购下单付款(在建)与本地下单 worker(领任务、留证据)
|
||||
gateway/ 下单任务队列 + 状态镜像 + 长轮询领取接口
|
||||
```
|
||||
|
||||
## 抓取原理
|
||||
@@ -123,8 +129,23 @@ PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此
|
||||
| `POST /api/auth/status` | 查询登录态,默认真实探测一次 |
|
||||
| `POST /api/auth/reload` | 人工重新登录后免重启换上新 cookie |
|
||||
|
||||
两个服务共用同一个 Bearer Token,错误码表也是同一份。
|
||||
启动后分别在 `http://127.0.0.1:31107/docs` 与 `:31108/docs` 查看 OpenAPI 文档。
|
||||
下单任务网关(:31109):
|
||||
|
||||
| 接口 | 说明 |
|
||||
| --- | --- |
|
||||
| `GET /health` | 健康检查,含 worker 心跳、长时间无人领任务告警 |
|
||||
| `POST /api/orders` | 上游提交下单意图(幂等) |
|
||||
| `GET /api/orders/lease` | 本地 worker 长轮询领取(全局并发度 1) |
|
||||
| `POST /api/orders/{id}/renew` | 续租(worker 在长任务里每 60s 调一次) |
|
||||
| `POST /api/orders/{id}/report` | 本地回报订单状态(同 state 重复上报幂等) |
|
||||
| `POST /api/orders/{id}/reclaim` | 把 stale 任务重新租给 worker(**绝不自动重投**) |
|
||||
| `GET /api/orders/{id}` | 任务详情 + 完整状态历史 |
|
||||
| `GET /api/orders` | 任务列表(运维与上游对账用) |
|
||||
|
||||
网关的契约与状态机详见 [docs/order-gateway.md](docs/order-gateway.md)。
|
||||
|
||||
三个服务共用同一个 Bearer Token,错误码表也是同一份。
|
||||
启动后分别在 `http://127.0.0.1:31107/docs`、`:31108/docs`、`:31109/docs` 查看 OpenAPI 文档。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -138,7 +159,7 @@ uv sync --extra dev --extra browser
|
||||
|
||||
## 启动
|
||||
|
||||
启动前建议先按 `.env.example` 配置 `.env`,两个服务共用这一份。
|
||||
启动前建议先按 `.env.example` 配置 `.env`,三个服务共用这一份。
|
||||
|
||||
```bash
|
||||
# 抓取服务,默认 0.0.0.0:31107;可多开实例
|
||||
@@ -146,9 +167,12 @@ uv sync --extra dev --extra browser
|
||||
|
||||
# 交易服务,默认 0.0.0.0:31108;只能起一个实例
|
||||
.venv/Scripts/python.exe -m app.trading.main
|
||||
|
||||
# 下单任务网关,默认 0.0.0.0:31109;只能起一个实例(SQLite + 全局并发度 1)
|
||||
.venv/Scripts/python.exe -m app.gateway.main
|
||||
```
|
||||
|
||||
只需要抓取时不必起交易服务。交易服务启动前要先人工登录一次:
|
||||
只需要抓取时不必起交易/网关服务。交易服务启动前要先人工登录一次:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python.exe scripts/login.py --site all
|
||||
@@ -557,20 +581,28 @@ cookie 落在 `.auth/`(已 gitignore,内含可直接冒充账号的凭据,
|
||||
| 5002 | 加购失败(交易服务) | 400 |
|
||||
| 5003 | 下单失败(交易服务) | 400 |
|
||||
| 5004 | 下单安全闸门未通过:未显式确认或金额超上限(交易服务) | 400 |
|
||||
| 6001 | 任务不存在(网关) | 404 |
|
||||
| 6002 | 租约无效:不是持有者、已过期或任务已终结(网关) | 409 |
|
||||
| 6003 | 任务状态不允许该操作(如对已终结任务 reclaim)(网关) | 409 |
|
||||
| 6004 | 已有任务在执行中,本次不发放(正常返回空,仅诊断用) | 200 |
|
||||
|
||||
错误码在两站、两个服务之间通用。ラクマ 链路不会出现 `3002`(无反爬拦截行为)
|
||||
错误码在两站、三个服务之间通用。ラクマ 链路不会出现 `3002`(无反爬拦截行为)
|
||||
与 `4002`(无子站跳转);`5xxx` 只会来自交易服务——抓取服务全程匿名,不会有登录态问题。
|
||||
`5001` 与 `5004` 都标记为不可重试:前者要人工重新登录,后者要调用方改入参。
|
||||
`6xxx` 只会来自网关,全部标记为不可重试——任务编排侧重试无意义,部分场景
|
||||
(如租约过期)重试可能变成重复下单。
|
||||
|
||||
## 常用环境变量
|
||||
|
||||
完整列表见 [.env.example](.env.example)。配置项前缀统一为 `RAKUTEN_`,两个服务共用同一份
|
||||
完整列表见 [.env.example](.env.example)。配置项前缀统一为 `RAKUTEN_`,三个服务共用同一份
|
||||
配置文件、各读各的那部分;两站共用同一套抓取参数(并发数、超时、重试次数),
|
||||
`SESSION_TTL` 与浏览器兜底只对乐天链路生效。
|
||||
|
||||
- 抓取服务:`RAKUTEN_APP_HOST`、`RAKUTEN_APP_PORT`(默认 `31107`)、`RAKUTEN_APP_ENV`
|
||||
- 交易服务:`RAKUTEN_TRADING_HOST`、`RAKUTEN_TRADING_PORT`(默认 `31108`)、`RAKUTEN_AUTH_STATE_DIR`(默认 `.auth`)、`RAKUTEN_ORDER_MAX_TOTAL_YEN`(默认 `30000`)
|
||||
- 鉴权:`RAKUTEN_BEARER_TOKEN`(两个服务共用)
|
||||
- 下单任务网关:`RAKUTEN_GATEWAY_HOST`、`RAKUTEN_GATEWAY_PORT`(默认 `31109`)、`RAKUTEN_GATEWAY_DB_PATH`(默认 `data/gateway.db`)、`RAKUTEN_LEASE_TTL_SECONDS`(默认 `300`)、`RAKUTEN_WORKER_OFFLINE_ALERT_SECONDS`(默认 `300`)
|
||||
- 本地下单 worker(在交易服务内,按 `RAKUTEN_ORDER_GATEWAY_URL` 是否配置决定是否启动):`RAKUTEN_ORDER_GATEWAY_URL`、`RAKUTEN_WORKER_ID`、`RAKUTEN_TRADING_DB_PATH`(默认 `data/trading.db`)、`RAKUTEN_EVIDENCE_DIR`(默认 `data/evidence`)、`RAKUTEN_SCRAPER_BASE_URL`
|
||||
- 鉴权:`RAKUTEN_BEARER_TOKEN`(三个服务共用)
|
||||
- 抓取:`RAKUTEN_MAX_SITE_CONCURRENCY`(默认 `8`,两站各自独立计数)、`RAKUTEN_HTTP_MAX_ATTEMPTS`(默认 `3`)、`RAKUTEN_SESSION_TTL_SECONDS`(默认 `1800`,仅乐天)
|
||||
- 浏览器兜底(仅乐天):`RAKUTEN_BROWSER_FALLBACK_ENABLED`、`RAKUTEN_BROWSER_HEADLESS`、`RAKUTEN_BROWSER_CHANNEL`
|
||||
- 代理(需日本 IP 时):`RAKUTEN_PROXY_SERVER`、`RAKUTEN_PROXY_USERNAME`、`RAKUTEN_PROXY_PASSWORD`
|
||||
|
||||
Reference in New Issue
Block a user