Files
rakuten-api/README.md
T
q792602257andClaude Opus 4.6 07107094a7 实现下单任务网关与本地 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>
2026-07-27 16:25:20 +08:00

29 KiB

Rakuten Scraper Service

面向乐天集团两个购物站点的 HTTP API 服务:

站点 域名 形态
乐天市场(楽天市場) rakuten.co.jp B2C 商城:店铺 × 商品 × SKU
ラクマ(Rakuma) fril.jp C2C 二手集市:个人卖家 × 单件商品

两站均支持搜索商品详情商家信息商家名下商品

三个部署单元

同一个仓库出三个服务,分进程运行:

抓取服务 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 份,让同一个账号被多个进程并发操作。

网关单独成第三个部署单元,是因为它有状态(任务队列),不能塞进可多开的抓取服务, 也不能塞进在本地、零入站端口的交易服务——本地通过出站长轮询从这里领任务。 完整规格见 docs/order-gateway.md

依赖方向固定为 scraping → sharedtrading → sharedgateway → shared,三方互不 import (tests/test_architecture.py 会守着)。交易侧需要商品信息时走抓取服务的 HTTP 接口, 需要任务调度时走网关的 HTTP 接口——下单要用的 purchase 块本来就是抓取服务的对外契约。

抓取服务部署在服务器,交易服务部署在本地(便于管理账号、排查支付问题),本地在 NAT 后 没有公网入口,因此下单请求不是推进来的,而是由本地长轮询主动领取。 任务网关与本地 worker 的规格见 docs/order-gateway.md

app/
  shared/     配置、错误码、日志、响应信封与鉴权、任务/订单状态枚举(三方共用,不认识任何一侧)
  scraping/   站点常量 / 会话 / 解析器 / 抓取路由(本 README 的绝大部分)
  trading/    登录态、加购下单付款(在建)与本地下单 worker(领任务、留证据)
  gateway/    下单任务队列 + 状态镜像 + 长轮询领取接口

抓取原理

两站的页面形态与防护完全不同,因此各走一条独立链路。

乐天市场:内联 JSON + Akamai

搜索页与(手机版)商品详情页都把整页数据以 JSON 形式内联在 window.__INITIAL_STATE__ 里, 因此不需要解析 DOM,直接取这段 JSON 即可拿到完整结构化数据。

站点前置 Akamai Bot Manager不需要真浏览器交互

请求方式 实测耗时
请求头不完整、无 cookie ~11s(与响应体大小无关,是限速而非封禁)
完整浏览器请求头 + 复用 Akamai cookie ~0.6–0.9s

所以主链路是纯 httpx,Playwright 仅作为被拦截时的兜底(取 cookie 回灌后重试)。

服务按「指纹画像」维护两条独立通道,各自持有独立 cookie 罐:

  • PC 通道 → 搜索页 search.rakuten.co.jp、店铺页 www.rakuten.co.jp
  • 手机通道 → 商品详情页 item.rakuten.co.jp (详情页只有手机 UA 才返回带 __INITIAL_STATE__ 的统一模板;PC UA 返回的是各店铺自定义的 EUC-JP 老页面)

ラクマ:服务端渲染 HTML,无限速

ラクマ 是 Rails 服务端渲染的传统 HTML,没有 __INITIAL_STATE__ 这类内联状态, 只能解析 DOM。好在页面上挂了成套的埋点属性,比可见文案稳定得多,也带有 DOM 上 没有的字段(商品数值 ID、卖家 ID、分类/品牌 ID),解析优先取这些:

  • data-gtm-click / onclick 里的 dataLayer JSON — 商品卡片的结构化字段
  • 页面级 data-rat-cp-* 属性 — 成色、运费负担、发货地(已售出商品的规格表会消失,靠它兜底)
  • <script type="application/ld+json"> — 商品的 Product、店铺的 Store 微数据(评价数只有这里给)

实测没有 Akamai 那类限速:冷请求(无 cookie、无预热)即 0.5–1.1s,与预热后持平; PC UA 在搜索页、详情页、店铺页上都能拿到完整模板。因此 ラクマ 只有一条通道、 不做 cookie 预热,也不接浏览器兜底。

