apigRechargeOnPay.md 10 KB

apigRechargeOnPay 云函数

功能说明

微信支付成功后,由支付回调自动触发的 APIG 充值云函数。 通过 pay_code2fun_id 参数绑定,支付成功后微信回调自动执行,无需前端调用 saveRecharge

解决的问题: 用户支付后关闭页面导致余额不到账。

触发方式

  • 不需要前端主动调用
  • pay_code2 支付流程自动触发:前端请求 pay_code2 时传入 fun_id,微信支付成功回调后服务端自动执行该云函数

接收参数(由微信回调 PayReturn 数据传入)

云函数通过 request.params.data 接收微信支付回调数据(PayReturn),关键字段:

字段 说明
out_trade_no 商户订单号(前端生成的 tradeNo)
total_fee 支付金额(分)
result_code 支付结果,"SUCCESS" 表示成功
transaction_id 微信支付交易号

执行逻辑

1. 从回调数据提取 out_trade_no(即前端的 tradeNo)
2. 查询 APIGOrder 表 WHERE orderNum = out_trade_no → 找到对应订单
3. 校验订单未重复充值(detail.rechargedBy != 'apigRechargeOnPay')
4. 从 order.apig 获取 APIG objectId,从 order.detail.addCount 获取充值数
5. 查询 APIG 表获取当前余额
6. 更新 APIG.count = 当前余额 + detail.addCount
7. 更新 APIGOrder:isPay=true,合并 rechargedBy/rechargedAt/newCount 到 detail
8. 返回充值结果

云函数代码

重要: 此云函数由微信支付回调通过 fun_id 自动触发,不要在前端调用。 云函数平台部署,非 Parse 服务端函数。直接复制下方代码到云函数平台即可。

