chore: checkpoint admin restore state
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user