feat(gpc): add auto mode permission checks

This commit is contained in:
朴圣佑
2026-05-09 23:23:32 +08:00
parent 872f382815
commit dfe5139250
16 changed files with 1086 additions and 72 deletions
+90 -9
View File
@@ -8,6 +8,8 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -86,6 +88,17 @@
return cleaned;
}
function normalizeGpcHelperPhoneMode(value = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
}
const normalized = String(value || '').trim().toLowerCase();
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
? GPC_HELPER_PHONE_MODE_AUTO
: GPC_HELPER_PHONE_MODE_MANUAL;
}
function normalizeGpcOtpChannel(value = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
@@ -143,6 +156,17 @@
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
}
function buildGpcBalanceUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
}
if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) {
return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
}
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
}
function unwrapGpcResponse(payload = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
@@ -176,6 +200,55 @@
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
}
function getGpcRemainingUses(payload = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload);
}
const data = unwrapGpcResponse(payload);
const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining);
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
}
function isGpcAutoModeEnabled(payload = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload);
}
const data = unwrapGpcResponse(payload);
return data?.auto_mode_enabled === true || data?.autoModeEnabled === true;
}
async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') {
const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl);
if (!apiUrl) {
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
}
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
'X-API-Key': apiKey,
},
}, 30000);
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`);
}
const balanceData = unwrapGpcResponse(data);
const remainingUses = getGpcRemainingUses(balanceData);
const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase();
if (status && status !== 'active') {
throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`);
}
if (remainingUses !== null && remainingUses <= 0) {
throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。');
}
if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) {
throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
}
}
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
const fetcher = typeof fetchImpl === 'function'
? fetchImpl
@@ -226,25 +299,31 @@
if (!apiUrl) {
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
}
const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode);
const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO;
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
const pin = String(state?.gopayHelperPin || '').trim();
const apiKey = resolveGpcHelperApiKey(state);
if (!phoneNumber) {
throw new Error('创建 GPC 订单失败:缺少手机号。');
if (!isAutoMode && !phoneNumber) {
throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
}
if (!pin) {
throw new Error('创建 GPC 订单失败:缺少 PIN。');
if (!isAutoMode && !pin) {
throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
}
throwIfStopped();
await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
throwIfStopped();
const payload = {
access_token: token,
phone_mode: 'manual',
country_code: countryCode,
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
phone_mode: phoneMode,
};
if (!isAutoMode) {
payload.country_code = countryCode;
payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
}
const orderCreatedAt = Date.now();
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
@@ -273,6 +352,7 @@
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
orderCreatedAt,
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode),
country: 'ID',
currency: 'IDR',
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
@@ -308,6 +388,7 @@
gopayHelperTaskStatus: result.taskStatus,
gopayHelperStatusText: result.statusText,
gopayHelperRemoteStage: result.remoteStage,
gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
gopayHelperTaskPayload: result.responsePayload,
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
@@ -318,7 +399,7 @@
gopayHelperStartPayload: null,
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
});
await addLog(`步骤 6GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await addLog(`步骤 6GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await completeStepFromBackground(6, {
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
+87 -14
View File
@@ -11,7 +11,27 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const GPC_TASK_POLL_INTERVAL_MS = 3000;
const GPC_REMOTE_STAGE_LABELS = {
auto_otp_wait: '等待自动 OTP',
checkout_order_start: '创建订单',
checkout_start: '创建订单',
completed: '充值完成',
gopay_validate_pin: '校验 PIN',
otp_ready: '等待 PIN',
otp_submitted_local: 'OTP 已提交',
payment_processing: '支付处理中',
pin_submitted_local: 'PIN 已提交',
sms_otp_wait: '等待短信 OTP',
whatsapp_otp_wait: '等待 WhatsApp OTP',
};
const GPC_WAITING_FOR_LABELS = {
auto_otp: '自动 OTP',
otp: 'OTP',
pin: 'PIN',
};
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
id: PLUS_PAYMENT_METHOD_PAYPAL,
@@ -112,6 +132,56 @@
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function normalizeGpcHelperPhoneMode(value = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
}
const normalized = String(value || '').trim().toLowerCase();
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
? GPC_HELPER_PHONE_MODE_AUTO
: GPC_HELPER_PHONE_MODE_MANUAL;
}
function formatGpcRemoteStageLabel(stage = '') {
const normalized = String(stage || '').trim().toLowerCase();
if (!normalized) {
return '';
}
return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
}
function formatGpcWaitingForLabel(waitingFor = '') {
const normalized = String(waitingFor || '').trim().toLowerCase();
if (!normalized) {
return '';
}
return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
}
function formatGpcTaskStatusLog(task = {}) {
const statusText = String(task?.status_text || task?.statusText || '').trim();
const status = String(task?.status || '').trim();
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
const stageText = formatGpcRemoteStageLabel(remoteStage);
const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
const mainText = stageText || statusText || status || '处理中';
const parts = [`步骤 7GPC 任务状态:${mainText}`];
if (waitingForText && !mainText.includes(waitingForText)) {
parts.push(`,等待 ${waitingForText}`);
}
return parts.join('');
}
function getGpcHelperPhoneMode(state = {}, task = null) {
return normalizeGpcHelperPhoneMode(
task?.phone_mode
|| task?.phoneMode
|| state?.gopayHelperPhoneMode
|| state?.phoneMode
);
}
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
}
@@ -299,7 +369,7 @@
task.task_id = String(task.task_id || task.taskId || '').trim();
task.status = String(task.status || '').trim().toLowerCase();
task.status_text = String(task.status_text || task.statusText || '').trim();
task.phone_mode = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase();
task.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
@@ -318,6 +388,7 @@
gopayHelperTaskId: task.task_id,
gopayHelperTaskStatus: task.status,
gopayHelperStatusText: task.status_text,
gopayHelperPhoneMode: task.phone_mode,
gopayHelperRemoteStage: task.remote_stage,
gopayHelperApiWaitingFor: task.api_waiting_for,
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
@@ -675,12 +746,16 @@
});
}
function isGpcTaskOtpWait(task = {}) {
return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
function isGpcTaskManualMode(task = {}, state = {}) {
return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
}
function isGpcTaskPinWait(task = {}) {
return task?.phone_mode === 'manual'
function isGpcTaskOtpWait(task = {}, state = {}) {
return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
}
function isGpcTaskPinWait(task = {}, state = {}) {
return isGpcTaskManualMode(task, state)
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
}
@@ -840,27 +915,25 @@
throw new Error('步骤 7GPC 模式缺少 API Key。');
}
const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
const rawPin = String(state?.gopayHelperPin || '').trim();
const pinDigits = rawPin.replace(/[^\d]/g, '');
const pin = normalizeSixDigitPin(rawPin);
if (!pin) {
if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
if (taskId && apiUrl && apiKey) {
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
}
throw new Error(pinDigits
? '步骤 7GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
: '步骤 7GPC 模式缺少 PIN 配置。');
: '步骤 7GPC 手动模式缺少 PIN 配置。');
}
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId}...`, 'info');
await addLog(`步骤 7GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId}...`, 'info');
try {
while (Date.now() <= deadline) {
throwIfStopped();
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
const statusText = task?.status_text || task?.status || '处理中';
const remoteStage = task?.remote_stage || '';
const waitingFor = task?.api_waiting_for || '';
await addLog(`步骤 7GPC 任务状态:${statusText}${remoteStage ? `${remoteStage}` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
await addLog(formatGpcTaskStatusLog(task), 'info');
if (task.status === 'completed') {
terminalReached = true;
@@ -879,7 +952,7 @@
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
}
if (isGpcTaskOtpWait(task)) {
if (isGpcTaskOtpWait(task, state)) {
if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'OTP');
}
@@ -940,7 +1013,7 @@
otpLastSubmittedAt = Date.now();
lastSubmittedOtp = normalizedOtp;
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
} else if (isGpcTaskPinWait(task) && !pinSubmitted) {
} else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'PIN');
}