wechat_miniprogram_refund.js 6.08 KB
const Router = require('koa-router');
const Promise = require('bluebird');
const tenpay = require('tenpay');
const config = require('config');
const mathjs = require('mathjs');
const route = new Router();
const payApi = new tenpay(
  Object.assign({}, config.get('wechatPay'), {
    pfx: require('fs').readFileSync(__dirname + '/../../cert/apiclient_cert.p12')
  }),
  true
);

const utils = require('../../utils/utils');
const { logger } = utils;

async function callRefundApi(redis, tradeNo, payPrice, refundFee, refundNo) {
  const logTitle = `【退款接口】【订单编号】:${tradeNo}`;
  // 获取退款单号序列
  const refundNoSuffix = await redis.incr(`wechat:refund:suffix:${tradeNo}`);
  const demoRefundFee = config.get('demo.refundFee');

  logger.info(
    `${logTitle},开始请求退款,【参数】:${JSON.stringify({
      out_trade_no: tradeNo,
      out_refund_no: refundNo ? refundNo : tradeNo + refundNoSuffix,
      total_fee: demoRefundFee || mathjs.evaluate(`${payPrice}*100`),
      refund_fee: demoRefundFee || mathjs.evaluate(`${refundFee}*100`)
    })}`
  );
  const resultRefund = await payApi.refund({
    out_trade_no: tradeNo,
    out_refund_no: refundNo ? refundNo : tradeNo + refundNoSuffix,
    total_fee: demoRefundFee || mathjs.evaluate(`${payPrice}*100`),
    refund_fee: demoRefundFee || mathjs.evaluate(`${refundFee}*100`)
  });

  const result =
    resultRefund.result_code.toUpperCase() === 'SUCCESS' &&
    resultRefund.return_code.toUpperCase() === 'SUCCESS';

  if (!result) {
    logger.info(
      `${logTitle},调用微信退款接口失败。【resultRefund】:${JSON.stringify(resultRefund)}`
    );
  } else {
    logger.info(`${logTitle},调用微信退款接口成功。`);
  }

  return result;
}

async function updateRefundStatus(connection, redis, order, wechatRefundProcessKey, refundFee) {
  const transitFee = (await redis.get(`wechat:refund:transit:${order.order_id}`)) || '0.00';
  // 如果所有金额都已退款,把订单置位申请退款中。否则订单为原状态
  const refundStatus = mathjs.evaluate(`${transitFee}/100+${refundFee}==${order.pay_price}`)
    ? 1
    : order.refund_status;
  const result = await connection.queryAsync(
    'UPDATE ct_store_order SET refund_status=? WHERE order_id=?',
    [refundStatus, order.order_id]
  );

  if (result && result.affectedRows) {
    // 设置时间间隔
    await redis.setex(wechatRefundProcessKey, 300, 1);
    // 设置在途资金
    await redis.incrby(
      `wechat:refund:transit:${order.order_id}`,
      mathjs.evaluate(`${refundFee}*100`)
    );

    logger.info(`【退款接口】【订单编号】:${order.order_id},退款申请已发出!`);

    return '退款申请已发出';
  } else {
    logger.info(
      `【退款接口】【订单编号】:${order.order_id},更新退款信息失败,【result】:${JSON.stringify(
        result
      )}`
    );
    return '退款申请已发出,更新退款信息失败';
  }
}

route.post('/wxpay/js/refund', async ctx => {
  const refundInfo = ctx.request.body;
  const tradeNo = refundInfo.tradeNo;
  const refundNo = refundInfo.refundNo;
  const logTitle = `【退款接口】【订单编号】:${tradeNo}`;
  const wechatRefundProcessKey = `wechat:refund:${tradeNo}`;

  // 记录客户端IP
  logger.info(`${logTitle},【客户端ip】:${ctx.ip}`);
  // 同一个订单退款间隔不能少于5分钟
  const processing = await ctx.redis.get(wechatRefundProcessKey);
  if (processing) {
    ctx.status = 400;
    return (ctx.body = '同一订单两次退款申请需要间隔5分钟');
  }

  if (refundInfo && tradeNo && refundInfo.refundFee) {
    logger.info(`${logTitle},【请求参数】:${JSON.stringify(refundInfo)}`);

    const connection = Promise.promisifyAll(await ctx.pool.getConnectionAsync());
    // 当前在途金额
    const transitFee = (await ctx.redis.get(`wechat:refund:transit:${tradeNo}`)) || '0.00';
    // 此次退款金额
    const refundFee = parseFloat(refundInfo.refundFee);
    // 查询订单真实性
    const result = await connection.queryAsync(
      'SELECT id,pay_price,refund_price,refund_status,order_id FROM ct_store_order WHERE order_id=?',
      [tradeNo]
    );

    if (result && result.length) {
      const order = result[0];

      // 退款金额大于实际支付金额,为避开小数,所有金额以分结算
      if (
        !refundNo &&
        mathjs.evaluate(
          `${transitFee}+${refundFee}*100+${order.refund_price}*100>${order.pay_price}*100`
        )
      ) {
        logger.info(`${logTitle},退款总额大于实付金额`);

        ctx.status = 400;
        ctx.body = '退款总额大于实付金额';
      } else {
        // 调用微信退款接口
        const callApiSuccess = await callRefundApi(
          ctx.redis,
          tradeNo,
          order.pay_price,
          refundFee,
          refundNo
        );
        if (callApiSuccess) {
          // 退款申请已发出,状态可置为200;
          ctx.status = 200;

          // 重新发起的退款
          if (refundNo) {
            ctx.body = '已发起重新退款';
          } else {
            try {
              ctx.body = await updateRefundStatus(
                connection,
                ctx.redis,
                order,
                wechatRefundProcessKey,
                refundFee
              );
            } catch (e) {
              logger.info(
                `${logTitle},更新退款信息失败,【错误】:${e.message},【wechatRefundProcessKey】:${wechatRefundProcessKey},refundFee】:${refundFee}`
              );
              ctx.body = '退款申请已发出,更新退款信息失败';
            }
          }
        } else {
          ctx.status = 500;
          ctx.body = '退款失败';
        }
      }
    } else {
      logger.info(`${logTitle},订单编号有误`);

      ctx.status = 400;
      ctx.body = '订单编号有误';
    }

    // 释放数据库链接
    connection.release();
  } else {
    logger.error(`${logTitle},未收到任何参数`);

    ctx.status = 400;
    ctx.body = '参数缺失';
  }
});

module.exports = route;