async function handler(request, response) {
  console.log('apigRechargeOnPay 开始');
  
  // DEBUG: 打印关键属性
  console.log('request.body:', JSON.stringify(request.body));
  
  try {
    // 1. 提取回调数据
    // 服务端调用云函数时传: { id: fun_id, params: jsonData }
    // 所以微信回调数据在 request.body.params 中
    var payData = null;
    var body = request.body;
    
    // body 可能是字符串,先解析
    if (typeof body === 'string') {
      try { body = JSON.parse(body); } catch(e) { body = {}; }
    }
    
    // 优先从 body.params 提取(服务端实际发送路径)
    if (body && body.params) {
      var d = body.params;
      if (typeof d === 'string') {
        try { d = JSON.parse(d); } catch(e) { d = {}; }
      }
      if (d.out_trade_no) {
        payData = d;
        console.log('从 request.body.params 提取, out_trade_no:', d.out_trade_no);
      }
    }
    
    // fallback: body.data(兼容旧版服务端)
    if (!payData && body && body.data) {
      var d2 = body.data;
      if (typeof d2 === 'string') {
        try { d2 = JSON.parse(d2); } catch(e) { d2 = {}; }
      }
      if (d2.out_trade_no) {
        payData = d2;
        console.log('从 request.body.data 提取, out_trade_no:', d2.out_trade_no);
      }
    }
    
    // fallback: 直接在 body 中找(兼容手动测试等场景)
    if (!payData && body && body.out_trade_no) {
      payData = body;
      console.log('从 request.body 直接提取, out_trade_no:', body.out_trade_no);
    }
    
    // fallback: request.params(兼容云函数平台手动测试)
    if (!payData && request.params) {
      var p = request.params;
      if (typeof p === 'string') {
        try { p = JSON.parse(p); } catch(e) { p = {}; }
      }
      if (p.out_trade_no) {
        payData = p;
        console.log('从 request.params 提取, out_trade_no:', p.out_trade_no);
      }
    }
    
    if (!payData || !payData.out_trade_no) {
      console.log('未找到out_trade_no, body:', JSON.stringify(body));
      response.json({ code: 400, success: false, error: '未找到out_trade_no' });
      return;
    }
    
    // 兼容数组格式(微信回调XML解析后字段可能是数组如 ["CU2026..."])
    var outTradeNo = payData.out_trade_no;
    if (Array.isArray(outTradeNo)) outTradeNo = outTradeNo[0];
    var transactionId = payData.transaction_id || '';
    if (Array.isArray(transactionId)) transactionId = transactionId[0] || '';
    
    // 2. 查询 APIGOrder(字段: orderNum=商户单号, apig=APIG指针, detail=JSON含addCount)
    var orderResult = await Psql.query(
      'SELECT * FROM "APIGOrder" WHERE "orderNum" = $1 ORDER BY "createdAt" DESC LIMIT 1',
      [outTradeNo]
    );
    
    if (!orderResult || orderResult.length === 0) {
      console.log('未找到订单, orderNum:', outTradeNo);
      response.json({ code: 404, success: false, error: '未找到订单: ' + outTradeNo });
      return;
    }
    
    var order = orderResult[0];
    var detail = order.detail;
    if (typeof detail === 'string') {
      try { detail = JSON.parse(detail); } catch(e) { detail = {}; }
    }
    if (!detail) detail = {};
    
    console.log('订单:', order.objectId, 'apig:', order.apig, 'isPay:', order.isPay, 'detail:', JSON.stringify(detail));
    
    // 3. 防重复: 已由云函数或 saveRecharge 处理过则跳过
    if (detail.rechargedBy === 'apigRechargeOnPay') {
      console.log('已由云函数充值,跳过');
      response.json({ code: 200, success: true, msg: '已充值(云函数)' });
      return;
    }
    if (order.isPay === true || order.isPay === 'true') {
      console.log('订单已支付(isPay=true),可能已由saveRecharge处理,跳过');
      response.json({ code: 200, success: true, msg: '已充值(isPay)' });
      return;
    }
    
    // apig 可能是: "Vo3ROWEvDy" 或 {"objectId":"Vo3ROWEvDy",...} 或 "APIG$Vo3ROWEvDy"
    var apigId = order.apig;
    if (typeof apigId === 'object' && apigId && apigId.objectId) {
      apigId = apigId.objectId;
    } else if (typeof apigId === 'string' && apigId.indexOf('$') > -1) {
      apigId = apigId.split('$')[1];
    }
    var addCount = parseInt(detail.addCount) || 0;
    
    console.log('解析后 apigId:', apigId, 'addCount:', addCount);
    
    if (!apigId || addCount <= 0) {
      console.log('数据异常 apig原始值:', JSON.stringify(order.apig), 'apigId:', apigId, 'addCount:', addCount);
      response.json({ code: 400, success: false, error: '订单数据异常' });
      return;
    }
    
    // 4. 获取 APIGAuth(计费表)objectId
    var apigAuthId = null;
    if (detail.apigAuth) {
      if (typeof detail.apigAuth === 'object' && detail.apigAuth.objectId) {
        apigAuthId = detail.apigAuth.objectId;
      } else if (typeof detail.apigAuth === 'string') {
        // 可能是 "APIGAuth$T0iOotHcDX" 格式
        apigAuthId = detail.apigAuth.indexOf('$') > -1 ? detail.apigAuth.split('$')[1] : detail.apigAuth;
      }
    }
    
    console.log('apigAuthId:', apigAuthId);
    
    if (!apigAuthId) {
      console.log('未找到apigAuth, detail:', JSON.stringify(detail));
      response.json({ code: 400, success: false, error: '订单缺少apigAuth信息' });
      return;
    }
    
    // 5. 查询 APIGAuth 计费表余额
    var authResult = await Psql.query(
      'SELECT "objectId", "count", "used" FROM "APIGAuth" WHERE "objectId" = $1 LIMIT 1',
      [apigAuthId]
    );
    
    if (!authResult || authResult.length === 0) {
      console.log('未找到APIGAuth:', apigAuthId);
      response.json({ code: 404, success: false, error: '未找到APIGAuth: ' + apigAuthId });
      return;
    }
    
    var currentCount = parseInt(authResult[0].count) || 0;
    var newCount = currentCount + addCount;
    console.log('APIGAuth余额计算:', currentCount, '+', addCount, '=', newCount);
    
    // 6. 更新 APIGAuth 计费表余额
    await Psql.query(
      'UPDATE "APIGAuth" SET "count" = $1, "updatedAt" = NOW() WHERE "objectId" = $2',
      [newCount, apigAuthId]
    );
    console.log('APIGAuth余额已更新:', newCount);
    
    // 7. 更新 APIGOrder: isPay=true, detail 加入充值标记
    detail.newCount = newCount;
    detail.transactionId = transactionId;
    detail.rechargedAt = new Date().toISOString();
    detail.rechargedBy = 'apigRechargeOnPay';
    
    await Psql.query(
      'UPDATE "APIGOrder" SET "isPay" = true, "detail" = $1::jsonb, "updatedAt" = NOW() WHERE "objectId" = $2',
      [JSON.stringify(detail), order.objectId]
    );
    console.log('订单已更新:', order.objectId);
    
    // 8. 返回
    response.json({ 
      code: 200, success: true, msg: '充值成功',
      data: { orderId: order.objectId, apigAuthId: apigAuthId, oldCount: currentCount, newCount: newCount }
    });
    console.log('apigRechargeOnPay 完成');

  } catch (error) {
    console.error('apigRechargeOnPay 失败:', error.message, error.stack);
    response.json({ code: 500, success: false, error: error.message });
  }
}

云函数部署说明

  • 云函数名称: apigRechargeOnPay
  • 触发方式: 由 pay_code2fun_id 参数绑定,微信支付回调后自动执行
  • 无需前端调用,完全由后端处理

部署步骤

  1. 在 Parse Dashboard 或服务端创建云函数,代码如上
  2. 获取云函数的 objectId 作为 fun_id
  3. 前端 pay_code2 调用时传入 fun_id: '<云函数objectId>'

前端适配(apig-pay.html)

升级前:

// 前端发起支付
const payResp = await postJSON(PARSE_BASE + '/pay_code2', {
  _ApplicationId: APP_ID,
  company: PAY_COMPANY,
  out_trade_no: tradeNo,
  total_fee: +tier.price,
  body: apig.title + ' 接口充值'
});
// ... 支付成功后前端调 saveRecharge(有页面关闭风险)

升级后:

// 前端发起支付 — 加入 fun_id
const payResp = await postJSON(PARSE_BASE + '/pay_code2', {
  _ApplicationId: APP_ID,
  company: PAY_COMPANY,
  out_trade_no: tradeNo,
  total_fee: +tier.price,
  body: apig.title + ' 接口充值',
  fun_id: FUN_ID  // 云函数ID,微信回调自动触发充值
});
// ... 支付成功后仅刷新余额展示(无需前端调 saveRecharge)

关键优势

  1. 防丢单: 充值由微信回调触发,用户关闭页面也能到账
  2. 防重复: 云函数内有防重复充值逻辑(检查 isPay + detail.addCount)
  3. 可追溯: detail 字段记录完整的充值链路信息
  4. 前端简化: 前端只需展示支付二维码和结果,无需处理充值逻辑