TP5 实现微信APP支付(详细步骤)
2020-07-09 本文已影响0人
华仔233
1、微信支付
1.1、安装依赖包
- 我这里使用 EasyWeChat 实现微信支付,另外我附上之前我封装的微信类。
- 详细操作里面写的比较清楚,请先看这个:https://www.jianshu.com/p/d376d921bf16
1.2、准备工作
(1)需要去微信开放平台注册账号
- appid(微信开放平台上的应用id)
- mch_id(微信申请成功之后邮件中的商户id)
- notify_url(支付成功后的回调地址)
- api_key(在微信商户平台上自己设定的api密钥 32位)
- apiclient_cert.pem 和 apiclient_key.pem 证书
相关的申请流程可以参考网站: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Pay/Vendor_Service_Center.html
(2)将对应的配置写入配置文件中(配置文件可以参考我下面封装的微信类)
(3)创建订单表
-
关于订单表,这里给你们参考下我的
1.3、封装 WechatServer.php
微信服务类
<?php
namespace app\common\service;
use EasyWeChat\Factory;
use EasyWeChat\MiniProgram\Application;
use think\Hook;
/**
* 微信服务
* @package app\common\service
*/
class WeChatService
{
//微信公众号配置
private $officeConfig = [
'app_id' => 'asdasdasf23413', //测试号
'secret' => 'asdasdasr3421412412312312', //测试号
// 'app_id' => '515125141',
// 'secret' => '123131212asdasdsadasd',
// 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名
'response_type' => 'array',
//...
];
//微信支付
private $payConfig = [
// 必要配置
'app_id' => 'asdasdasf23413',
'secret' => 'asdasdasr3421412412312312', //
'mch_id' => '515125141',
'key' => 'asdasdasdas', // API 密钥
// 如需使用敏感接口(如退款、发送红包等)需要配置 API 证书路径(登录商户平台下载 API 证书)
'cert_path' => PUBLIC_PATH . '/wechatCert/apiclient_cert.pem', // XXX: 绝对路径!!!!
'key_path' => PUBLIC_PATH . '/wechatCert/apiclient_key.pem', // XXX: 绝对路径!!!!
'notify_url' => '', // 你也可以在下单时单独设置来想覆盖它
'log' => [
'level' => 'debug',
'file' => PUBLIC_PATH . '/logs/pay_wechat.log',
],
];
//微信小程序配置
private $miniConfig = [
'app_id' => 'qwad312321312312',
'secret' => 'asdasdasdasdasdasdweqe',
// 下面为可选项
// 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名
'response_type' => 'array',
'log' => [
'level' => 'debug',
'file' => PUBLIC_PATH . '/logs/mini_wechat.log',
],
];
//开放平台配置
private $openConfig = [
'app_id' => '开放平台第三方平台 APPID',
'secret' => '开放平台第三方平台 Secret',
'token' => '开放平台第三方平台 Token',
'aes_key' => '开放平台第三方平台 AES Key'
];
private $token = ''; //获取小程序的ACCESS_TOKEN
private $isContract = false; //是否开启支付中签约
/**
* @ApiTitle (实例化)
*/
public function connect($type = 0)
{
//实例化对象
if ($type == 0) $app = Factory::miniProgram($this->miniConfig); //微信小程序
if ($type == 1) $app = Factory::officialAccount($this->officeConfig); //微信公众号
if ($type == 3) $app = Factory::openPlatform($this->openConfig); //微信开放平台
if ($type == 10) $app = Factory::payment($this->payConfig); //微信支付
//获取token
$accessToken = $app->access_token;
$this->token = $accessToken->getToken()['access_token']; // token 数组 token['access_token'] 字符串
return $app ?? false;
}
/**
* 统一下单
* @param int $type 支付类型(0-微信小程序,1-微信公众号,2-微信开放平台,10-微信支付)
* @param int $tradeType 支付方式
* @param string $body 描述
* @param string $oderNumber 订单号
* @param float $amount 金额
*/
public function unify($type, $tradeType, $body, $oderNumber, $amount)
{
$app = (new WeChatService())->connect($type);
$result = $app->order->unify([
'body' => $body,
'out_trade_no' => $oderNumber,
'total_fee' => $amount * 100, // **单位:分**
'notify_url' => '', // 支付结果通知网址,如果不设置则会使用配置里的默认地址
'trade_type' => $tradeType, // 请对应换成你的支付方式对应的值类型
// 'spbill_create_ip' => '', // 可选,如不传该参数,SDK 将会自动获取相应 IP 地址
// 'openid' => $openId, //从用户额外表中取到openId
], $this->isContract);
//第二次验签
if ($result['return_code'] == 'SUCCESS') {
$result = $app->jssdk->appConfig($result['prepay_id']); //第二次签名
return $result;
} else {
return false;
}
}
}
1.4、第一步:创建订单
/**
* @ApiTitle (用户开通VIP创建不同的订单)
* @ApiMethod (POST)
*/
public function createOrder()
{
//根据传入内容或者查库获取相关的数据
//$amount 需要内部计算,不能传入
//$userId 从Token中获取用户ID
//$orderNumber 需要生成随机不重复的字符串,用于商户内部订单号
//生成资金流水记录
Db::startTrans();
try {
// 具体业务数据表插入以及操作
//将数据插入资金表
(new UserAccountModel)->insert([
'from_id' => $userId, //支付方ID(系统默认为1)
'to_id' => 1, //收款方ID(系统默认为1)
'type' => 1, //资金类型:1=VIP开通/升级,2=推荐提成,3=退款
'money' => $amount, //资金金额
'desc' => $subject, //相关描述
'pay_status' => 0, //支付状态:0=未到账,1=已到账
'order_number' => $orderNumber, //订单流水号
'create_time' => date('Y-m-d H:i:s', time()), //创建时间
]);
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
$this->success('创建订单成功!', ['order_number' => $orderNumber,]);
}
-
$amount
表示支付金额,需要根据具体业务计算,不能取 input 传入的值。 -
pay_status
用于判断订单是否支付完成,这一步主要通过订单查询/异步调用时候成功的情况下才会变为1 - 另外订单表中需要有一个
is_deal
字段,用于判断 是否处理(0=否,1=是)支付成功的操作 ,因为 异步调用 有时候会出现问题,我们也需要在 调用支付宝查询订单 的时候通过这个字段去判断是否处理从而去 更新订单表。
1.5、第二步:获取调起微信支付的参数
/**
* @ApiTitle (用户支付订单)
* @ApiMethod (POST)
*/
public function payAmount()
{
$payType = intval(input('pay_type')) ?? 0; //支付类型 10-APP微信支付 20-APP支付宝
$orderNumber = input('order_number') ?? 0; //内部订单号
if (!$orderNumber) $this->error('订单号不得为空!');
//相关的业务类型判断
// ....
// 计算相关的资金价格 $amount
//操作备注
$payTypeText = '';
$accountType = 0;
if ($payType == 10 || $payType == 11) {
$payTypeText = '微信支付';
$accountType = 1;
}
if ($payType == 20 || $payType == 21) {
$payTypeText = '支付宝';
$accountType = 2;
}
if ($vipType == 1) $subject = $payTypeText . '方式开通' . $newVip['title'] . ',充值金额:' . $amount . '元';
if ($vipType == 2) $subject = $payTypeText . '方式续费' . $newVip['title'] . ',充值金额:' . $amount . '元';
if ($vipType == 3) $subject = $payTypeText . '方式升级' . $newVip['title'] . ',充值金额:' . $amount . '元';
//更新资金表
(new UserAccountModel)->where('order_number', $orderNumber)->update([
'desc' => $subject, //相关描述
'pay_type' => $accountType, //支付方式:1=微信支付,2=支付宝,3=银行卡,11=其他
]);
if ($payType == 10) { //APP微信支付
$result = (new WeChatService())->unify(10, 'APP', $subject, $orderNumber, $amount);
$this->success('调起微信支付成功', $result); //支付成功
} elseif ($payType == 20) { //支付宝支付
//订单内容
$order = [
'out_trade_no' => $orderNumber,
'total_amount' => $amount,
'subject' => $subject,
];
$alipay = Pay::alipay(config('alipay'))->app($order); //app支付
$res = $alipay->getContent();
$this->success('调起支付宝支付成功', $res);
}
}
- 后端 只需要在调用微信支付地方调用这段方法即可。
- H5 支付,公众号支付,扫码支付,支付中签约,全部都是用这个接口下单。
- 前端 微信支付参考文档 https://pay.weixin.qq.com/wiki/doc/api/index.html。
- 其中
$res
是返回 给前端调起微信 的参数,如下:
{
"code": 1,
"msg": "成功",
"time": "1593600229",
"data": {
"appid": "wx23123124124",
"partnerid": "123123123123",
"prepayid": "wx01184350746102c94fbadf8d1379806200",
"noncestr": "5efc68e65d469",
"timestamp": 123123123,
"package": "Sign=WXPay",
"sign": "05FC29CD1CEEAFSDA34341251B1C77"
}
}
1.6、查询订单
/**
* @ApiTitle (获取订单支付状态)
* @ApiMethod (GET)
*/
public function getPayStatus()
{
$orderNumber = input('order_number'); //内部订单流水号
$userAccount = (new UserAccountModel)->where('order_number', $orderNumber)->find();
if (!$userAccount) $this->error('不存在该订单!');
//未处理:0=否,1=是(用于处理业务逻辑)
if ($userAccount->is_deal == 0) {
//微信异步调用异常情况下:
//支付成功根据支付方式:1=微信支付,2=支付宝,3=银行卡,11=其他
if ($userAccount->pay_type == 1) {
//微信支付查看订单
$app = (new WeChatService)->connect(10);
$res = $app->order->queryByOutTradeNumber($orderNumber);
if ($res['return_code'] === 'SUCCESS') { // return_code 表示通信状态,不代表支付状态
if ($res['result_code'] === 'SUCCESS') { //以下字段在return_code为SUCCESS的时候有返回
$tradeNo = $res['transaction_id']; //微信支付订单号
$totalFee = $res['total_fee']; //充值总金额
$timeEnd = $res['time_end']; //支付完成时间
//如果金额不匹配直接退出
if (($userAccount->money) != $totalFee / 100) goto S;
//支付成功
$this->paySuccess($orderNumber, $tradeNo, $timeEnd);
}
}
} elseif ($userAccount->pay_type == 2) {
//支付宝查看订单
$res = Pay::alipay(config('alipay'))->find($orderNumber);
$state = $res->trade_status; //订单状态
$outTradeNo = $res->out_trade_no; //自定义订单号
$tradeNo = $res->trade_no; //支付宝订单号
$totalAmount = $res->total_amount; //充值总金额
$appId = $res->app_id; //收款方的APPID
$payTime = $res->gmt_payment; //交易付款时间
if (!in_array($state, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) goto S;
if (!$userAccount) goto S;
if ($userAccount['money'] != $totalAmount) goto S;
if ($appId != config('alipay.app_id')) goto S;
//支付成功
$this->paySuccess($outTradeNo, $tradeNo, $payTime);
}
}
S:
//需要再查一次订单状态
$userAccount = (new UserAccountModel)->where('order_number', $orderNumber)->find();
//返回数据前端
$data = [
'order_number' => $orderNumber,
'vip_title' => $vip['title'],
'pay_status' => $userAccount['pay_status'], //支付状态:0=待支付,1=支付成功
'pay_type' => $userAccount['pay_type'], //支付方式:1=微信支付,2=支付宝,3=银行卡,11=其他
];
$this->success('获取订单信息成功!', $data);
}
- 查询订单时候用到
is_deal
用于判断异步接口是否调用处理,没有则调用一次
1.7、微信异步操作
/**
* @ApiTitle (微信异步支付会员VIP)
* @ApiMethod (POST)
*/
public function wechatNotify()
{
$app = (new WeChatService())->connect(10);
$response = $app->handlePaidNotify(function ($message, $fail) {
$outTradeNo = $message['out_trade_no']; //自定义订单号
//查询是否存在订单
$res = (new UserAccountModel)->where('order_number', $outTradeNo)->find();
// 如果订单不存在 或者 订单已经支付过了 或者 已经处理过
if (!$res || $res->pay_time || $res->pay_status == 1 || $res->is_deal == 1) return true;
if ($message['return_code'] === 'SUCCESS') { // return_code 表示通信状态,不代表支付状态
if ($message['result_code'] === 'SUCCESS') { //以下字段在return_code为SUCCESS的时候有返回
if ($message['trade_state'] === 'SUCCESS') { //以下字段在return_code 和result_code都为SUCCESS的时候有返回
$tradeNo = $message['transaction_id']; //微信支付订单号
$totalFee = $message['total_fee']; //充值总金额
$timeEnd = $message['time_end']; //支付完成时间
//如果金额不匹配直接退出
if (($res->money) != $totalFee / 100) return true;
//支付成功
$this->paySuccess($outTradeNo, $tradeNo, $timeEnd);
}
}
} else {
return $fail('通信失败,请稍后再通知我');
}
return true;
});
$response->send(); // Laravel 里请使用:return $response;
}
- 其中必须
trade_state
是SUCCESS
的时候才算支付成功 - 其中
$this->paySuccess($outTradeNo, $tradeNo, $payTime);
用于 支付成功调用的接口 - 该接口主要根据(订单号查询到的)订单业务类型 去调不同的方法,同时需要加入 并发锁
1.8、支付成功的方法
/**
* @ApiTitle (支付成功的操作,需要锁)
* @ApiInternal
* @param string $outTradeNo 商户内部订单号
* @param string $tradeNo 微信/支付宝订单号
* @param string $payTime 支付時間
* @return bool|string
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
private function paySuccess($outTradeNo, $tradeNo, $payTime)
{
//加锁失败!
if (!RedisService::lock('paySuccess_' . $outTradeNo)) return false;
//查询订单
$res = (new UserAccountModel)->where('order_number', $outTradeNo)->find();
if (!$res) return false;
Db::startTrans();
try {
//更新资金表状态
(new UserAccountModel)->where('order_number', $outTradeNo)->update([
'trade_no' => $tradeNo, //微信订单号
'pay_time' => $payTime, //支付时间
'pay_status' => 1, //支付状态:0=未到账,1=已到账
]);
//查看订单类型 1=VIP开通/升级,2=推荐提成,3=退款
if ($res->type == 1 && $res->is_deal == 0) {
$res = $this->vipSuccess($res['from_id'], $outTradeNo);
if (!$res) throw new Exception('订单状态处理异常');
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
//记录资金日志
Log::warning($e->error());
return false;
}
return true;
}
-
$this->vipSuccess($res['from_id'], $outTradeNo);
是充值会员成功的方法
1.9、Redis锁方法
- composer安装
predis
,命令行:composer require predis/predis
,点击查看链接。 - 新建一个
RedisServer.php
服务类
<?php
namespace app\common\service;
use app\common\controller\Api;
use Predis\Client;
class RedisService extends Api
{
//Redis并发锁
const SU_REDIS_LOCK = 'redis::lock::'; //Redis并发锁(后面跟对应业务的锁名)
private static $prefix = '';
private static $client;
/**
* 单例模式获取redis连接实例
* @return Client
*/
public static function connect()
{
if (!self::$client) {
self::$prefix = config('redis_prefix');
$config = [
'scheme' => 'tcp',
'host' => config('redis_host'),
'port' => config('redis_port'),
'timeout' => 60,
'read_write_timeout ' => 60,
];
//没有配置密码时,不传入密码项参数
if (config('redis_password')) $config['password'] = config('redis_password');
self::$client = new Client($config, ['prefix' => self::$prefix]);
}
return self::$client;
}
/**
* 添加自定义并发锁
* 原理是redis的单线程操作
* @param string $lockName 锁名
* @param int $expireTTL 过期时间
* @return bool 是否由当前调用加锁成功
*/
public static function lock(string $lockName, int $expireTTL = 10)
{
$redis = self::connect();
$countKey = self::SU_REDIS_LOCK . $lockName;
$flag = false; //默认是加锁失败
$redisIncr = $redis->incr($countKey); //只有第一个操作的返回是1
if ($redisIncr === 1) {
$redis->expire($countKey, $expireTTL);
$flag = true; //只有第一次操作的才算加锁成功
}
return $flag;
}
/**
* 解除自定义并发锁
* @param string $lockName 锁名
* @return bool 是否成功
*/
public static function unlock(string $lockName)
{
$countKey = self::SU_REDIS_LOCK . $lockName;
return (bool)self::connect()->del([$countKey]);
}
}
微信调用的时候需要配置白名单,这样才可以调用微信内部的方法,例如查询订单的接口。
大功告成,其中调试也会遇到问题,我们可以在日志中进行查看,日志在配置中可以进行修改。
欢迎来指导和学习,如果有什么问题可以在留言区留言并一起探讨。
另外还有详细的 支付宝APP支付过程