Files
rakuten-api/app/trading/core/auth_site.py
T
q792602257andClaude Opus 4.6 65e3ed31f8 交易服务收敛为仅 rakuten:移除 rakuma 登录态与下单入口
trading 不再管理 ラクマ 的购物车购买、支付与订单监控。删除 auth_site /
auth_session / login_runner / models.AuthSite 中的 rakuma 分支与常量、
account.yaml.example 的 rakuma 段,并清理相关测试。scraping 侧的
ラクマ 抓取 API(/api/rakuma/*)保留不动。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-28 11:50:07 +08:00

104 lines
4.5 KiB
Python

"""登录态相关的站点常量
与抓取用的 `scraping/core/site.py` / `rakuma_site.py` 分开:那两个模块描述的是
「匿名抓取怎么拿到页面」,这里描述的是「怎么判断这套 cookie 还登录着」以及登录
入口在哪。
交易服务只管理乐天市场的购物车购买、支付与订单监控;ラクマ 的抓取仍在抓取服务里
提供,但不进入交易链路(没有加购契约,也永不实现付款/订单监控)。
登录态探针的选取原则:挑一个**未登录时行为明确可辨**的页面,而不是靠首页上有没有
用户名这类会随改版漂移的文案。乐天的信号(实测):手机版购物车页始终返回 200,
未登录时正文含「現在ログインしていません」,登录后该串消失。购物车页同时是加购
结果的校验页,一页两用。
> 注意:`checkout-flow-probe-findings §2` 已确认新版 SP cart SPA 不再渲染这个
> marker,`auth_session._check_rakuten` 仍用它的原因是 httpx 拿到的是 SSR 落地
> HTML(SPA bundle 之前的响应体),在 SSR 阶段 marker 仍在。Playwright 渲染后的
> 页面判据换走 `__INITIAL_STATE__.user.isLoggedIn`,详见 site_interact.py。
每站的这组事实收在 `PROFILES` 里,`scripts/login.py`(起浏览器人工登录)与
`AuthSession`(在 httpx 里探测)共用同一份,避免两处各写一遍判据后悄悄漂移。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Final
from app.shared import headers
# ---- 乐天市场 ----
# 手机版购物车。加购走的是 sp.basket 集群,校验也用手机版,保持同一套指纹。
RAKUTEN_CART_URL: Final = "https://sp.cart.step.rakuten.co.jp/cart"
# 登录入口。人工登录时打开这个地址,站点会在登录成功后跳回 my.rakuten.co.jp。
RAKUTEN_LOGIN_URL: Final = "https://www.rakuten.co.jp/myrakuten/"
# 购物车页上表示「当前会话未登录」的文案。出现即判定登录态失效。
RAKUTEN_LOGGED_OUT_MARKER: Final = "現在ログインしていません"
# ---- 登录态请求指纹 ----
# 这个 UA 必须与 scripts/login.py 起浏览器时用的一致:cookie 是在那个 UA 下拿到的,
# 服务端再拿它发请求时换了 UA,可能触发站点的设备校验让登录态提前失效。
#
# 值与抓取侧当前相同,但**刻意不复用**抓取侧的常量:抓取 UA 是为绕反爬服务的,
# 随时可能为了成功率被调整,那种调整不该波及已落盘的账号 cookie。
RAKUTEN_USER_AGENT: Final = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
)
@dataclass(frozen=True, slots=True)
class SiteAuthProfile:
"""一个站点的登录态配置:登录入口、探针、指纹与落盘文件名"""
name: str
label: str
login_url: str
probe_url: str # 判断登录态是否仍有效的页面
user_agent: str
mobile: bool
state_filename: str # storage_state 落盘文件名(放在 settings.auth_state_dir 下)
def headers(self) -> dict[str, str]:
"""该站登录态请求用的完整导航请求头"""
return headers.navigation_headers(self.user_agent, mobile=self.mobile)
PROFILES: Final[dict[str, SiteAuthProfile]] = {
"rakuten": SiteAuthProfile(
name="rakuten",
label="楽天市場",
login_url=RAKUTEN_LOGIN_URL,
probe_url=RAKUTEN_CART_URL,
user_agent=RAKUTEN_USER_AGENT,
mobile=True,
state_filename="rakuten_state.json",
),
}
SITES: Final = tuple(PROFILES)
def profile(site: str) -> SiteAuthProfile:
"""取某站的登录态配置,未知站点直接报错"""
try:
return PROFILES[site]
except KeyError:
raise ValueError(f"未知站点:{site}") from None
def is_logged_in(site: str, *, final_url: str, body: str) -> bool:
"""按该站判据,从探针页的落地 URL 与正文判断是否仍登录着
`site` 参数目前恒为 `"rakuten"`,保留参数形态是为了与 `scripts/login.py` 共用
签名,未来若要加站点不必同时改两处调用方。
两处共用:`AuthSession` 传 httpx 响应的 URL 与文本,`scripts/login.py` 传
浏览器页面的 URL 与内容。判据只写一遍,两条链路不会各判各的。
"""
if site != "rakuten":
raise ValueError(f"未知站点:{site}")
return RAKUTEN_LOGGED_OUT_MARKER not in body