const DEFAULT_TIMEOUT_MS = 10000; function trimEnv(name) { const value = process.env[name]; return typeof value === 'string' ? value.trim() : ''; } function parseTimeout(value) { const n = Number(value); return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_MS; } async function safeJson(response) { const text = await response.text(); if (!text) return null; try { return JSON.parse(text); } catch { return text; } } function responseMessage(payload, fallback) { if (payload && typeof payload === 'object' && typeof payload.message === 'string' && payload.message.trim()) { return payload.message.trim(); } if (typeof payload === 'string' && payload.trim()) return payload.trim(); return fallback; } export function createPlanSyncService(logger = console) { const baseUrl = trimEnv('PLAN_SYNC_TARGET_BASE_URL').replace(/\/$/, ''); const username = trimEnv('PLAN_SYNC_USERNAME'); const password = trimEnv('PLAN_SYNC_PASSWORD'); const timeoutMs = parseTimeout(trimEnv('PLAN_SYNC_TIMEOUT_MS')); if (!baseUrl || !username || !password) { return { enabled: false, reason: '未配置生产套餐同步环境变量', async syncPlanUpsert() { return { enabled: false, ok: false, message: '未配置生产套餐同步环境变量' }; }, async syncPlanDelete() { return { enabled: false, ok: false, message: '未配置生产套餐同步环境变量' }; }, async syncAllPlans() { return { enabled: false, ok: false, message: '未配置生产套餐同步环境变量', synced: 0, total: 0 }; }, }; } async function request(path, init = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(`${baseUrl}${path}`, { ...init, signal: controller.signal }); } finally { clearTimeout(timer); } } async function loginAndGetCookie() { const response = await request('/auth/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ username, password }), }); const payload = await safeJson(response); if (!response.ok) { throw new Error(`生产后台登录失败:${responseMessage(payload, `${response.status} ${response.statusText}`)}`); } const cookie = response.headers.get('set-cookie'); if (!cookie) throw new Error('生产后台登录成功,但未返回会话 Cookie'); return cookie.split(';', 1)[0]; } async function syncRequest(path, init = {}) { const cookie = await loginAndGetCookie(); const response = await request(path, { ...init, headers: { 'content-type': 'application/json', 'x-plan-sync-hop': '1', ...(init.headers ?? {}), cookie, }, }); const payload = await safeJson(response); if (!response.ok) { throw new Error(responseMessage(payload, `${response.status} ${response.statusText}`)); } return payload; } async function syncPlanUpsert(planType, plan) { try { await syncRequest(`/admin-api/subscriptions/plans/${encodeURIComponent(planType)}`, { method: 'PUT', body: JSON.stringify(plan), }); return { enabled: true, ok: true, message: `已同步到生产后台 ${baseUrl}`, }; } catch (error) { logger.warn?.('[plan-sync] upsert failed', { planType, error: String(error) }); return { enabled: true, ok: false, message: error instanceof Error ? error.message : '同步生产后台失败', }; } } async function syncPlanDelete(planType) { try { await syncRequest(`/admin-api/subscriptions/plans/${encodeURIComponent(planType)}`, { method: 'DELETE', }); return { enabled: true, ok: true, message: `已同步删除生产后台套餐 ${planType}`, }; } catch (error) { logger.warn?.('[plan-sync] delete failed', { planType, error: String(error) }); return { enabled: true, ok: false, message: error instanceof Error ? error.message : '同步生产后台失败', }; } } async function syncAllPlans(plans) { let synced = 0; const failures = []; for (const plan of plans) { const result = await syncPlanUpsert(plan.planType, plan); if (result.ok) synced += 1; else failures.push(`${plan.planType}: ${result.message}`); } return { enabled: true, ok: failures.length === 0, message: failures.length === 0 ? `已同步 ${synced}/${plans.length} 个套餐到生产后台` : `已同步 ${synced}/${plans.length} 个套餐,失败:${failures.join(';')}`, synced, total: plans.length, failures, }; } return { enabled: true, reason: '', syncPlanUpsert, syncPlanDelete, syncAllPlans, }; }