⚠️ ラクマ 对无法识别的筛选取值不会报错,而是静默返回首页(如 statuses=99 返回 75KB 的首页 HTML)。 本服务的枚举值全部取自站点前端 bundle 里 SearchPanel._url() 的构造逻辑,并逐个实测过; 解析器也会校验页面确实是结果页,识别不出时报 4001 而不是当成「0 条结果」。

功能概览

  • FastAPI 提供 HTTP API
  • Bearer Token 接口鉴权(Authorization: Bearer <token>
  • Akamai cookie 自动预热与复用,过期自动重新预热(仅乐天链路)
  • 失败逐级升级:重新预热 → Playwright 兜底取 cookie → 结构化报错
  • 搜索结果自动剔除混入的 CPC 广告位(乐天)
  • 识别站点深翻页「静默回绕到第 1 页」的行为,避免上游重复入库(乐天)
  • 商品详情按落地域名分派解析,覆盖楽天ブックス / Rakuten Fashion / ビックカメラ 等官方旗舰子站

接口

抓取服务(:31107):

接口 站点 说明
GET /health 健康检查,含各抓取通道状态
POST /api/search 乐天 商品搜索
POST /api/genres 乐天 分类树
POST /api/item_detail 乐天 商品详情
POST /api/shop_detail 乐天 商家详情
POST /api/shop_items 乐天 商家名下商品
POST /api/rakuma/search ラクマ 商品搜索
POST /api/rakuma/categories ラクマ 分类树
POST /api/rakuma/item_detail ラクマ 商品详情
POST /api/rakuma/shop_detail ラクマ 卖家详情
POST /api/rakuma/shop_items ラクマ 卖家名下商品

接口按站点分前缀而非合并加 site 参数,因为两站的筛选体系差异很大 (乐天有 genre_id / 成色 / SuperDEAL,ラクマ 有 category_id / brand_id / 匿名配送 / 鉴定服务), 合并会让大半字段对另一站无效。

交易服务(:31108):

接口 说明
GET /health 健康检查,含两站登录态(只读缓存,不打站点)
POST /api/auth/status 查询登录态,默认真实探测一次
POST /api/auth/reload 人工重新登录后免重启换上新 cookie

下单任务网关(: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

三个服务共用同一个 Bearer Token,错误码表也是同一份。 启动后分别在 http://127.0.0.1:31107/docs:31108/docs:31109/docs 查看 OpenAPI 文档。

安装

uv sync --extra dev

# 可选:启用浏览器兜底(不装也能正常跑,只是失去兜底能力)
uv sync --extra dev --extra browser
.venv/Scripts/python.exe -m playwright install chromium

启动

启动前建议先按 .env.example 配置 .env,三个服务共用这一份。

# 抓取服务,默认 0.0.0.0:31107;可多开实例
.venv/Scripts/python.exe -m app.scraping.main

# 交易服务,默认 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

只需要抓取时不必起交易/网关服务。交易服务启动前要先人工登录一次:

.venv/Scripts/python.exe scripts/login.py --site all

浏览器窗口打开后手动完成登录(账号密码只在浏览器与站点之间传递,脚本不读取), cookie 落在 .auth/(已 gitignore,内含可直接冒充账号的凭据,不要提交或外传)。 后续重新登录后调 POST /api/auth/reload 换上新 cookie,不必重启服务。

测试

.venv/Scripts/python.exe -m pytest -q

单测全部离线运行:解析器用的是 tests/fixtures/ 下从真实页面抓取并裁剪后的状态样本。

请求示例

以下 /api/* 为乐天市场接口,/api/rakuma/* 为 ラクマ 接口。

搜索(乐天)

POST /api/search

三种用法,优先级从高到低:

1. 透传原始搜索 URLpage > 1 时会覆盖 URL 里的页码)

{ "search_url": "https://search.rakuten.co.jp/search/mall/カメラ/?s=3&f=101", "page": 3 }

2. 关键词 + 筛选

{
  "keyword": "nintendo switch",
  "page": 2,
  "sort": "price_asc",
  "min_price": 3000,
  "max_price": 30000,
  "free_shipping": true,
  "condition": "used"
}

3. 仅按分类

{ "genre_id": "565950" }

keywordgenre_idsearch_url 三者至少提供一个。

搜索参数

参数 类型 说明
keyword string 关键词
genre_id string 乐天分类 ID,可与关键词叠加
page int 页码,1–150
sort enum standard(默认)/ price_asc / price_desc / newest / review_count / review_score / price_with_shipping_asc / price_with_shipping_desc
min_price / max_price int 价格区间(日元)
shop_id int 限定店铺,取搜索结果里的 shop.shop_id
condition enum new / used / rental
free_shipping bool 仅免运费
include_sold_out bool 包含售罄商品
has_review bool 仅有评论
next_day_delivery bool 仅次日达
super_deal bool 仅 SuperDEAL
exclude_keyword string 排除词
title_only bool 仅在商品标题中匹配
or_query bool 关键词之间用 OR 而非 AND
min_review_score int 最低评分 1–5
tags string[] 站点标签 ID
exclude_ads bool 剔除 CPC 广告位,默认 true

分页注意事项

站点声明的命中总数(total_count)远大于实际可翻到的条数(reachable_count,随查询条件变化, 实测从 405 到 6750 不等)。超出可达窗口时站点不会返回空列表,而是静默回绕到第 1 页—— 本服务会识别这种情况,把 out_of_range 置为 true 并清空 items,避免上游把重复数据当新数据入库。

翻页时以 has_more 为准即可。

分类(乐天)

POST /api/genres

用于取得 /api/search 需要的 genre_id,逐层下钻即可定位到叶子分类。

顶层分类(39 个):

{}

下钻某个分类

{ "genre_id": "101205" }

返回该分类自身(name / full_name / description / is_leaf)、祖先路径 ancestors (从顶层到父级,不含自身,可直接拼面包屑)以及直接子分类 children

{}              → 39 个顶层分类
101205 テレビゲーム  → ancestors=[]                              children=27
565950 Nintendo Switch → ancestors=[テレビゲーム]                  children=3
566404 ソフト        → ancestors=[テレビゲーム, Nintendo Switch]   children=0, is_leaf=true

两点注意:

  • 子分类的 item_count 是该分类下的商品数;顶层列表不返回该值,因为站点在那一层 给的是「当前查询在该分类下的命中数」,并非分类自身的商品总量。
  • full_namedescription 只有顶层分类页才提供,下级分类为空串。

商品详情(乐天)

POST /api/item_detail

shop_code + item_code(即商品 URL 的两段路径),或直接传 item_url

{ "shop_code": "edion", "item_code": "4902370549263" }
{ "item_url": "https://item.rakuten.co.jp/edion/4902370549263/" }

include_sku_variants 默认 true;SKU 组合可能多达数百条(实测有 180 条的商品), 不需要明细时置为 false,此时仍会保留 sku.axissku.variant_count

搜索结果中的 shop.shop_code + item_code 可直接用作本接口入参。

部分官方店的商品页会跳转到独立子站,响应里的 source / source_url 会标明数据来源, 字段覆盖差异见下一节。

商家(乐天)

商家详情 POST /api/shop_detail

shop_code(店铺首页 URL 的路径段),或直接传 shop_url

{ "shop_code": "edion" }

返回店铺名、简介、评分与评价数、招牌图与 logo、是否 39ショップ、休息日等。

评价数过少时站点不展示评分,此时 review_displayedfalsereview_score 不可信。

商家名下商品 POST /api/shop_items

{ "shop_id": 272415, "page": 2, "keyword": "テレビ", "sort": "price_asc" }

站点没有可分页抓取的「店铺内商品」页——店铺商品实际就是搜索结果按 sid= 限定店铺后的产物, 因此本接口内部转成一次搜索,返回结构与 /api/search 完全一致,也支持在店铺内继续按 关键词、分类、价格、成色筛选。

shop_idshop_code 至少给一个;只给 shop_code 时服务端会先取一次店铺详情换出 shop_id(多一次请求),能直接给 shop_id 时优先给。shop_id 可从搜索结果的 shop.shop_id/api/shop_detail 取得。

官方旗舰子站

部分乐天官方店的商品页会 302 跳出 item.rakuten.co.jp,落到各自独立的站点上。 这些站点技术栈各不相同,服务按落地域名分派到对应解析器,统一收敛成同一份 ItemDetailData;响应里的 source 标明数据来自哪个站点,source_url 是实际解析的地址。

source 店铺代码 落地域名 页面数据格式
ichiba 其余全部 item.rakuten.co.jp __INITIAL_STATE__
books book(楽天ブックス) books.rakuten.co.jp schema.org 微数据 + DOM
brandavenue stylife(Rakuten Fashion) brandavenue.rakuten.co.jp __INITIAL_STATE__(结构与市场页不同)
biccamera biccamera biccamera.rakuten.co.jp __NUXT__

落到未登记的站点时返回错误码 4002OFF_ICHIBA_REDIRECT)并附上跳转目标地址, 便于上游区分「站点不支持」与「被反爬拦截」,而不是白白重试。

各来源字段覆盖差异

子站页面能提供的信息不同,以下字段并非各处都有:

字段 ichiba books brandavenue biccamera
名称 / 价格 / 图片 / 简介
genre_id / breadcrumbs
is_sold_out
review 异步加载 异步加载
sku.variants(规格组合) — 图书无规格 颜色 × 尺码 — 单一规格
sku.attributes(规格表) 出版社/ISBN 等 挂在各 variant 上
shipping 运费明细 仅库存措辞 仅「是否含运费」
shop.shop_id
breadcrumbs[].url 站内分类编码,拼不出链接
purchase(加购标识) 无件数/规格 shop_bid

抽样实测(6 个关键词 × 各 20 条,共 120 条)整体 99% 的商品可正常解析详情 (唯一失败是一条已下架商品,返回 4004):

类目关键词 可解析
switch 20/20
カメラ 20/20
Tシャツ 20/20
20/20
化粧品 20/20
iphone 19/20

来源分布:ichiba 91、books 19、brandavenue 8、biccamera 1。

ラクマ 接口

ラクマ 是 C2C 二手集市,与乐天市场的商业形态不同,字段语义也不同,因此模型不与市场侧合并

  • 每件商品都是独一无二的一件:没有 SKU 组合、没有库存数量、没有起订量
  • 「商家」就是个人卖家:没有店铺代码,用一串 hash 标识;没有运费门槛、店铺日历这些商城概念
  • 成色由卖家自己申告,分 6 档(市场侧只有新品/中古/租赁 3 档)

搜索(ラクマ)

POST /api/rakuma/search

{
  "keyword": "switch",
  "page": 2,
  "sort": "price_asc",
  "conditions": ["new", "almost_new"],
  "transaction": "on_sale",
  "min_price": 3000,
  "max_price": 20000,
  "free_shipping": true
}

也可以直接透传 search_urlpage > 1 时覆盖 URL 里的页码)。 keywordcategory_idbrand_idsearch_url 四者至少提供一个。

参数 类型 说明
keyword string 关键词
category_id string ラクマ 分类 ID(与乐天的 genre_id 是两套体系)
brand_id string ラクマ 品牌 ID
page int 页码,1–100(站点 page > 100 直接 404)
sort enum standard(默认)/ newest / price_asc / price_desc / like_count
min_price / max_price int 价格区间(日元)
conditions enum[] 成色,可多选:new / almost_new / no_damage / slight_damage / damaged / poor
transaction enum on_sale 仅在售 / sold_out 仅售罄;不传为不限
free_shipping bool 仅「送料込み」(卖家承担运费)
anonymous_shipping bool 仅匿名配送
except_for_no_brand bool 排除无品牌商品;与 brand_id 互斥
authenticity_types enum[] 鉴定服务:before_delivery / after_delivery
exclude_keyword string 排除词,必须与 keyword 同时使用(站点在无关键词时不认该参数)

每页固定 40 条。total_count 取自页面埋点里的精确值——页面上可见的「約1,190,000件」 是四舍五入后的展示值,不要拿它做分页计算;翻页以 has_more 为准。

分类(ラクマ)

POST /api/rakuma/categories

{ "category_id": "10007", "include_descendants": true }

不传 category_id 返回 14 个顶层分类;传入后返回该分类的名称、full_name 路径名、 ancestors 祖先链与 children 直接子分类。分类共三层(14 顶层 / 169 二级 / 1503 三级), 拿到的 category_id 可直接用于 /api/rakuma/search

参数 类型 说明
category_id string 目标分类;不传取顶层列表。无效 ID 返回 404
include_descendants bool children 里带完整子树而非只有直接子级,默认 false

站点的分类一览页一次就把整棵树写进页面(与 URL 上的 category_id 无关), 因此不论查哪一层、要不要子树,服务端都只打一次请求——不像乐天 /api/genres 需要逐层下钻。 响应里的 total_count 是站点分类树的节点总数(当前 1686),可用于确认取到的是全量树。

本接口不返回商品数。 站点的分类数据里没有这一项,只有 /category/{id} 列表页的埋点上有, 要逐个分类多打一次请求,成本与收益不匹配。需要某分类的商品数时,用该 category_id 调一次 /api/rakuma/searchtotal_count

商品详情(ラクマ)

POST /api/rakuma/item_detail

{ "item_id": "4aca1d6db3e422f3a251a8a8b61e1eff" }

item_id(商品 URL 的最后一段 hash)或 item_url。搜索结果里的 item_id 可直接用作入参。

返回名称、价格、描述、图片、成色、配送信息(负担方/方式/发货日/发货地)、 点赞与评论数,以及 seller 出品者摘要——其中的 seller.shop_id 可直接用于 /api/rakuma/shop_detail

已售出商品的页面会换一套布局,规格表整体消失。 此时服务改从页面级埋点属性取值, 成色、运费负担、发货地仍能拿到,但配送方式与尺码会为空;价格与名称来自 ld+json,不受影响。

卖家(ラクマ)

卖家详情 POST /api/rakuma/shop_detail

{ "shop_id": "422750cb7921557bc8dba2416915d968", "include_reviews": true }

返回店铺名、昵称、头像与封面、简介、评分与评价数、本人确认状态(is_verified)、 以及该卖家的商品总数 item_count

include_reviews 默认 false;置为 true 时会并发多抓一次站点的评价子页, 额外返回最新 100 条评价 reviews,以及好评/普通/差评分档计数:

  • rating_breakdown — 全部评价(出品 + 购入)
  • seller_rating_breakdown — 仅出品方评价

卖家名下商品 POST /api/rakuma/shop_items

{ "shop_id": "422750cb7921557bc8dba2416915d968", "page": 2 }

按页返回该卖家的全部商品(含已售出,用每条的 is_sold_out 区分)。 店铺页每页 36 条(与搜索页的 40 条不同)。

站点在店铺页不提供排序与筛选参数,因此本接口只有页码;需要筛选请改用 /api/rakuma/search

两站字段对照

概念 乐天市场 ラクマ
商品标识 shop_code + item_code 两段 item_id 单个 hash
商家标识 shop_code(如 edion)/ shop_id(数值) shop_id(hash)
分类 genre_id + /api/genres 分类树(逐层下钻,带商品数) category_id + /api/rakuma/categories 分类树(一次取全,无商品数)
品牌 brand_id
成色 3 档(new/used/rental 6 档卖家申告
规格 sku.axis + sku.variants 无(单件商品,仅一个 size 字段)
每页条数 45(搜索) 40(搜索)/ 36(店铺页)
翻页上限 reachable_count,随查询变化(405~6750) 固定 100 页
加购标识 purchase 无(未实现)

加购与下单(仅乐天市场)

详情响应里的 purchase 块给出构造「加入购物车」请求所需的标识。 抓取服务只提供数据,不执行加购——加购需要账号登录态,属于交易服务 (app/trading/,尚在建设中)。这个 purchase 块正是两个服务之间的契约: 交易服务调抓取服务的 /api/item_detail 取它,而不是直接 import 解析器。

"purchase": {
  "cart_url": "https://sp.basket.step.rakuten.co.jp/rms/mall/bss/cartadd/set",
  "cart_method": "POST",
  "form_fields": {          // 原样提交的固定字段
    "shop_bid": "231431",
    "item_id": "10008065",
    "inventory_flag": "2",  // 1=单一库存,2=多规格
    "__event": "ES01_003_001"
  },
  "quantity_field": "units",      // 购买件数填这里
  "variant_field": "variant_id",  // 选中的 SKU 填这里
  "options_field": "choice",      // 商品选项填这里
  "has_required_options": true,   // 有必填选项,缺失会被站点拒绝
  "options": [
    { "id": 1, "name": "名入れ", "type": "select", "is_required": true,
      "values": [{ "name": "希望する【次の項目で入力】" }] },
    { "id": 2, "name": "【お名前】", "type": "text", "is_required": false, "values": [] }
  ]
}

拼装规则:

  • form_fields 原样带上,再按 quantity_field 填件数。
  • inventory_flag=1variant_id 已在 form_fields 里预填好;inventory_flag=2必须由调用方从 sku.variants[].variant_id 选一个,填到 variant_field
  • 字段名为空字符串表示该站不支持该项(如 books 不能指定件数与规格)。
  • optionstype=selectvalues 中选,type=text 由买家填写;is_required=true 的项不能省。

几点差异值得留意:

  • cart_url 逐商品不同,不要写死。不同店铺落在不同 basket 集群(实测有 sp.basket…ts.sp.basket…),brandavenue 的端点还要再经一层编号映射。
  • booksitem_id 与 URL 上的商品编号不是同一个值(如 URL 17065211 对应 item_id 20600328)。下单请用 purchase.form_fields.item_iditem_code 只是市场侧编号。
  • biccamera 没有 shop_bid,走自己的 JSON 接口;它的选项取值结构未取到样本验证,因此只用 has_required_options 如实回报「有无选项」而不给出选项定义,这类商品需另行处理。

错误码

code 含义 HTTP
0 成功 200
1001 鉴权失败 401
1002 请求参数校验失败 422
1003 请求参数不合法(如 URL 不属于目标站点) 400
2002 抓取并发槽位等待超时 400
3001 上游网络异常 / 5xx 400
3002 被反爬阻断 400
4001 页面解析失败(ラクマ 传了站点不认的筛选取值时也归此类) 400
4002 商品页跳转至未登记的乐天子站 400
4004 商品/店铺不存在或已下架 404
5001 账号未登录或登录态失效(交易服务) 401
5002 加购失败(交易服务) 400
5003 下单失败(交易服务) 400
5004 下单安全闸门未通过:未显式确认或金额超上限(交易服务) 400
6001 任务不存在(网关) 404
6002 租约无效:不是持有者、已过期或任务已终结(网关) 409
6003 任务状态不允许该操作(如对已终结任务 reclaim)(网关) 409
6004 已有任务在执行中,本次不发放(正常返回空,仅诊断用) 200

错误码在两站、三个服务之间通用。ラクマ 链路不会出现 3002(无反爬拦截行为) 与 4002(无子站跳转);5xxx 只会来自交易服务——抓取服务全程匿名,不会有登录态问题。 50015004 都标记为不可重试:前者要人工重新登录,后者要调用方改入参。 6xxx 只会来自网关,全部标记为不可重试——任务编排侧重试无意义,部分场景 (如租约过期)重试可能变成重复下单。

常用环境变量

完整列表见 .env.example。配置项前缀统一为 RAKUTEN_,三个服务共用同一份 配置文件、各读各的那部分;两站共用同一套抓取参数(并发数、超时、重试次数), SESSION_TTL 与浏览器兜底只对乐天链路生效。

  • 抓取服务:RAKUTEN_APP_HOSTRAKUTEN_APP_PORT(默认 31107)、RAKUTEN_APP_ENV
  • 交易服务:RAKUTEN_TRADING_HOSTRAKUTEN_TRADING_PORT(默认 31108)、RAKUTEN_AUTH_STATE_DIR(默认 .auth)、RAKUTEN_ORDER_MAX_TOTAL_YEN(默认 30000
  • 下单任务网关:RAKUTEN_GATEWAY_HOSTRAKUTEN_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_URLRAKUTEN_WORKER_IDRAKUTEN_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_ENABLEDRAKUTEN_BROWSER_HEADLESSRAKUTEN_BROWSER_CHANNEL
  • 代理(需日本 IP 时):RAKUTEN_PROXY_SERVERRAKUTEN_PROXY_USERNAMERAKUTEN_PROXY_PASSWORD

从中国大陆直连实测可用、无需代理;RAKUTEN_PROXY_SERVER 留空即可。