|
|
@@ -776,10 +776,16 @@ const PARSE_BASE = API_BASE + '/parse/functions';
|
|
|
const PAY_COMPANY = '1AiWpTEDH9';
|
|
|
const APP_ID = 'ncloudmaster';
|
|
|
|
|
|
+// 充值云函数ID — 微信支付回调自动执行 apigRechargeOnPay
|
|
|
+// 部署云函数后将 objectId 填入此处,或通过 URL 参数 fun_id 传入
|
|
|
+const DEFAULT_FUN_ID = 'HOkkX72PMF';
|
|
|
+
|
|
|
// URL params
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
-const authId = params.get('authid') || '';
|
|
|
+let authId = params.get('authid') || '';
|
|
|
const userId = params.get('user') || params.get('userid') || '';
|
|
|
+const apigId = params.get('apigid') || '';
|
|
|
+const funId = params.get('fun_id') || DEFAULT_FUN_ID;
|
|
|
|
|
|
let apig = null;
|
|
|
let selectedIndex = 0;
|
|
|
@@ -790,8 +796,8 @@ let pollTimer = null;
|
|
|
|
|
|
// ─── Init ───
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
- if (!authId) {
|
|
|
- showError('缺少 authid 参数,请通过正确链接访问本页面。');
|
|
|
+ if (!authId && !(userId && apigId)) {
|
|
|
+ showError('缺少参数。请传入 authid,或同时传入 user 和 apigid。');
|
|
|
return;
|
|
|
}
|
|
|
// Show loading state
|
|
|
@@ -803,6 +809,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
// ─── Load APIG Info ───
|
|
|
async function loadApig() {
|
|
|
try {
|
|
|
+ // 如果没有 authId 但有 user+apigid,先查询/创建 APIGAuth
|
|
|
+ if (!authId && userId && apigId) {
|
|
|
+ console.log('[INIT] 无 authId,通过 user+apigid 查询 APIGAuth...');
|
|
|
+ authId = await resolveAuthId(userId, apigId);
|
|
|
+ if (!authId) {
|
|
|
+ showError('无法获取或创建计费账套,请联系管理员。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ console.log('[INIT] 获取到 authId:', authId);
|
|
|
+ }
|
|
|
+
|
|
|
// 1. Try getApig REST endpoint (works when authId is an APIGAuth objectId)
|
|
|
const resp = await postJSON(API_BASE + '/api/apig/getApig', { authid: authId });
|
|
|
if (resp.code === 200 && resp.data) {
|
|
|
@@ -815,8 +832,6 @@ async function loadApig() {
|
|
|
}
|
|
|
|
|
|
// 2. Fallback: use cloud function to query APIG by objectId
|
|
|
- // Cloud function IDs: getApigInfo=t012qy4XVe, getApigAuth=H7X7y9Wfc0
|
|
|
- // Note: FmodeParse SDK uses internal dispatch; direct REST call needs actual function name
|
|
|
console.log('getApig returned no data, trying cloud function...');
|
|
|
const cfFuncName = params.get('cfName') || 'getApigInfo';
|
|
|
const cfResp = await parseCloudCall(cfFuncName, {
|
|
|
@@ -835,32 +850,62 @@ async function loadApig() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 3. Fallback: accept APIG data from URL params (OpenClaw can pass these)
|
|
|
- const urlTitle = params.get('title');
|
|
|
- const urlPrices = params.get('prices');
|
|
|
- const urlCounts = params.get('counts');
|
|
|
- if (urlTitle && urlPrices && urlCounts) {
|
|
|
- console.log('Using APIG data from URL params');
|
|
|
- const prices = urlPrices.split(',').map(Number);
|
|
|
- const counts = urlCounts.split(',').map(Number);
|
|
|
- apig = {
|
|
|
- objectId: authId,
|
|
|
- title: urlTitle,
|
|
|
- content: params.get('desc') || '',
|
|
|
- count: parseInt(params.get('balance') || '0'),
|
|
|
- priceStep: prices.map((p, i) => ({ price: p, count: counts[i] || 0 }))
|
|
|
- };
|
|
|
- applyTestTier();
|
|
|
- renderApigInfo();
|
|
|
- renderTiers();
|
|
|
- loadOrderHistory();
|
|
|
- return;
|
|
|
+ showError('无法加载接口信息。请确认参数正确。');
|
|
|
+ } catch (e) {
|
|
|
+ showError('网络错误: ' + e.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ─── 通过 user+apigid 查询或创建 APIGAuth ───
|
|
|
+async function resolveAuthId(user, apig) {
|
|
|
+ // 1. 查询是否已有 APIGAuth(user 对应 company,api 对应 APIG)
|
|
|
+ try {
|
|
|
+ const query = {
|
|
|
+ _method: 'GET',
|
|
|
+ where: JSON.stringify({
|
|
|
+ api: { __type: 'Pointer', className: 'APIG', objectId: apig },
|
|
|
+ company: { __type: 'Pointer', className: 'Company', objectId: user }
|
|
|
+ }),
|
|
|
+ limit: 1
|
|
|
+ };
|
|
|
+ const resp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(query), {
|
|
|
+ headers: { 'X-Parse-Application-Id': APP_ID }
|
|
|
+ });
|
|
|
+ const data = await resp.json();
|
|
|
+ if (data.results && data.results.length > 0) {
|
|
|
+ console.log('[resolveAuthId] 找到已有 APIGAuth:', data.results[0].objectId);
|
|
|
+ return data.results[0].objectId;
|
|
|
}
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('[resolveAuthId] 查询失败:', e.message);
|
|
|
+ }
|
|
|
|
|
|
- showError('无法加载接口信息。请确认 authid 对应有效的 APIGAuth 记录,或通过 URL 参数传入套餐数据。');
|
|
|
+ // 2. 不存在则创建新的 APIGAuth
|
|
|
+ try {
|
|
|
+ console.log('[resolveAuthId] 未找到 APIGAuth,创建新记录...');
|
|
|
+ const createResp = await fetch(API_BASE + '/parse/classes/APIGAuth', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'X-Parse-Application-Id': APP_ID,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ api: { __type: 'Pointer', className: 'APIG', objectId: apig },
|
|
|
+ company: { __type: 'Pointer', className: 'Company', objectId: user },
|
|
|
+ count: 0,
|
|
|
+ used: 0
|
|
|
+ })
|
|
|
+ });
|
|
|
+ const created = await createResp.json();
|
|
|
+ if (created.objectId) {
|
|
|
+ console.log('[resolveAuthId] 创建成功:', created.objectId);
|
|
|
+ return created.objectId;
|
|
|
+ }
|
|
|
+ console.error('[resolveAuthId] 创建失败:', JSON.stringify(created));
|
|
|
} catch (e) {
|
|
|
- showError('网络错误: ' + e.message);
|
|
|
+ console.error('[resolveAuthId] 创建异常:', e.message);
|
|
|
}
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
function applyTestTier() {
|
|
|
@@ -951,23 +996,56 @@ async function startPayment() {
|
|
|
|
|
|
const tier = apig.priceStep[selectedIndex];
|
|
|
|
|
|
- // 2. Call pay_code2 to get QR URL
|
|
|
- const payResp = await postJSON(PARSE_BASE + '/pay_code2', {
|
|
|
+ // 2. Create order FIRST (so backend callback can find it)
|
|
|
+ console.log('[PAY] Creating order...', { tradeNo, tier: tier.price, funId });
|
|
|
+ await createOrder(tier);
|
|
|
+ console.log('[PAY] Order created, order=', order);
|
|
|
+
|
|
|
+ // 3. Call pay_code2 to get QR URL (with fun_id for auto-recharge)
|
|
|
+ const payParams = {
|
|
|
_ApplicationId: APP_ID,
|
|
|
company: PAY_COMPANY,
|
|
|
out_trade_no: tradeNo,
|
|
|
total_fee: +tier.price,
|
|
|
body: apig.title + ' 接口充值'
|
|
|
- });
|
|
|
+ };
|
|
|
+ // 传入 fun_id: 微信支付成功后回调自动执行充值云函数
|
|
|
+ if (funId) {
|
|
|
+ payParams.fun_id = funId;
|
|
|
+ }
|
|
|
+ console.log('[PAY] Calling pay_code2 with params:', JSON.stringify(payParams));
|
|
|
+ let payResp = null;
|
|
|
+ try {
|
|
|
+ const payController = new AbortController();
|
|
|
+ const payTimeout = setTimeout(() => payController.abort(), 15000);
|
|
|
+ const rawResp = await fetch(PARSE_BASE + '/pay_code2', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payParams),
|
|
|
+ signal: payController.signal
|
|
|
+ });
|
|
|
+ clearTimeout(payTimeout);
|
|
|
+ payResp = await rawResp.json();
|
|
|
+ console.log('[PAY] pay_code2 response:', JSON.stringify(payResp));
|
|
|
+ } catch (fetchErr) {
|
|
|
+ console.error('[PAY] pay_code2 fetch error:', fetchErr.message);
|
|
|
+ // 如果带 fun_id 请求失败,尝试不带 fun_id 重试
|
|
|
+ if (payParams.fun_id) {
|
|
|
+ console.log('[PAY] Retrying pay_code2 WITHOUT fun_id...');
|
|
|
+ delete payParams.fun_id;
|
|
|
+ payResp = await postJSON(PARSE_BASE + '/pay_code2', payParams);
|
|
|
+ console.log('[PAY] pay_code2 retry response:', JSON.stringify(payResp));
|
|
|
+ } else {
|
|
|
+ throw fetchErr;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (!payResp.result || !payResp.result.code_url) {
|
|
|
- throw new Error('获取支付码失败');
|
|
|
+ if (!payResp || !payResp.result || !payResp.result.code_url) {
|
|
|
+ throw new Error('获取支付码失败: ' + JSON.stringify(payResp));
|
|
|
}
|
|
|
const codeUrl = payResp.result.code_url[0];
|
|
|
nonceStr = payResp.result.nonce_str;
|
|
|
-
|
|
|
- // 3. Create order
|
|
|
- createOrder(tier);
|
|
|
+ console.log('[PAY] QR code URL:', codeUrl);
|
|
|
|
|
|
// 4. Show QR modal
|
|
|
document.getElementById('modalPrice').textContent = tier.price;
|
|
|
@@ -1041,44 +1119,60 @@ function startPolling() {
|
|
|
}
|
|
|
|
|
|
async function doRecharge() {
|
|
|
- // Show success first, then try recharge in background (saveRecharge may timeout)
|
|
|
showSuccess();
|
|
|
+ const oldCount = apig.count || 0;
|
|
|
+ const tier = apig.priceStep[selectedIndex];
|
|
|
+ console.log('═══ [RECHARGE] 开始 ═══ oldCount:', oldCount, 'addCount:', tier.count, 'fun_id:', funId);
|
|
|
+
|
|
|
+ // 步骤1: 等待后端微信回调自动执行云函数(fun_id 机制)
|
|
|
+ // 轮询 8 次(间隔 3 秒,共 24 秒)— 微信回调通常 5~15 秒到达
|
|
|
+ for (let i = 1; i <= 8; i++) {
|
|
|
+ console.log('[RECHARGE] 步骤1: 等待后端回调... 第' + i + '/8次 (3秒后)');
|
|
|
+ await new Promise(r => setTimeout(r, 3000));
|
|
|
+ await refreshBalance();
|
|
|
+ if (apig.count > oldCount) {
|
|
|
+ console.log('[RECHARGE] ✅ 后端回调充值成功! 余额:', oldCount, '→', apig.count);
|
|
|
+ console.log('═══ [RECHARGE] 结束 ═══ 最终余额:', apig.count);
|
|
|
+ loadOrderHistory();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤2: 后端回调未生效,调用 saveRecharge 保底
|
|
|
+ console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
|
|
|
try {
|
|
|
- const tier = apig.priceStep[selectedIndex];
|
|
|
- // Build recharge body
|
|
|
- // authid = APIGAuth objectId (T0iOotHcDX)
|
|
|
- // apigid = APIG objectId from order or from getApig response
|
|
|
- const apigObjId = apig.objectId || authId;
|
|
|
- const body = {
|
|
|
+ const rechargeBody = {
|
|
|
+ user: userId || authId,
|
|
|
+ authComp: userId || authId,
|
|
|
authid: authId,
|
|
|
- apigid: apigObjId,
|
|
|
- oldCount: apig.count || 0,
|
|
|
+ apigid: apig.objectId,
|
|
|
+ oldCount: oldCount,
|
|
|
count: tier.count,
|
|
|
orderid: order ? order.objectId : ''
|
|
|
};
|
|
|
- // Server requires fcompany or user; fallback to authId if userId is empty
|
|
|
- body.user = userId || authId;
|
|
|
- body.authComp = userId || authId;
|
|
|
- body.fcompany = userId || authId;
|
|
|
- console.log('saveRecharge body:', JSON.stringify(body));
|
|
|
- // Use AbortController for 15s timeout
|
|
|
- const controller = new AbortController();
|
|
|
- const timer = setTimeout(() => controller.abort(), 15000);
|
|
|
- const resp = await fetch(API_BASE + '/api/apig/saveRecharge', {
|
|
|
- method: 'POST',
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify(body),
|
|
|
- signal: controller.signal
|
|
|
- });
|
|
|
- clearTimeout(timer);
|
|
|
- const result = await resp.json().catch(() => null);
|
|
|
- console.log('saveRecharge response:', result);
|
|
|
- // Refresh balance display
|
|
|
- await refreshBalance();
|
|
|
+ console.log('[RECHARGE] saveRecharge 参数:', JSON.stringify(rechargeBody));
|
|
|
+ const rechargeResp = await postJSON(API_BASE + '/api/apig/saveRecharge', rechargeBody);
|
|
|
+ console.log('[RECHARGE] saveRecharge 响应:', JSON.stringify(rechargeResp));
|
|
|
} catch (e) {
|
|
|
- console.warn('Recharge call failed (non-critical):', e.message);
|
|
|
+ console.warn('[RECHARGE] saveRecharge 失败:', e.message);
|
|
|
}
|
|
|
- // Refresh order history to show the new order
|
|
|
+
|
|
|
+ // 步骤3: 再轮询 3 次确认余额(saveRecharge 或延迟回调)
|
|
|
+ for (let i = 1; i <= 3; i++) {
|
|
|
+ await new Promise(r => setTimeout(r, 2000));
|
|
|
+ await refreshBalance();
|
|
|
+ if (apig.count > oldCount) {
|
|
|
+ console.log('[RECHARGE] ✅ saveRecharge 充值成功! 余额:', oldCount, '→', apig.count);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ console.log('[RECHARGE] 步骤3: 第' + i + '次余额未变 (' + apig.count + ')');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (apig.count <= oldCount) {
|
|
|
+ console.warn('[RECHARGE] ⚠️ 充值可能延迟,请稍后刷新页面查看');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('═══ [RECHARGE] 结束 ═══ 最终余额:', apig.count);
|
|
|
loadOrderHistory();
|
|
|
}
|
|
|
|