feat(payment): 支持动态获取支付链接货币与计费国家的映射

This commit is contained in:
cnlimiter
2026-03-28 00:05:05 +08:00
parent 8d979894bf
commit 4ed740e1be
6 changed files with 40 additions and 12 deletions

View File

@@ -66,7 +66,7 @@
- Team Manager 服务列表管理(多服务,连接测试)
- Outlook OAuth 参数
- 注册参数(超时、重试、密码长度等)
- 验证码等待配置
- 验证码等待配置(超时时间、轮询间隔、收件箱未找到时最多重发次数)
- 数据库管理(备份、清理)
- 支持远程 PostgreSQL
@@ -373,6 +373,8 @@ docker-compose build --no-cache
- 代理优先级:动态代理 > 代理列表(随机/默认) > 直连
- CPA / Sub2API / Team Manager 上传始终直连,不走代理;其中 CPA 可选把账号记录的代理写入 auth file 的 `proxy_url`
- 注册时自动随机生成用户名和生日(年龄范围 18-45 岁)
- 验证码重发:收件箱超时未获取到验证码时,自动重新发送验证码并再次轮询,最多重发次数可在「验证码配置」中设置(默认 2 次,设为 0 禁用)
- 支付链接货币与计费国家动态对应,从 ChatGPT API 实时获取国家/货币列表(缓存 7 天),不再受限于内置静态映射表
- 支付链接生成使用账号 access_token 鉴权,走全局代理配置
- 无痕打开支付页默认调用系统 Chrome/Edge 的隐私模式
- 订阅状态自动检测调用 `chatgpt.com/backend-api/me`,走全局代理

View File

@@ -356,6 +356,12 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
category=SettingCategory.EMAIL,
description="验证码轮询间隔(秒)"
),
"email_code_resend_max_retries": SettingDefinition(
db_key="email_code.resend_max_retries",
default_value=2,
category=SettingCategory.EMAIL,
description="收件箱未找到验证码时,最多重新发送验证码的次数"
),
# Outlook 配置
"outlook_provider_priority": SettingDefinition(
@@ -407,6 +413,7 @@ SETTING_TYPES: Dict[str, Type] = {
"cpa_enabled": bool,
"email_code_timeout": int,
"email_code_poll_interval": int,
"email_code_resend_max_retries": int,
"outlook_provider_priority": list,
"outlook_health_failure_threshold": int,
"outlook_health_disable_duration": int,
@@ -707,6 +714,7 @@ class Settings(BaseModel):
# 验证码配置
email_code_timeout: int = 120
email_code_poll_interval: int = 3
email_code_resend_max_retries: int = 2
# Outlook 配置
outlook_provider_priority: List[str] = ["imap_old", "imap_new", "graph_api"]

View File

@@ -96,12 +96,13 @@ def generate_plus_link(
account: Account,
proxy: Optional[str] = None,
country: str = "SG",
currency: Optional[str] = None,
) -> str:
"""生成 Plus 支付链接(后端携带账号 cookie 发请求)"""
if not account.access_token:
raise ValueError("账号缺少 access_token")
currency = _COUNTRY_CURRENCY_MAP.get(country, "USD")
currency = currency or _COUNTRY_CURRENCY_MAP.get(country, "USD")
headers = {
"Authorization": f"Bearer {account.access_token}",
"Content-Type": "application/json",
@@ -145,12 +146,13 @@ def generate_team_link(
seat_quantity: int = 5,
proxy: Optional[str] = None,
country: str = "SG",
currency: Optional[str] = None,
) -> str:
"""生成 Team 支付链接(后端携带账号 cookie 发请求)"""
if not account.access_token:
raise ValueError("账号缺少 access_token")
currency = _COUNTRY_CURRENCY_MAP.get(country, "USD")
currency = currency or _COUNTRY_CURRENCY_MAP.get(country, "USD")
headers = {
"Authorization": f"Bearer {account.access_token}",
"Content-Type": "application/json",

View File

@@ -1537,19 +1537,31 @@ class RegistrationEngine:
result.error_message = "发送验证码失败"
return result
# 10. 获取验证码
# 10. 获取验证码(支持重发重试)
self._log("10. 等待验证码...")
self._emit_status("otp_secondary", "等待验证码邮件", step_index=10)
otp_phase_started_at = time.time()
code, otp_phase = self._phase_otp_secondary(
PhaseContext(otp_sent_at=self._otp_sent_at),
started_at=otp_phase_started_at,
)
_resend_max = get_settings().email_code_resend_max_retries
code, otp_phase = None, None
for _resend_attempt in range(_resend_max + 1):
if _resend_attempt > 0:
self._log(f"10. 收件箱未找到验证码,第 {_resend_attempt} 次重新发送验证码...")
self._emit_status("otp_resend", f"重新发送验证码(第 {_resend_attempt} 次)", step_index=10)
if not self._send_verification_code():
self._log("重新发送验证码失败,跳过本次重试", "warning")
continue
code, otp_phase = self._phase_otp_secondary(
PhaseContext(otp_sent_at=self._otp_sent_at),
started_at=otp_phase_started_at,
)
if code:
break
otp_phase_started_at = time.time()
if not code:
result.error_message = (
otp_phase.error_message if otp_phase.error_message else "获取验证码失败"
otp_phase.error_message if otp_phase and otp_phase.error_message else "获取验证码失败"
)
result.error_code = otp_phase.error_code
result.error_code = otp_phase.error_code if otp_phase else ""
return result
# 11. 验证验证码

View File

@@ -35,7 +35,8 @@ class GenerateLinkRequest(BaseModel):
seat_quantity: int = 5
proxy: Optional[str] = None
auto_open: bool = False # 生成后是否自动无痕打开
country: str = "SG" # 计费国家,决定货币 # 生成后是否自动无痕打开
country: str = "SG" # 计费国家,决定货币
currency: Optional[str] = None # 前端动态获取的货币代码,优先于静态映射表
class OpenIncognitoRequest(BaseModel):
@@ -70,7 +71,7 @@ def generate_payment_link(request: GenerateLinkRequest):
try:
if request.plan_type == "plus":
link = generate_plus_link(account, proxy, country=request.country)
link = generate_plus_link(account, proxy, country=request.country, currency=request.currency)
elif request.plan_type == "team":
link = generate_team_link(
account,
@@ -79,6 +80,7 @@ def generate_payment_link(request: GenerateLinkRequest):
seat_quantity=request.seat_quantity,
proxy=proxy,
country=request.country,
currency=request.currency,
)
else:
raise HTTPException(status_code=400, detail="plan_type 必须为 plus 或 team")

View File

@@ -95,10 +95,12 @@ async function generateLink() {
const country = document.getElementById('country-select').value || 'SG';
const currency = countryCurrencyMap[country] || '';
const body = {
account_id: parseInt(accountId),
plan_type: selectedPlan,
country: country,
currency: currency,
};
if (selectedPlan === 'team') {