wechatpay-apiv3 / wechatpay-php

微信支付 APIv3 的官方 PHP Library,同时也支持 APIv2
Apache License 2.0
490 stars 100 forks source link

企业付款到零钱问题 #78

Closed baofeng15 closed 2 years ago

baofeng15 commented 2 years ago

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

TheNorthMemory commented 2 years ago

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

baofeng15 commented 2 years ago

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

xy-peng commented 2 years ago

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

baofeng15 commented 2 years ago

//企业付款到零钱 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;
}
TheNorthMemory commented 2 years ago

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

https://github.com/wechatpay-apiv3/wechatpay-php/blob/d384ebae883f6c9075d11fda4a59e969818b91b7/tests/OpenAPI/V2/Mmpaymkttransfers/Promotion/TransfersTest.php#L135-L140

$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未被重置也应是幂等的,没道理重复付款),可以调整代码逻辑为同步模式如:

https://github.com/wechatpay-apiv3/wechatpay-php/blob/d384ebae883f6c9075d11fda4a59e969818b91b7/tests/OpenAPI/V2/Mmpaymkttransfers/Promotion/TransfersTest.php#L94-L96

TheNorthMemory commented 2 years ago

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

baofeng15 commented 2 years ago

有验返回值的,如下: $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']; }

TheNorthMemory commented 2 years ago

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

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

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

TheNorthMemory commented 2 years ago

这个接口是特殊接口之一,返回值没有sign字典,无法验签;而SDK默认是强验签,如果没有给特殊请求 handler 则会把所有返回值均打入 RejectedPromise,建议按照 https://github.com/wechatpay-apiv3/wechatpay-php/issues/78#issuecomment-1004819696 方式改造请求参数