diff --git a/src/web/app.py b/src/web/app.py index 8bb9710..52e02d4 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -160,6 +160,13 @@ def create_app() -> FastAPI: async def startup_event(): """应用启动事件""" import asyncio + from ..database.init_db import initialize_database + + # 确保数据库已初始化(reload 模式下子进程也需要初始化) + try: + initialize_database() + except Exception as e: + logger.warning(f"数据库初始化: {e}") # 设置 TaskManager 的事件循环 loop = asyncio.get_event_loop() diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index ba8a45b..f0f2256 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -787,6 +787,58 @@ async def batch_upload_accounts_to_cpa(request: BatchCPAUploadRequest): return results +class Sub2ApiUploadRequest(BaseModel): + """单账号 Sub2API 上传请求""" + service_id: Optional[int] = None + concurrency: int = 3 + priority: int = 50 + + +@router.post("/{account_id}/upload-sub2api") +async def upload_account_to_sub2api(account_id: int, request: Sub2ApiUploadRequest = None): + """上传单个账号到 Sub2API""" + from ...core.sub2api_upload import upload_to_sub2api + + service_id = request.service_id if request else None + concurrency = request.concurrency if request else 3 + priority = request.priority if request else 50 + + api_url = None + api_key = None + if service_id: + with get_db() as db: + svc = crud.get_sub2api_service_by_id(db, service_id) + if not svc: + raise HTTPException(status_code=404, detail="指定的 Sub2API 服务不存在") + api_url = svc.api_url + api_key = svc.api_key + else: + with get_db() as db: + svcs = crud.get_sub2api_services(db, enabled=True) + if svcs: + api_url = svcs[0].api_url + api_key = svcs[0].api_key + + if not api_url or not api_key: + raise HTTPException(status_code=400, detail="未找到可用的 Sub2API 服务,请先在设置中配置") + + with get_db() as db: + account = crud.get_account_by_id(db, account_id) + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + if not account.access_token: + return {"success": False, "error": "账号缺少 Token,无法上传"} + + success, message = upload_to_sub2api( + [account], api_url, api_key, + concurrency=concurrency, priority=priority + ) + if success: + return {"success": True, "message": message} + else: + return {"success": False, "error": message} + + class BatchSub2ApiUploadRequest(BaseModel): """批量 Sub2API 上传请求""" ids: List[int] = [] diff --git a/static/js/accounts.js b/static/js/accounts.js index fbb4751..3bf73e6 100644 --- a/static/js/accounts.js +++ b/static/js/accounts.js @@ -25,10 +25,8 @@ const elements = { refreshBtn: document.getElementById('refresh-btn'), batchRefreshBtn: document.getElementById('batch-refresh-btn'), batchValidateBtn: document.getElementById('batch-validate-btn'), - batchUploadCpaBtn: document.getElementById('batch-upload-cpa-btn'), - batchUploadSub2ApiBtn: document.getElementById('batch-upload-sub2api-btn'), + batchUploadBtn: document.getElementById('batch-upload-btn'), batchCheckSubBtn: document.getElementById('batch-check-sub-btn'), - batchUploadTmBtn: document.getElementById('batch-upload-tm-btn'), batchDeleteBtn: document.getElementById('batch-delete-btn'), exportBtn: document.getElementById('export-btn'), exportMenu: document.getElementById('export-menu'), @@ -95,17 +93,13 @@ function initEventListeners() { // 批量验证Token elements.batchValidateBtn.addEventListener('click', handleBatchValidate); - // 批量上传CPA - elements.batchUploadCpaBtn.addEventListener('click', handleBatchUploadCpa); - // 批量检测订阅 elements.batchCheckSubBtn.addEventListener('click', handleBatchCheckSubscription); - // 批量上传Sub2API - elements.batchUploadSub2ApiBtn.addEventListener('click', handleBatchUploadSub2Api); - - // 批量上传TM - elements.batchUploadTmBtn.addEventListener('click', handleBatchUploadTm); + // 上传下拉菜单 + document.getElementById('batch-upload-cpa-item').addEventListener('click', (e) => { e.preventDefault(); handleBatchUploadCpa(); }); + document.getElementById('batch-upload-sub2api-item').addEventListener('click', (e) => { e.preventDefault(); handleBatchUploadSub2Api(); }); + document.getElementById('batch-upload-tm-item').addEventListener('click', (e) => { e.preventDefault(); handleBatchUploadTm(); }); // 批量删除 elements.batchDeleteBtn.addEventListener('click', handleBatchDelete); @@ -324,15 +318,12 @@ function renderAccounts(accounts) { - - @@ -469,19 +460,15 @@ function updateBatchButtons() { elements.batchDeleteBtn.disabled = count === 0; elements.batchRefreshBtn.disabled = count === 0; elements.batchValidateBtn.disabled = count === 0; - elements.batchUploadCpaBtn.disabled = count === 0; - elements.batchUploadSub2ApiBtn.disabled = count === 0; + elements.batchUploadBtn.disabled = count === 0; elements.batchCheckSubBtn.disabled = count === 0; - elements.batchUploadTmBtn.disabled = count === 0; elements.exportBtn.disabled = count === 0; elements.batchDeleteBtn.textContent = count > 0 ? `🗑️ 删除 (${count})` : '🗑️ 批量删除'; elements.batchRefreshBtn.textContent = count > 0 ? `🔄 刷新 (${count})` : '🔄 刷新Token'; elements.batchValidateBtn.textContent = count > 0 ? `✅ 验证 (${count})` : '✅ 验证Token'; - elements.batchUploadCpaBtn.textContent = count > 0 ? `☁️ 上传 (${count})` : '☁️ 上传CPA'; - elements.batchUploadSub2ApiBtn.textContent = count > 0 ? `🔗 Sub2API (${count})` : '🔗 上传Sub2API'; + elements.batchUploadBtn.textContent = count > 0 ? `☁️ 上传 (${count})` : '☁️ 上传'; elements.batchCheckSubBtn.textContent = count > 0 ? `🔍 检测 (${count})` : '🔍 检测订阅'; - elements.batchUploadTmBtn.textContent = count > 0 ? `🚀 上传TM (${count})` : '🚀 上传TM'; } // 刷新单个账号Token @@ -816,6 +803,43 @@ function selectCpaService() { }); } +// 统一上传入口:弹出目标选择 +async function uploadAccount(id) { + const targets = [ + { label: '☁️ 上传到 CPA', value: 'cpa' }, + { label: '🔗 上传到 Sub2API', value: 'sub2api' }, + { label: '🚀 上传到 Team Manager', value: 'tm' }, + ]; + + const choice = await new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal active'; + modal.innerHTML = ` + `; + document.body.appendChild(modal); + modal.querySelector('#_upload-close').addEventListener('click', () => { modal.remove(); resolve(null); }); + modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); resolve(null); } }); + modal.querySelectorAll('button[data-val]').forEach(btn => { + btn.addEventListener('click', () => { modal.remove(); resolve(btn.dataset.val); }); + }); + }); + + if (!choice) return; + if (choice === 'cpa') return uploadToCpa(id); + if (choice === 'sub2api') return uploadToSub2Api(id); + if (choice === 'tm') return uploadToTm(id); +} + // 上传单个账号到CPA async function uploadToCpa(id) { const choice = await selectCpaService(); @@ -849,8 +873,8 @@ async function handleBatchUploadCpa() { const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到CPA吗?`); if (!confirmed) return; - elements.batchUploadCpaBtn.disabled = true; - elements.batchUploadCpaBtn.textContent = '上传中...'; + elements.batchUploadBtn.disabled = true; + elements.batchUploadBtn.textContent = '上传中...'; try { const payload = buildBatchPayload(); @@ -994,8 +1018,8 @@ async function handleBatchUploadSub2Api() { const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Sub2API 吗?`); if (!confirmed) return; - elements.batchUploadSub2ApiBtn.disabled = true; - elements.batchUploadSub2ApiBtn.textContent = '上传中...'; + elements.batchUploadBtn.disabled = true; + elements.batchUploadBtn.textContent = '上传中...'; try { const payload = buildBatchPayload(); @@ -1017,6 +1041,26 @@ async function handleBatchUploadSub2Api() { // ============== Team Manager 上传 ============== +// 上传单账号到 Sub2API +async function uploadToSub2Api(id) { + const choice = await selectSub2ApiService(); + if (choice === null) return; + try { + toast.info('正在上传到 Sub2API...'); + const payload = {}; + if (choice.service_id != null) payload.service_id = choice.service_id; + const result = await api.post(`/accounts/${id}/upload-sub2api`, payload); + if (result.success) { + toast.success('上传成功'); + loadAccounts(); + } else { + toast.error('上传失败: ' + (result.error || result.message || '未知错误')); + } + } catch (e) { + toast.error('上传失败: ' + e.message); + } +} + // 上传单账号到 Team Manager async function uploadToTm(id) { try { @@ -1039,8 +1083,8 @@ async function handleBatchUploadTm() { const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 Team Manager 吗?`); if (!confirmed) return; - elements.batchUploadTmBtn.disabled = true; - elements.batchUploadTmBtn.textContent = '上传中...'; + elements.batchUploadBtn.disabled = true; + elements.batchUploadBtn.textContent = '上传中...'; try { const result = await api.post('/payment/accounts/batch-upload-tm', buildBatchPayload()); diff --git a/templates/accounts.html b/templates/accounts.html index 6b0523e..fec857d 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -125,18 +125,19 @@ - - - +