基于TP6+Uniapp开发微信支付商家转账API最新对接教程(2025年11月)中篇:服务端开发实践(ThinkPHP6)

  • 原创
  • 作者:程序员三丰
  • 发布时间:2025-12-04 13:51
  • 浏览量:62
最近刚对接了微信官方商家转账API,基于此整理了3篇系列文档,从阅读文档到获取配置,再到开发实践整个过程,以作备忘。本文主要分享服务端基于TP6的开发实践,对接微信API,开发自己的应用API。

数据结构模型

注意:本文档中模型的写法是NiuShop的写法,NiuShop开发文档:https://www.kancloud.cn/niucloud/niushop_b2c_v5_dev/3049012

API请求日志记录数据模型

通过一个日志表来记录商户转账API相关所有请求日志,有助于开发调试。

数据库表 SQL:

CREATE TABLE `merchant_transfer_api_log` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `site_id` int NOT NULL DEFAULT '0' COMMENT '站点id',
    `merchant_id` int NOT NULL DEFAULT '0' COMMENT '商户id',
    `related_type` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '关联类型:merchant-member-withdraw=商户会员提现,member-withdraw=普通会员提现',
    `related_id` int NOT NULL DEFAULT '0' COMMENT '关联类型对应的记录id',
    `log_type` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '日志类型:api-request=创建类请求,api-query=查询类请求,api-notify=异步通知',
    `operator_identity` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '关联操作人身份:system=系统,platform-admin=平台管理员,merchant-admin=商户管理员,merchant-member=商户绑定会员,member=普通会员',
    `operator_id` int NOT NULL DEFAULT '0' COMMENT '关联操作人id,操作人身份为system时该字段的值为0,其他根据场景为操作人id',
    `api_desc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'api简介',
    `request_data` longtext CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '请求数据(JSON)',
    `response_data` longtext CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '响应数据(JSON)',
    `create_time` int NOT NULL DEFAULT '0' COMMENT '创建时间',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=62 DEFAULT CHARSET=utf8mb3 COMMENT='商户转账api请求日志表';

创建模型:

// file: app/model/merchant/MerchantTransferApiLog.php

<?php
/**
 *
 */

namespace app\model\merchant;

use app\model\BaseModel;
use Exception;
use think\facade\Cache;

/**
 * 转账请求日志
 */
class MerchantTransferApiLog extends BaseModel
{
    public string $table_name = 'merchant_transfer_api_log';

    public string $table_pk = 'id';

    /**
     * 关联类型 related_type :商户会员提现
     */
    const RELATED_TYPE_MCH_MEM_WITHDRAW = 'merchant-member-withdraw';
    /**
     * 关联类型 related_type :普通会员提现
     */
    const RELATED_TYPE_MEM_WITHDRAW = 'member-withdraw';

    const RELATED_TYPES = [
        'merchant-member-withdraw' => '商户会员提现',
        'member-withdraw' => '普通会员提现',
    ];

    /**
     * 日志类型 log_type :创建类请求
     */
    const LOG_TYPE_API_REQUEST = 'api-request';
    /**
     * 日志类型 log_type :查询类请求
     */
    const LOG_TYPE_API_QUERY = 'api-query';
    /**
     * 日志类型 log_type :异步通知
     */
    const LOG_TYPE_API_NOTIFY = 'api-notify';
    const LOG_TYPES = [
        'api-request' => '创建类请求',
        'api-query' => '查询类请求',
        'api-notify' => '异步通知'
    ];

    /**
     * 关联操作人身份 operator_identity :系统
     */
    const OPERATOR_IDENTITY_SYSTEM = 'system';
    /**
     * 关联操作人身份 operator_identity :平台管理员
     */
    const OPERATOR_IDENTITY_PLATFORM_ADMIN = 'platform-admin';
    /**
     * 关联操作人身份 operator_identity :商户管理员
     */
    const OPERATOR_IDENTITY_MERCHANT_ADMIN = 'merchant-admin';
    /**
     * 关联操作人身份 operator_identity :商户绑定会员
     */
    const OPERATOR_IDENTITY_MERCHANT_MEMBER = 'merchant-member';
    /**
     * 关联操作人身份 operator_identity :普通会员
     */
    const OPERATOR_IDENTITY_MEMBER = 'member';
    const OPERATOR_IDENTITIES = [
        'system' => '系统',
        'platform-admin' => '平台管理员',
        'merchant-admin' => '商户管理员',
        'merchant-member' => '商户绑定会员',
        'member' => '普通会员',
    ];

    public function addRecord($data)
    {
        $data['create_time'] = time();
        return model($this->table_name)->add($data);
    }

    public function update(array $data, array $condition): int
    {
        return model($this->table_name)->update($data, $condition);
    }
}

商户提现记录数据模型

数据库表 SQL:

CREATE TABLE `merchant_withdraw` (
  `id` int NOT NULL AUTO_INCREMENT,
  `site_id` int NOT NULL DEFAULT '0' COMMENT '站点id',
  `merchant_id` int NOT NULL DEFAULT '0' COMMENT '商户id',
  `withdraw_no` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '提现交易号',
  `member_id` int NOT NULL DEFAULT '0' COMMENT '会员id(操作提现的会员id,商家会绑定一个或多个会员)',
  `transfer_type` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '转账提现类型:wechat-wallet=微信零钱,bank-card=银行卡',
  `realname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '真实姓名',
  `apply_amount` decimal(10,2) NOT NULL COMMENT '提现申请金额',
  `rate` decimal(10,2) NOT NULL COMMENT '提现手续费比率',
  `service_money` decimal(10,2) NOT NULL COMMENT '提现手续费',
  `actual_amount` decimal(10,2) NOT NULL COMMENT '提现到账金额',
  `apply_time` int NOT NULL DEFAULT '0' COMMENT '申请时间',
  `audit_time` int NOT NULL DEFAULT '0' COMMENT '审核时间',
  `payment_time` int NOT NULL DEFAULT '0' COMMENT '转账时间',
  `transfer_bill_no` varchar(100) NOT NULL DEFAULT '' COMMENT '转账成功返回的微信单号',
  `status` int NOT NULL DEFAULT '0' COMMENT '状态:0=待审核,1=待转账,2=已转账,-1=拒绝,-2=转账失败',
  `memo` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '备注',
  `refuse_reason` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '拒绝理由',
  `bank_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '银行名称',
  `account_number` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '收款账号',
  `mobile` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
  `certificate` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '凭证',
  `certificate_remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '凭证说明',
  `fail_reason` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '失败原因',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb3 COMMENT='商圈商户提现记录表';

