mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-07-12 16:01:22 +08:00
feat(payment): 支持动态获取支付链接货币与计费国家的映射
This commit is contained in:
@@ -66,7 +66,7 @@
|
|||||||
- Team Manager 服务列表管理(多服务,连接测试)
|
- Team Manager 服务列表管理(多服务,连接测试)
|
||||||
- Outlook OAuth 参数
|
- Outlook OAuth 参数
|
||||||
- 注册参数(超时、重试、密码长度等)
|
- 注册参数(超时、重试、密码长度等)
|
||||||
- 验证码等待配置
|
- 验证码等待配置(超时时间、轮询间隔、收件箱未找到时最多重发次数)
|
||||||
- 数据库管理(备份、清理)
|
- 数据库管理(备份、清理)
|
||||||
- 支持远程 PostgreSQL
|
- 支持远程 PostgreSQL
|
||||||
|
|
||||||
@@ -373,6 +373,8 @@ docker-compose build --no-cache
|
|||||||
- 代理优先级:动态代理 > 代理列表(随机/默认) > 直连
|
- 代理优先级:动态代理 > 代理列表(随机/默认) > 直连
|
||||||
- CPA / Sub2API / Team Manager 上传始终直连,不走代理;其中 CPA 可选把账号记录的代理写入 auth file 的 `proxy_url`
|
- CPA / Sub2API / Team Manager 上传始终直连,不走代理;其中 CPA 可选把账号记录的代理写入 auth file 的 `proxy_url`
|
||||||
- 注册时自动随机生成用户名和生日(年龄范围 18-45 岁)
|
- 注册时自动随机生成用户名和生日(年龄范围 18-45 岁)
|
||||||
|
- 验证码重发:收件箱超时未获取到验证码时,自动重新发送验证码并再次轮询,最多重发次数可在「验证码配置」中设置(默认 2 次,设为 0 禁用)
|
||||||
|
- 支付链接货币与计费国家动态对应,从 ChatGPT API 实时获取国家/货币列表(缓存 7 天),不再受限于内置静态映射表
|
||||||
- 支付链接生成使用账号 access_token 鉴权,走全局代理配置
|
- 支付链接生成使用账号 access_token 鉴权,走全局代理配置
|
||||||
- 无痕打开支付页默认调用系统 Chrome/Edge 的隐私模式
|
- 无痕打开支付页默认调用系统 Chrome/Edge 的隐私模式
|
||||||
- 订阅状态自动检测调用 `chatgpt.com/backend-api/me`,走全局代理
|
- 订阅状态自动检测调用 `chatgpt.com/backend-api/me`,走全局代理
|
||||||
|
|||||||
@@ -356,6 +356,12 @@ SETTING_DEFINITIONS: Dict[str, SettingDefinition] = {
|
|||||||
category=SettingCategory.EMAIL,
|
category=SettingCategory.EMAIL,
|
||||||
description="验证码轮询间隔(秒)"
|
description="验证码轮询间隔(秒)"
|
||||||
),
|
),
|
||||||
|
"email_code_resend_max_retries": SettingDefinition(
|
||||||
|
db_key="email_code.resend_max_retries",
|
||||||
|
default_value=2,
|
||||||
|
category=SettingCategory.EMAIL,
|
||||||
|
description="收件箱未找到验证码时,最多重新发送验证码的次数"
|
||||||
|
),
|
||||||
|
|
||||||
# Outlook 配置
|
# Outlook 配置
|
||||||
"outlook_provider_priority": SettingDefinition(
|
"outlook_provider_priority": SettingDefinition(
|
||||||
@@ -407,6 +413,7 @@ SETTING_TYPES: Dict[str, Type] = {
|
|||||||
"cpa_enabled": bool,
|
"cpa_enabled": bool,
|
||||||
"email_code_timeout": int,
|
"email_code_timeout": int,
|
||||||
"email_code_poll_interval": int,
|
"email_code_poll_interval": int,
|
||||||
|
"email_code_resend_max_retries": int,
|
||||||
"outlook_provider_priority": list,
|
"outlook_provider_priority": list,
|
||||||
"outlook_health_failure_threshold": int,
|
"outlook_health_failure_threshold": int,
|
||||||
"outlook_health_disable_duration": int,
|
"outlook_health_disable_duration": int,
|
||||||
@@ -707,6 +714,7 @@ class Settings(BaseModel):
|
|||||||
# 验证码配置
|
# 验证码配置
|
||||||
email_code_timeout: int = 120
|
email_code_timeout: int = 120
|
||||||
email_code_poll_interval: int = 3
|
email_code_poll_interval: int = 3
|
||||||
|
email_code_resend_max_retries: int = 2
|
||||||
|
|
||||||
# Outlook 配置
|
# Outlook 配置
|
||||||
outlook_provider_priority: List[str] = ["imap_old", "imap_new", "graph_api"]
|
outlook_provider_priority: List[str] = ["imap_old", "imap_new", "graph_api"]
|
||||||
|
|||||||
@@ -96,12 +96,13 @@ def generate_plus_link(
|
|||||||
account: Account,
|
account: Account,
|
||||||
proxy: Optional[str] = None,
|
proxy: Optional[str] = None,
|
||||||
country: str = "SG",
|
country: str = "SG",
|
||||||
|
currency: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成 Plus 支付链接(后端携带账号 cookie 发请求)"""
|
"""生成 Plus 支付链接(后端携带账号 cookie 发请求)"""
|
||||||
if not account.access_token:
|
if not account.access_token:
|
||||||
raise ValueError("账号缺少 access_token")
|
raise ValueError("账号缺少 access_token")
|
||||||
|
|
||||||
currency = _COUNTRY_CURRENCY_MAP.get(country, "USD")
|
currency = currency or _COUNTRY_CURRENCY_MAP.get(country, "USD")
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {account.access_token}",
|
"Authorization": f"Bearer {account.access_token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -145,12 +146,13 @@ def generate_team_link(
|
|||||||
seat_quantity: int = 5,
|
seat_quantity: int = 5,
|
||||||
proxy: Optional[str] = None,
|
proxy: Optional[str] = None,
|
||||||
country: str = "SG",
|
country: str = "SG",
|
||||||
|
currency: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成 Team 支付链接(后端携带账号 cookie 发请求)"""
|
"""生成 Team 支付链接(后端携带账号 cookie 发请求)"""
|
||||||
if not account.access_token:
|
if not account.access_token:
|
||||||
raise ValueError("账号缺少 access_token")
|
raise ValueError("账号缺少 access_token")
|
||||||
|
|
||||||
currency = _COUNTRY_CURRENCY_MAP.get(country, "USD")
|
currency = currency or _COUNTRY_CURRENCY_MAP.get(country, "USD")
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {account.access_token}",
|
"Authorization": f"Bearer {account.access_token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1537,19 +1537,31 @@ class RegistrationEngine:
|
|||||||
result.error_message = "发送验证码失败"
|
result.error_message = "发送验证码失败"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 10. 获取验证码
|
# 10. 获取验证码(支持重发重试)
|
||||||
self._log("10. 等待验证码...")
|
self._log("10. 等待验证码...")
|
||||||
self._emit_status("otp_secondary", "等待验证码邮件", step_index=10)
|
self._emit_status("otp_secondary", "等待验证码邮件", step_index=10)
|
||||||
otp_phase_started_at = time.time()
|
otp_phase_started_at = time.time()
|
||||||
code, otp_phase = self._phase_otp_secondary(
|
_resend_max = get_settings().email_code_resend_max_retries
|
||||||
PhaseContext(otp_sent_at=self._otp_sent_at),
|
code, otp_phase = None, None
|
||||||
started_at=otp_phase_started_at,
|
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:
|
if not code:
|
||||||
result.error_message = (
|
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
|
return result
|
||||||
|
|
||||||
# 11. 验证验证码
|
# 11. 验证验证码
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class GenerateLinkRequest(BaseModel):
|
|||||||
seat_quantity: int = 5
|
seat_quantity: int = 5
|
||||||
proxy: Optional[str] = None
|
proxy: Optional[str] = None
|
||||||
auto_open: bool = False # 生成后是否自动无痕打开
|
auto_open: bool = False # 生成后是否自动无痕打开
|
||||||
country: str = "SG" # 计费国家,决定货币 # 生成后是否自动无痕打开
|
country: str = "SG" # 计费国家,决定货币
|
||||||
|
currency: Optional[str] = None # 前端动态获取的货币代码,优先于静态映射表
|
||||||
|
|
||||||
|
|
||||||
class OpenIncognitoRequest(BaseModel):
|
class OpenIncognitoRequest(BaseModel):
|
||||||
@@ -70,7 +71,7 @@ def generate_payment_link(request: GenerateLinkRequest):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if request.plan_type == "plus":
|
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":
|
elif request.plan_type == "team":
|
||||||
link = generate_team_link(
|
link = generate_team_link(
|
||||||
account,
|
account,
|
||||||
@@ -79,6 +80,7 @@ def generate_payment_link(request: GenerateLinkRequest):
|
|||||||
seat_quantity=request.seat_quantity,
|
seat_quantity=request.seat_quantity,
|
||||||
proxy=proxy,
|
proxy=proxy,
|
||||||
country=request.country,
|
country=request.country,
|
||||||
|
currency=request.currency,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="plan_type 必须为 plus 或 team")
|
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 country = document.getElementById('country-select').value || 'SG';
|
||||||
|
|
||||||
|
const currency = countryCurrencyMap[country] || '';
|
||||||
const body = {
|
const body = {
|
||||||
account_id: parseInt(accountId),
|
account_id: parseInt(accountId),
|
||||||
plan_type: selectedPlan,
|
plan_type: selectedPlan,
|
||||||
country: country,
|
country: country,
|
||||||
|
currency: currency,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedPlan === 'team') {
|
if (selectedPlan === 'team') {
|
||||||
|
|||||||
Reference in New Issue
Block a user