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,
|
|||
|
|
};
|
|||
|
|
}
|