创建模型:

<?php
namespace app\model\merchant;

use app\model\BaseModel;
use Exception;
use think\facade\Cache;

/**
 * 商家提现
 */
class MerchantWithdraw extends BaseModel
{
    public string $table_name = 'merchant_withdraw';

    public string $table_pk = 'id';

    /**
     * 提现类型:微信零钱
     */
    const TRANS_TYPE_WECHAT_WALLET = 'wechat-wallet';
    /**
     * 提现类型:银行卡
     */
    const TRANS_TYPE_BANK_CARD = 'bank-card';

    const TRANS_TYPES = [
        'wechat-wallet' => '微信零钱',
        'bank-card' => '银行卡'
    ];

    /**
     * 提现状态:待审核
     */
    const TRANS_STATUS_APPLIED = 0;
    /**
     * 提现状态:待转账
     */
    const TRANS_STATUS_PENDING = 1;
    /**
     * 提现状态:已转账
     */
    const TRANS_STATUS_SUCCESS = 2;
    /**
     * 提现状态:拒绝
     */
    const TRANS_STATUS_REFUSE = -1;
    /**
     * 提现状态:转账失败
     */
    const TRANS_STATUS_FAIL = -2;
    const TRANS_STATUS = [
        0 => '待审核',
        1 => '待提现',
        2 => '已提现',
        -1 => '拒绝',
        -2 => '转账失败',
    ];

    public function addRecord($data)
    {
        $data['apply_time'] = time();
        return model($this->table_name)->add($data);
    }

    public function updateRecord(array $condition, array $data)
    {
        $res = model($this->table_name)->update($data, $condition);
        return $this->success($res);
    }

    public function getInfo(array $condition, string $fields = '*', string $alias = 'a', $join = null, $data = null)
    {
        $info = model($this->table_name)->getInfo($condition, $fields, $alias, $join, $data);
        if ($info) {
            $info['apply_time'] = date('Y-m-d H:i:s', $info['apply_time']);
            $info['audit_time'] = !empty($info['audit_time']) ? date('Y-m-d H:i:s', $info['audit_time']) : '-';
            $info['payment_time'] = !empty($info['payment_time']) ? date('Y-m-d H:i:s', $info['payment_time']) : '-';
            $info['transfer_type_text'] = self::TRANS_TYPES[$info['transfer_type']];
            $info['status_text'] = self::TRANS_STATUS[$info['status']];
        }
        return $this->success($info);
    }

    /**
     * 获取订单分页列表
     * @param array $condition
     * @param int $page
     * @param int $page_size
     * @param string $order
     * @param string $field
     * @param string $alias
     * @param array $join
     * @param string $group
     * @return array
     */
    public function getPageList(
        array  $condition = [],
        int    $page = 1,
        int    $page_size = PAGE_LIST_ROWS,
        string $order = 'a.apply_time desc',
        string $field = '*',
        string $alias = 'a',
        array  $join = [],
        string $group = ''): array
    {
        try {
            $res = model($this->table_name)->pageList($condition, $field, $order, $page, $page_size, $alias, $join, $group);

            foreach ($res['list'] as $k => &$v) {
                $v['apply_time'] = date('Y-m-d H:i:s', $v['apply_time']);
                $v['audit_time'] = !empty($v['audit_time']) ? date('Y-m-d H:i:s', $v['audit_time']) : '-';
                $v['payment_time'] = !empty($v['payment_time']) ? date('Y-m-d H:i:s', $v['payment_time']) : '-';
                $v['transfer_type_text'] = self::TRANS_TYPES[$v['transfer_type']];
                $v['status_text'] = self::TRANS_STATUS[$v['status']];
            }
            return $this->success($res);
        } catch (Exception $e) {
            return $this->error($e->getMessage());
        }
    }

