实现下单任务网关与本地 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:
+72
-3
@@ -3,14 +3,16 @@
|
||||
配置项统一使用 RAKUTEN_ 前缀,例如 RAKUTEN_APP_PORT=31107。
|
||||
支持 .env 文件自动加载。
|
||||
|
||||
抓取服务与交易服务是两个进程,但共用这一个 Settings 类:两边都要日志、代理、
|
||||
超时与同一个 Bearer token,拆成两份配置只会让部署时多维护一套。下面按
|
||||
「通用 / 仅抓取 / 仅交易」分区标注,各进程只读自己那部分。
|
||||
抓取、交易与下单任务网关是三个进程,但共用这一个 Settings 类:三方都要日志、
|
||||
代理、超时与同一个 Bearer token,拆成三份配置只会让部署时多维护一套。下面按
|
||||
「通用 / 仅抓取 / 仅交易 / 仅网关 / 仅交易 worker」分区标注,各进程只读自己那部分。
|
||||
"""
|
||||
import socket
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
@@ -47,6 +49,11 @@ class Settings(BaseSettings):
|
||||
trading_host: str = "0.0.0.0"
|
||||
trading_port: int = 31108
|
||||
|
||||
# 下单任务网关监听地址(第三个部署单元)。网关部署在服务器侧,本地 worker
|
||||
# 通过出站长轮询从这里领任务。详见 docs/order-gateway.md。
|
||||
gateway_host: str = "0.0.0.0"
|
||||
gateway_port: int = 31109
|
||||
|
||||
# ---- 日志配置 ----
|
||||
log_level: str = "INFO"
|
||||
log_to_file: bool | None = None # None 表示根据环境自动决定
|
||||
@@ -102,6 +109,36 @@ class Settings(BaseSettings):
|
||||
# 页面改版导致买到远超预期的订单。设为 0 表示不设上限(不建议)。
|
||||
order_max_total_yen: int = 30000
|
||||
|
||||
# ---- 下单任务网关(仅网关进程 app.gateway.main 使用)----
|
||||
# 网关的 SQLite 文件路径(相对项目根目录)。任务队列与状态镜像都在这里,
|
||||
# 部署时务必放在持久化卷上,丢了等于丢了一批下单任务。
|
||||
gateway_db_path: str = "data/gateway.db"
|
||||
# 任务租约 TTL(秒)。worker 领取后必须在此时间内首次 report 或 renew,
|
||||
# 否则网关把任务标记为 stale。**绝不自动重投**(见 docs/order-gateway.md §5)。
|
||||
lease_ttl_seconds: int = 300
|
||||
# 长轮询单次最长挂起秒数。worker 端的 wait 参数会被夹到这个上限。
|
||||
lease_max_wait_seconds: int = Field(default=60, ge=1, le=300)
|
||||
# worker 心跳超时阈值(秒)。网关 /health 据此判断 worker 是否失联:
|
||||
# 正常 worker 每 30 秒来一次 lease,超过该阈值未来 lease 即视为异常。
|
||||
worker_offline_alert_seconds: int = 300
|
||||
|
||||
# ---- 本地下单 worker(仅交易服务内的 worker 子模块使用)----
|
||||
# 网关 URL。**留空则不启动 worker**,交易服务只跑登录态接口。
|
||||
# 部署形态:本地机(NAT 后无公网入口)通过出站长轮询领任务,详见
|
||||
# docs/order-gateway.md。
|
||||
order_gateway_url: str = ""
|
||||
# worker 标识。同一时间只能有一个 worker 持有 lease,这个值用来区分不同
|
||||
# 本地机;留空时取主机名。
|
||||
worker_id: str | None = None
|
||||
# 本地订单 SQLite 文件路径。订单主表 + 状态事件 + 证据索引都在这里,
|
||||
# 是执行事实的权威记录;网关上只是镜像。
|
||||
trading_db_path: str = "data/trading.db"
|
||||
# 页面证据落盘目录(HTML 快照 + 截图 + meta.json),按 task_id 分子目录。
|
||||
evidence_dir: str = "data/evidence"
|
||||
# 抓取服务基地址。worker 需要商品数据(加购要用 purchase 块)时出站请求这里,
|
||||
# 不直接 import 解析器——抓取与交易是两个进程,详见 README「两个部署单元」。
|
||||
scraper_base_url: str = ""
|
||||
|
||||
# ---- 目标站点 ----
|
||||
home_url: str = DEFAULT_HOME_URL
|
||||
|
||||
@@ -155,6 +192,38 @@ class Settings(BaseSettings):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def gateway_db_path_resolved(self) -> Path:
|
||||
"""网关 SQLite 文件的绝对路径,父目录不存在时创建"""
|
||||
path = Path(self.gateway_db_path)
|
||||
if not path.is_absolute():
|
||||
path = BASE_DIR / path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def trading_db_path_resolved(self) -> Path:
|
||||
"""本地订单 SQLite 文件的绝对路径,父目录不存在时创建"""
|
||||
path = Path(self.trading_db_path)
|
||||
if not path.is_absolute():
|
||||
path = BASE_DIR / path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def evidence_path(self) -> Path:
|
||||
"""证据目录的绝对路径,不存在时创建"""
|
||||
path = Path(self.evidence_dir)
|
||||
if not path.is_absolute():
|
||||
path = BASE_DIR / path
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def worker_id_effective(self) -> str:
|
||||
"""worker 标识:显式配置优先,否则取主机名"""
|
||||
return self.worker_id or socket.gethostname()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
|
||||
Reference in New Issue
Block a user