165 lines
4.9 KiB
JavaScript
165 lines
4.9 KiB
JavaScript
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,
|
||
};
|
||
}
|