    /**
     * 修改状态
     * @param int $id
     * @param int $status
     * @return array
     */
    public function modifyMerchantOrderStatus(int $id, int $status): array
    {
        // todo 此处需要增加根据实际业务增加检查状态更改逻辑及限制

        $res = model($this->table_name)->update(
            ['status' => $status],
            [[$this->table_pk, '=', $id]]
        );
        return $this->success($res);
    }

    /**
     * 提现流水号
     */
    public function createWithdrawNo()
    {
        $cache = Cache::get('merchant_withdraw_no' . time());
        if (empty($cache)) {
            Cache::set('niutk' . time(), 1000);
            $cache = Cache::get('merchant_withdraw_no' . time());
        } else {
            $cache = $cache + 1;
            Cache::set('merchant_withdraw_no' . time(), $cache);
        }
        $no = date('Ymdhis', time()) . rand(1000, 9999) . $cache;
        return $no;
    }
}

封装扩展工具类

该类与业务无关,便于移植、高效复用,纯工具类库。(建议在TP生态下使用,其他框架请根据实际情况调整)

// file: extend/WechatPayV3Lib.php

<?php

namespace extend;

use Exception;
use GuzzleHttp\Exception\RequestException;
use think\facade\Env;
use WeChatPay\Builder;
use WeChatPay\BuilderChainable;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Formatter;

/**
 * 微信支付 APIv3
 * @date 20251104
 * @author moyuy77 (微信号)
 */
class WechatPayV3Lib
{
    /**
     * @var BuilderChainable
     */
    public BuilderChainable $instance;

    public array $config = [];

    public function __construct(array $config)
    {
        // 商户号,如:1711028017
        $merchantId = $config['mch_id'];

        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
        // 如:E:\www\www.51blog.xyz\upload\wechatpay\cert\20250421112046174520564623813.pem
        $merchantPrivateKeyFilePath = $config['mch_pri_key_filepath'];
        $merchantPrivateKeyInstance = Rsa::from(file_get_contents($merchantPrivateKeyFilePath), Rsa::KEY_TYPE_PRIVATE);

        // 「商户API证书」的「证书序列号」,如:7988C8B2DD95C7D6DA23674FCEA1FF5B9C56F47C
        $merchantCertificateSerial = $config['mch_api_cert_serial'];

        // 从本地文件中加载「微信支付平台证书」,可由内置CLI工具下载到,用来验证微信支付应答的签名
        // 如:E:\www\www.51blog.xyz\upload\wechatpay\cert\wechatpay_1325FC2753504A95BF8D957AA781F2AB84F130AB.pem
        $platformCertificateFilePath = $config['platform_cert_filepath'];
        $onePlatformPublicKeyInstance = Rsa::from(file_get_contents($platformCertificateFilePath), Rsa::KEY_TYPE_PUBLIC);

        // 「微信支付平台证书」的「平台证书序列号」
        // 可以从「微信支付平台证书」文件解析,也可以在 商户平台 -> 账户中心 -> API安全 查询到
        // 如:1325FC2753504A95BF8D281AA781F2AB84F130AB
        $platformCertificateSerial = $config['platform_cert_serial'];

        // 从本地文件中加载「微信支付公钥」,用来验证微信支付应答的签名
        // 如:E:\www\www.51blog.xyz\upload\wechatpay\cert\pub_key.pem
        $platformPublicKeyFilePath = $config['platform_pub_key_filepath'];
        $twoPlatformPublicKeyInstance = Rsa::from(file_get_contents($platformPublicKeyFilePath), Rsa::KEY_TYPE_PUBLIC);

        // 「微信支付公钥」的「微信支付公钥ID」
        // 需要在 商户平台 -> 账户中心 -> API安全 查询
        // 如:PUB_KEY_ID_0117110272172025110300211591772205
        $platformPublicKeyId = $config['platform_pub_key_id'];

        // 构造一个 APIv3 客户端实例
        $this->instance = Builder::factory([
            'mchid' => $merchantId,
            'serial' => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs' => [
                $platformCertificateSerial => $onePlatformPublicKeyInstance,
                $platformPublicKeyId => $twoPlatformPublicKeyInstance,
            ],
        ]);

