Files
2026-06-30 20:26:33 +08:00

165 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}