wechatpay-apiv3/wechatpay-php

企业付款到零钱问题

baofeng15 opened this issue · 10 comments

用V2的企业付款到零钱功能,会出现偶尔重复付款多次的情况,可能是异步方式,多次请求了

口说无凭,你代码怎么写的?

按照例子写的,不知道是异步问题还是咋的,偶尔会出现重复付款问题

贴一下你的代码吧,show me the code,不然也没法看到具体是什么问题。注意隐藏掉敏感信息。

//企业付款到零钱
private function wechatpay_transfers($user_id, $money, $desc, $is_check_name = true) {
exit();
//ini_set("display_errors", "On");//打开错误提示
//ini_set("error_reporting", E_ALL);//显示所有错误
//ini_set("display_errors", "Off");
ini_set("error_reporting", E_ALL & ~E_USER_DEPRECATED);//显示所有错误,用户生成的弃用警告除外
$user = $this->db->select('id,real_name,openid')->from('user')->where(array('id' => $user_id))->get()->row_array();
if (!empty($user)) {
// 商户号,假定为1000100
$merchantId = config_item('pay_transfers_mchid');
// APIv2密钥(32字节) 假定为exposed_your_key_here_have_risks,使用请替换为实际值
$apiv2Key = config_item('pay_transfers_key');
// 商户私钥,文件路径假定为 /path/to/merchant/apiclient_key.pem
$merchantPrivateKeyFilePath = FCPATH . 'zhengshu/apiclient_key.pem';
// 商户证书,文件路径假定为 /path/to/merchant/apiclient_cert.pem
$merchantCertificateFilePath = FCPATH . 'zhengshu/apiclient_cert.pem';

        // 工厂方法构造一个实例
        $instance = WeChatPay\Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => 'nop',
            'privateKey' => 'any',
            'certs'      => ['any' => null],
            'secret'     => $apiv2Key,
            'merchant' => [
                'cert' => $merchantCertificateFilePath,
                'key'  => $merchantPrivateKeyFilePath,
            ],
        ]);
        
        $order_no = date('YmdHis') . str_pad(mt_rand(10, 999999), 6, '0', STR_PAD_BOTH);
        //企业付款到零钱,https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
        $res = $instance
        ->v2->mmpaymkttransfers->promotion->transfers
        ->postAsync([
            'xml' => [
                'mch_appid'        => config_item('pay_transfers_app_id'),
                'mchid'            => config_item('pay_transfers_mchid'),// 注意这个商户号,key是`mchid`非`mch_id`
                'partner_trade_no' => $order_no,
                'openid'           => $user['openid'],
                'check_name'       => $is_check_name ? 'FORCE_CHECK' : 'NO_CHECK',
                're_user_name'     => $user['real_name'],
                'amount'           => strval($money * 100),
                'desc'             => $desc
            ],
            'security' => true, //请求需要双向证书
            //'debug' => true //开启调试模式
        ])
        ->then(static function($response) {
            //print_r($response);
            return WeChatPay\Transformer::toArray((string)$response->getBody());
        })
        ->otherwise(static function($e) {
            //print_r($e);
            // 更多`$e`异常类型判断是必须的,这里仅列出一种可能情况,请根据实际对接过程调整并增加
            //if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
            //    return WeChatPay\Transformer::toArray((string)$e->getReason()->getBody());
            //}
            //return [];
            //echo $e->getBody();
            //echo $e->getBody()->__toString();
            //echo $e->getBody()->getContents();
            return WeChatPay\Transformer::toArray((string)$e->getBody());
        })
        ->wait();
        //print_r($res);
    }
    return $res;
}

从代码上看,特殊接口(无返回值验签)可以仿测试用例代码改进如下:

// yes, start with `@` to prevent the internal `E_USER_DEPRECATED`
@$endpoint->postAsync([
'xml' => $data, 'handler' => $stack
])->then(static function(ResponseInterface $res) {
self::responseAssertion($res);
})->wait();

