mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-06 20:02:51 +08:00
feat(payment): 支持动态获取支付链接货币与计费国家的映射
This commit is contained in:
@@ -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`,走全局代理
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. 验证验证码
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user