        $this->config = $config;
    }

    /**
     * 下载微信支付平台证书
     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012551764
     * @return array
     * @throws Exception
     */
    public function downloadPlatformCertificate(): array
    {
        try {
            $resp = $this->instance
                ->chain('v3/certificates')
                ->get();

            return json_decode((string)$resp->getBody(), true);
        } catch (Exception $e) {
            $errMsg = $e->getMessage();
            if (Env::get('app_debug') && $e instanceof RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $errMsg = $r->getStatusCode() . ' ' . $r->getReasonPhrase();
                $errMsg .= (string)$r->getBody();
                $errMsg .= $e->getTraceAsString();
            }

            throw new Exception($errMsg);
        }
    }

    /**
     * 商家转账 - 发起转账
     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012716434
     * @param array $params
     * @return array
     * @throws Exception
     */
    public function initiateMchTransfer(array $params): array
    {
        try {
            $body = [
                // 必填
                'appid' => $params['appid'],
                // 必填 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
                'out_bill_no' => $params['out_bill_no'],
                // 必填 【转账场景ID】 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1000(现金营销),1006(企业报销)等
                'transfer_scene_id' => $params['transfer_scene_id'],
                // 必填 【收款用户OpenID】 用户在商户appid下的唯一标识。
                'openid' => $params['openid'],
                // 【收款用户姓名】 收款方真实姓名。若传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致。转账金额>=2,000元时,必须传入该值。该字段需要加密传入
                // 'user_name' => $this->encryptor($params['user_name']),
                // 必填 【转账金额】 转账金额,单位为“分”,金额小于0.3元的不支持传user_name(收款用户姓名)字段,转账金额>=2,000元时,必须传入该值。
                'transfer_amount' => $params['transfer_amount'],
                // 必填 【转账备注】 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
                'transfer_remark' => $params['transfer_remark'],
                // 【通知地址】 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数。
                // 'notify_url' => $params['notify_url'],
                // 【用户收款感知】 用户收款时感知的收款原因。不填或填空,将展示转账场景的默认内容。
                'user_recv_perception' => $params['user_recv_perception'],
                // 【转账场景报备信息】 需按转账场景准确填写报备信息
                /**
                 * [
                 * // 必填 【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照转账场景报备信息字段说明传参。
                 * 'info_type' => $params['transfer_scene_report_infos']['info_type'],
                 * // 必填 【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照转账场景报备信息字段说明传参。
                 * 'info_content' => $params['transfer_scene_report_infos']['info_content'],
                 * ]
                 */
                'transfer_scene_report_infos' => $params['transfer_scene_report_infos']
            ];

            // 转账金额>=2,000元时,user_name必须传入该值
            if (intval($params['transfer_amount']) >= 200000) {
                $body['user_name'] = $this->encryptor($params['user_name']);
            }

            if (!empty($params['notify_url'])) {
                $body['notify_url'] = $params['notify_url'];
            }

            $resp = $this->instance
                ->chain('/v3/fund-app/mch-transfer/transfer-bills')
                ->post([
                    'json' => $body,
                    'headers' => [
                        'Wechatpay-Serial' => $this->config['platform_cert_serial'],
                    ],
                ]);

            return json_decode((string)$resp->getBody(), true);
        } catch (Exception $e) {
            $errMsg = $e->getMessage();
            if (Env::get('app_debug') && $e instanceof RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $errMsg = $r->getStatusCode() . ' ' . $r->getReasonPhrase();
                $errMsg .= (string)$r->getBody();
                $errMsg .= $e->getTraceAsString();
            }

            throw new Exception($errMsg);
        }
    }


    /**
     * 商户单号查询转账单
     * @link https://pay.weixin.qq.com/doc/v3/merchant/4012716437
     * @param string $out_bill_no
     * @return array
     * @throws Exception
     */
    public function queryMchTransferBillByOutBillNo(string $out_bill_no): array
    {
        try {
            $resp = $this->instance
                ->chain('/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/' . $out_bill_no)
                ->get();

            return json_decode((string)$resp->getBody(), true);
        } catch (Exception $e) {
            $errMsg = $e->getMessage();
            if (Env::get('app_debug') && $e instanceof RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $errMsg = $r->getStatusCode() . ' ' . $r->getReasonPhrase();
                $errMsg .= (string)$r->getBody();
                $errMsg .= $e->getTraceAsString();
            }

            throw new Exception($errMsg);
        }
    }

    /**
     * 加密敏感数据(根据微信官方API文档说明来决定要加密的字段)
     * @param string $content
     * @return string
     */
    public function encryptor(string $content): string
    {
        $thing = file_get_contents($this->config['platform_cert_filepath']);
        $platformPublicKeyInstance = Rsa::from($thing, Rsa::KEY_TYPE_PUBLIC);
        return Rsa::encrypt($content, $platformPublicKeyInstance);
    }

    /**
     * 解密回调通知数据
     * @param array $headers
     * @param string $body
     * @return array
     * @throws Exception
     */
    public function decryptMchTransferNotifyData(array $headers, string $body): array
    {
        $inWechatpaySignature = $headers['wechatpay-signature'];
        $inWechatpayTimestamp = $headers['wechatpay-timestamp'];
        $inWechatpaySerial = $headers['wechatpay-serial'];
        $inWechatpayNonce = $headers['wechatpay-nonce'];

        $apiv3Key = $this->config['api_v3_key'];// 在商户平台上设置的APIv3密钥

        // 根据通知的平台证书序列号,查询本地平台证书文件,
        $platformCertificateFilePath = $this->config['platform_cert_filepath'];
        $platformPublicKeyInstance = Rsa::from(file_get_contents($platformCertificateFilePath), Rsa::KEY_TYPE_PUBLIC);

        // 检查通知时间偏移量,允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $timeOffsetStatus = true;

        // 验签
        $verifiedStatus = Rsa::verify(
        // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $body),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );

        if ($timeOffsetStatus && $verifiedStatus) {
            // 使用PHP7的数据解构语法,从Array中解构并赋值变量
            $inBodyArray = (array)json_decode($body, true);
            ['resource' => [
                'ciphertext' => $ciphertext,
                'nonce' => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            return (array)json_decode($inBodyResource, true);
        } else {
            throw new Exception('通知数据解析失败!');
        }
    }
}

配置文件

创建一个专门管理微信生态相关的配置文件,来统一管理配置信息。

当然,如果也可以把配置信息保存到数据库中,使用时借助缓存也可高效读取。

// file: config/wechat.php

<?php
// +----------------------------------------------------------------------
// | 微信支付设置
// +----------------------------------------------------------------------

return [
    // 公众号
    'mp' => [
        'appid' => '',
        'appsecret' => ''
    ],
    // 小程序
    'mini' => [
        'appid' => '',
        'appsecret' => ''
    ],
    // 微信支付
    'pay' => [
        // 商户号
        'mch_id' => '',

        // 商户API私钥文件绝对路径
        'mch_pri_key_filepath' => '',

        // 商户API证书的证书序列号
        'mch_api_cert_serial' => '7988C8B2DD95C7GHJDA24974FCEA1FF5B9C56F47C',

        // 微信支付平台证书的文件绝对路径
        'platform_cert_filepath' => 'upload/wechat/cert/wechatpay_1325FC2763804A95BF8D957AA781F2AB84F130AB.pem',

        // 微信支付平台证书的证书序列号
        'platform_cert_serial' => '1325FC2753504A95BF8D957AT671F2AB84F130AB',

        // 微信支付公钥文件绝对路径
        'platform_pub_key_filepath' => 'upload/wechat/cert/pub_key.pem',

        // 微信支付公钥ID
        'platform_pub_key_id' => 'PUB_KEY_ID_0117110272172025110336911591002205',

        // APIv3 Key
        'api_v3_key' => 'fdc79fb823e916dc4521b30f2a37260b',
    ],
];

上面配置文件中相关开发必要参数的配置及获取,请仔细阅读该文档:https://pay.weixin.qq.com/doc/v3/merchant/4013070756

封装业务 Service

Service 类会轻度涉及到业务场景,便于在业务层复用。

比如,下面的 Service 类中就增加记录API请求日志到具体项目中,而上面的工具类就是纯纯对接微信API,可以拿到 任何其他项目中使用。

Service 类中还提供两种获取配置信息的方法,一种是读取配置文件,一种是从数据库中获取。

// file: app/service/WechatPayService.php

<?php

namespace app\service;

use app\model\system\Config as SystemConfigModel;
use Exception;
use extend\WechatPayV3Lib;
use think\facade\Config;
use think\facade\Route;
use app\model\merchant\MerchantTransferApiLog as MerchantTransferApiLogModel;

class WechatPayService
{
    public int $site_id;

    public string $app_module;

    public array $config;

    /**
     * @var WechatPayV3Lib
     */
    public WechatPayV3Lib $handler;

    public function __construct(int $site_id = 1, string $app_module = 'shop')
    {
        $payDbConfig = $this->getPayConfigFromDb($site_id, $app_module);
        $payFileConfig = $this->getPayConfigFromFile();

        $config = [
            // 商户号
            'mch_id' => $payDbConfig['mch_id'],

            // 商户API私钥文件绝对路径
            'mch_pri_key_filepath' => realpath($payDbConfig['apiclient_key']),

            // 商户API证书的证书序列号
            'mch_api_cert_serial' => $payFileConfig['mch_api_cert_serial'],

            // 微信支付平台证书的文件绝对路径
            'platform_cert_filepath' => realpath($payFileConfig['platform_cert_filepath']),

            // 微信支付平台证书的证书序列号
            'platform_cert_serial' => $payFileConfig['platform_cert_serial'],

            // 微信支付公钥文件绝对路径
            'platform_pub_key_filepath' => realpath($payFileConfig['platform_pub_key_filepath']),

            // 微信支付公钥ID
            'platform_pub_key_id' => $payFileConfig['platform_pub_key_id'],

            // APIv3密钥
            'api_v3_key' => $payFileConfig['api_v3_key'],
        ];

        $this->handler = new WechatPayV3Lib($config);
        $this->site_id = $site_id;
        $this->app_module = $app_module;
        $this->config = $config;
    }

    /**
     * 商家转账 - 发起转账
     * @param array $params
     * @param array $log_data
     * @return void
     * @throws Exception
     */
    public function initiateMchTransfer(array $params, array $log_data = []): array
    {
        $miniDbConfig = $this->getMiniConfigFromDb($this->site_id, $this->app_module);

        $initiate_params = [
            'appid' => $miniDbConfig['appid'],
            'out_bill_no' => $params['out_trade_no'],
            'transfer_scene_id' => $params['transfer_scene_id'],
            'openid' => $params['openid'],
            'user_name' => $params['user_name'],
            'transfer_amount' => $params['transfer_amount'],
            'transfer_remark' => $params['transfer_remark'],
            'notify_url' => $params['notify_url'],
            'user_recv_perception' => $params['user_recv_perception'],
            'transfer_scene_report_infos' => $params['transfer_scene_report_infos'],
        ];
        $result = $this->handler->initiateMchTransfer($initiate_params);

        // 记录API请求日志
        (new MerchantTransferApiLogModel())->addRecord(array_merge(
            $log_data,
            [
                'log_type' => MerchantTransferApiLogModel::LOG_TYPE_API_REQUEST,
                'request_data' => json_encode([
                    'config' => $this->config,
                    'request_params' => $initiate_params
                ], JSON_UNESCAPED_UNICODE),
                'response_data' => json_encode($result, JSON_UNESCAPED_UNICODE)
            ]
        ));

        $result['mch_id'] = $this->config['mch_id'];
        return $result;
    }

    /**
     * 商户单号查询转账单
     * @param string $out_bill_no
     * @param array $log_data
     * @return array
     * @throws Exception
     */
    public function queryMchTransferBillByOutBillNo(string $out_bill_no, array $log_data = []): array
    {
        $result = $this->handler->queryMchTransferBillByOutBillNo($out_bill_no);

        // 记录API请求日志
        (new MerchantTransferApiLogModel())->addRecord(array_merge(
            $log_data,
            [
                'log_type' => MerchantTransferApiLogModel::LOG_TYPE_API_QUERY,
                'request_data' => json_encode([
                    'config' => $this->config,
                    'request_params' => compact('out_bill_no')
                ]),
                'response_data' => json_encode($result)
            ]
        ));
        return $result;
    }

    /**
     * 处理商家转账回调通知
     * @param array $params
     * @param array $log_data
     * @return array
     * @throws Exception
     */
    public function handleInitiateMchTransferNotify(array $params, array $log_data = []): array
    {
        debug_log('mch-transfer-notify.log', str_repeat('##', 10));
        debug_log('mch-transfer-notify.log', $params);

        // 记录API请求日志
        $merchant_transfer_api_log_model = new MerchantTransferApiLogModel();
        $log_id = $merchant_transfer_api_log_model->addRecord(array_merge(
            $log_data,
            [
                'site_id' => $this->site_id,
                'related_type' => MerchantTransferApiLogModel::RELATED_TYPE_MCH_MEM_WITHDRAW,
                'log_type' => MerchantTransferApiLogModel::LOG_TYPE_API_NOTIFY,
                'operator_identity' => MerchantTransferApiLogModel::OPERATOR_IDENTITY_SYSTEM,
                'request_data' => json_encode([
                    'config' => $this->config,
                    'request_params' => $params
                ]),
                'response_data' => json_encode([])
            ]
        ));

        $result = $this->handler->decryptMchTransferNotifyData($params['headers'], $params['body']);

        return [$result, $log_id];
    }

    /**
     * 查询获取数据库微信支付配置
     * @param int $site_id
     * @param string $app_module
     * @return array
     * @throws Exception
     */
    public function getPayConfigFromDb(int $site_id = 1, string $app_module = 'shop'): array
    {
        $condition = [
            ['site_id', '=', $site_id],
            ['app_module', '=', $app_module],
            ['config_key', '=', 'WECHAT_PAY_CONFIG']
        ];
        $res = (new SystemConfigModel())->getConfig($condition);
        if (empty($res['data']['value'])) {
            throw new Exception('数据库微信支付配置为空,请检查后重试!');
        }
        return $res['data']['value'];
    }

    /**
     * 获取配置文件中的微信支付配置
     * @return array
     * @throws Exception
     */
    public function getPayConfigFromFile(): array
    {
        $config = Config::get('wechat.pay');
        if (empty($config)) {
            throw new Exception('配置文件的微信支付配置为空,请检查后重试!');
        }
        return $config;
    }

    /**
     * 查询获取数据库微信小程序配置
     * @param int $site_id
     * @param string $app_module
     * @return array
     * @throws Exception
     */
    public function getMiniConfigFromDb(int $site_id = 1, string $app_module = 'shop'): array
    {
        $condition = [
            ['site_id', '=', $site_id],
            ['app_module', '=', $app_module],
            ['config_key', '=', 'WEAPP_CONFIG']
        ];
        $res = (new SystemConfigModel())->getConfig($condition);
        if (empty($res['data']['value'])) {
            throw new Exception('数据库微信小程序配置为空,请检查后重试!');
        }
        return $res['data']['value'];
    }

    /**
     * 获取配置文件中的微信小程序配置
     * @return array
     * @throws Exception
     */
    public function getMiniConfigFromFile(): array
    {
        $config = Config::get('wechat.mini');
        if (empty($config)) {
            throw new Exception('配置文件的微信小程序配置为空,请检查后重试!');
        }
        return $config;
    }
}

在API控制器中调用

控制器中直接调用 Service 即可,无需调用扩展工具类。

// file: app/api/controller/Merchant.php

<?php

namespace app\api\controller;

use app\api\controller\BaseApi;
use app\model\merchant\MerchantWithdraw as MerchantWithdrawModel;
use app\model\merchant\MerchantTransferApiLog as MerchantTransferApiLogModel;
use app\service\WechatPayService;
use think\Exception;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Route;
use Throwable;

class Merchant extends BaseApi
{

    /**
     * 余额提现(商家中心)
     */
    public function withdraw()
    {
        $token_check_res = $this->checkToken();
        if ($token_check_res['code'] < 0) {
            return $this->jsonResponse($this->error($token_check_res['message']));
        }

        Db::startTrans();
        try {
            if (empty($this->merchant_info)) {
                throw new Exception('无操作权限');
            }

            $withdraw_amount = input('amount/f', 0);
            $realname = input('realname/s');

            if ($withdraw_amount <= 0) {
                throw new \Exception('请输入提现金额');
            }

            if ($withdraw_amount > $this->merchant_info['balance']) {
                throw new \Exception('请输入提现金额不能大于可提现余额');
            }

            if (empty($realname)) {
                throw new \Exception('请输入真实姓名');
            }

            // 创建商户提现记录
            $merchant_withdraw_model = new MerchantWithdrawModel();
            $create_data = [
                'site_id' => $this->site_id,
                'merchant_id' => $this->merchant_id,
                'member_id' => $this->member_id,
                'withdraw_no' => $merchant_withdraw_model->createWithdrawNo(),
                'transfer_type' => MerchantWithdrawModel::TRANS_TYPE_WECHAT_WALLET,
                'realname' => $realname,
                'apply_amount' => $withdraw_amount,
                'status' => MerchantWithdrawModel::TRANS_STATUS_PENDING,
            ];
            $ins_id = $merchant_withdraw_model->addRecord($create_data);

            $params = array(
                'out_trade_no' => $create_data['withdraw_no'],
                'transfer_scene_id' => '1000',
                'openid' => $this->member_info['weapp_openid'],
                'user_name' => $realname,
                'transfer_amount' => intval($withdraw_amount * 100),
                'transfer_remark' => '商圈商户余额提现',
                'user_recv_perception' => '现金奖励',
                'transfer_scene_report_infos' => [
                    [
                        'info_type' => '活动名称',
                        'info_content' => '会员消费返现',
                    ],
                    [
                        'info_type' => '奖励说明',
                        'info_content' => '会员余额提现',
                    ]
                ],
                'notify_url' => (string)Route::buildUrl('pay/MchTransfer/notify')->domain(true) . '/biz/mchwithdraw'
            );
            $log_data = [
                'site_id' => $this->site_id,
                'merchant_id' => $this->merchant_id,
                'related_type' => MerchantTransferApiLogModel::RELATED_TYPE_MCH_MEM_WITHDRAW,
                'related_id' => $ins_id,
                'operator_identity' => MerchantTransferApiLogModel::OPERATOR_IDENTITY_MERCHANT_MEMBER,
                'operator_id' => $this->member_id,
                'api_desc' => '商户会员提现'
            ];
            //调用在线转账借口
            $result = (new WechatPayService($this->site_id))->initiateMchTransfer($params, $log_data);

            $response_data = $this->success($result);

            Db::commit();
        } catch (Throwable $e) {
            Db::rollback();
            $response_data = $this->error($e->getMessage() ?? '请求失败', $e->getCode());
        }

        return $this->jsonResponse($response_data);
    }

}

异步通知

官方商家转账回调通知开发文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012712115

什么时候发送异步通知

  • 只有商家转账单据到终态后(转账完成或者转账失败,对应单据状态status的值为SUCCESS、CANCELLED和FAIL),微信支付才会把单据的信息发送给商户,商户需要接收处理该消息,并返回应答。

如何正确的对回调通知应答

  • 验签通过:商户需告知微信支付接收回调成功,HTTP应答状态码需返回200或204,无需返回应答报文。
  • 验签不通过:商户需告知微信支付接收回调失败,HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,报文格式:{ "code": "FAIL", "message": "失败" }

注意事项

  • 由于商户接收到回调通知报文后,要求在5秒内完成对报文的验签,并应答回调通知。所以先处理应答后再异步处理后面的业务逻辑(如更新订单状态),避免因业务处理耗时过长导致对回调通知应答超时。

通知频率

  • 0s/15s(尝试10次)/300s(尝试10次)/1800s(尝试44次)
// file: app/pay/controller/MchTransfer.php

<?php

namespace app\pay\controller;

use app\Controller;
use app\service\WechatPayService;
use think\facade\Request;
use Throwable;
use Exception;
use think\facade\Db;
use app\model\merchant\MerchantWithdraw as MerchantWithdrawModel;
use app\model\merchant\MerchantTransferApiLog as MerchantTransferApiLogModel;

class MchTransfer extends Controller
{
    public function notify()
    {
        $params = [
            'headers' => Request::header(),
            // 注意:body 一定要通过 php://input 的方式来获取,使用其他方式获取会导致验签失败
            'body' => file_get_contents('php://input'),
        ];

        Db::startTrans();
        try {
            // 此处返回解析得到的异步通知参数的同时,还返回记录id请求日志,便于下面更新日志关联的业务数据
            [$result, $api_log_id] = (new WechatPayService())->handleInitiateMchTransferNotify($params);

            $withdraw_model = new MerchantWithdrawModel();

            // 获取业务模型数据
            $withdraw_row = $withdraw_model->getInfo([['withdraw_no', '=', $result['out_bill_no']]], '*');
            if (empty($withdraw_row)) {
                throw new Exception('out_bill_no 无效');
            }

            // 回填日志数据
            $log_fill_data = ['related_id' => $withdraw_row['id']];
            (new MerchantTransferApiLogModel())->update($log_fill_data, [['id', '=', $api_log_id]]);

            if ($result['state'] == 'SUCCESS') {
                $withdraw_row_updata = [
                    'actual_amount' => bcdiv($result['transfer_amount'], 100, 2), // 微信返回的实际转账金额
                    'status' => 'success',
                    'payment_time' => strtotime($result['update_time']), // 微信返回的转账成功时间
                    'transfer_bill_no' => $result['transfer_bill_no'], // 微信返回的转账交易单号
                ];
            } elseif ($result['state'] == 'FAIL') {
                $withdraw_row_updata = [
                    'status' => 'fail',
                    'fail_reason' => $result['fail_reason'],
                ];
            } else {
                throw new Exception('非单据终态,需要继续通知直到终态!');
            }

            // 更新转账记录状态
            $transfer_model->update($withdraw_row_updata, [['id', '=', $withdraw_row['id']]]);

            // todo: 其他业务代码

            Db::commit();
        } catch (Throwable $e) {
            Db::rollback();
            debug_log('mch-transfer-notify.log', $e);
            return json(['code' => 'FAIL', 'message' => $e->getMessage()], 509);
        }
    }
}

打印日志工具函数

开发调试使用。如果长期使用,建议进行日志切割,如根据创建年月日层级目录,否则会导致单个日志文件体积越来越大。

/**
 * 快捷记录日志(开发调试工具函数)
 * @param string $file 日志文件名称
 * @param $data
 */
function debug_log(string $file, $data)
{
    if (empty($file)) return;

    file_put_contents($file, date('Y-m-d H:i:s') . ' ' . (is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data) . PHP_EOL, 8);
}
声明:本文为原创文章,51blog.xyz和作者拥有版权,如需转载,请注明来源于51blog.xyz并保留原文链接:https://mp.51blog.xyz/article/98.html

文章归档

推荐文章

buildadmin logo
Thinkphp8 Vue3 Element PLus TypeScript Vite Pinia

🔥BuildAdmin是一个永久免费开源,无需授权即可商业使用,且使用了流行技术栈快速创建商业级后台管理系统。

热门标签

PHP ThinkPHP ThinkPHP5.1 Go Mysql Mysql5.7 Redis Linux CentOS7 Git HTML CSS CSS3 Javascript JQuery Vue LayUI VMware Uniapp 微信小程序 docker wiki Confluence7 学习笔记 uView ES6 Ant Design Pro of Vue React ThinkPHP6.0 chrome 扩展 翻译工具 Nuxt SSR 服务端渲染 scrollreveal.js ThinkPHP8.0 Mac webman 跨域CORS vscode GitHub ECharts Canvas vue3 three.js 微信支付 PHP全栈开发