$stack = $instance->getDriver()->select(\WeChatPay\ClientDecoratorInterface::XML_BASED)->getConfig('handler');
$stack = clone $stack;
$stack->remove('transform_response');

$res = @$instance
->v2->mmpaymkttransfers->promotion->transfers
->postAsync([
    'xml' => [
        'mch_appid'        => config_item('pay_transfers_app_id'),
        'mchid'            => config_item('pay_transfers_mchid'),
        'partner_trade_no' => $order_no,
        'openid'           => $user['openid'],
        'check_name'       => $is_check_name ? 'FORCE_CHECK' : 'NO_CHECK',
        'amount'           => strval((int)($money * 100)),
        'desc'             => $desc
    ] + ($is_check_name ? [
        're_user_name'     => $user['real_name'],
    ] : []),
    'security' => true,
    'handler'  => $stack,
])
->then(static function($response) {
    return \WeChatPay\Transformer::toArray((string)$response->getBody());
})
->wait();

如果怀疑是异步被重试(理论上即使被重试,partner_trade_no未被重置也应是幂等的,没道理重复付款),可以调整代码逻辑为同步模式如:

// yes, start with `@` to prevent the internal `E_USER_DEPRECATED`
$res = @$endpoint->post(['xml' => $data, 'handler' => $stack]);
self::responseAssertion($res);

重复付款多次 可能的原因是,当接口返回值 return_code=SUCESSresult_code=FAIL 时,你的代码逻辑有缺陷,未明确异常码err_code,换单重入了(按照文档要求,是需要再判断err_code当为下列枚举值时,需要 原商户订单号重试partner_trade_no 重入再试)。

  • NOTENOUGH
  • SYSTEMERROR
  • NAME_MISMATCH
  • SIGN_ERROR
  • FREQ_LIMIT
  • MONEY_LIMIT
  • CA_ERROR
  • V2_ACCOUNT_SIMPLE_BAN
  • PARAM_IS_NOT_UTF8
  • SENDNUM_LIMIT
  • SEND_MONEY_LIMIT
  • RECEIVED_MONEY_LIMIT

有验返回值的,如下:
$result = $this->wechatpay_transfers($cash['user_id'], $cash['total'], '提现', $is_check_name);
if ($result['return_code'] == 'SUCCESS') {
if ($result['result_code'] == 'SUCCESS') {
$partner_trade_no = $result['partner_trade_no'];
//$payment_no = $result['payment_no'];
//$payment_time = $result['payment_time'];
$this->_cash('cashed', 'cash_agree', $cash['user_id'], 0, $cash['money'], '同意提现' . $cash['money'] . '元');
$this->db->where('id', $cash['id'])->update('cash', array('order_no' => $partner_trade_no, 'examine_status' => 1, 'examine_reason' => empty($post['reason']) ? '' : $post['reason'], 'examine_time' => date('Y-m-d H:i:s')));
$this->_admin_log('同意提现', json_encode($post));
} else {
$return_code = 2;
$return_message = 'err_code:' . $result['err_code'] . ',err_code_des:' . $result['err_code_des'];
}
} else {
$return_code = 1;
$return_message = 'return_code:' . $result['return_code'] . ',return_msg:' . $result['return_msg'];
}

建议把付款单号从 $this->wechatpay_transfers 内部提出到外部,以最终付款(业务)状态决定是否需要需要重试,另外排查是不是因12种异常状态被系统自动恢复付款,可按如下步骤:

  1. 登录 pay.weixin.qq.com 查询付款记录,看下相同金额的付款单号是不是相邻很近(或者有多笔单号一致)的付款记录;
  2. 拿付款单号,到这里,联系官方在线技术支持,排查下是不是相邻很近的 原单 被系统自动 恢复付款 了;

同步模式报一下错误是啥原因?
The promise was rejected

这个接口是特殊接口之一,返回值没有sign字典,无法验签;而SDK默认是强验签,如果没有给特殊请求 handler 则会把所有返回值均打入 RejectedPromise,建议按照 #78 (comment) 方式改造请求参数