From 4ed740e1bef46a394ea91f6bcb169f21a2b46d92 Mon Sep 17 00:00:00 2001 From: cnlimiter Date: Sat, 28 Mar 2026 00:05:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E6=94=AF=E6=8C=81=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=8E=B7=E5=8F=96=E6=94=AF=E4=BB=98=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E8=B4=A7=E5=B8=81=E4=B8=8E=E8=AE=A1=E8=B4=B9=E5=9B=BD=E5=AE=B6?= =?UTF-8?q?=E7=9A=84=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- src/config/settings.py | 8 ++++++++ src/core/openai/payment.py | 6 ++++-- src/core/register.py | 26 +++++++++++++++++++------- src/web/routes/payment.py | 6 ++++-- static/js/payment.js | 2 ++ 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3956a88..d3863d8 100644 --- a/README.md +++ b/README.md @@ -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`,走全局代理 diff --git a/src/config/settings.py b/src/config/settings.py index 38de52b..cff0608 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -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"] diff --git a/src/core/openai/payment.py b/src/core/openai/payment.py index d4c43a4..b1425b1 100644 --- a/src/core/openai/payment.py +++ b/src/core/openai/payment.py @@ -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", diff --git a/src/core/register.py b/src/core/register.py index d3cdf75..55d4663 100644 --- a/src/core/register.py +++ b/src/core/register.py @@ -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. 验证验证码 diff --git a/src/web/routes/payment.py b/src/web/routes/payment.py index 2adbca5..390b492 100644 --- a/src/web/routes/payment.py +++ b/src/web/routes/payment.py @@ -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") diff --git a/static/js/payment.js b/static/js/payment.js index ab0c621..f0be46c 100644 --- a/static/js/payment.js +++ b/static/js/payment.js @@ -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') {