1、前期準備工作
1.1、申請支付寶支付
我們需要注意配置的以下幾點:
1.2、安裝依賴
composer 命令安裝:
composer require yansongda/pay
- 我這裏安裝的是 yansongda/pay 擴展包,包括詳細的相關操作和使用方法。
- 該依賴包支持以下 :
支付寶支付(電腦支付、手機網站支付、APP 支付、刷卡支付、掃碼支付、賬戶轉賬、小程序支付)
微信支付(公衆號支付、小程序支付、H5 支付、刷卡支付、掃碼支付、APP 支付、企業付款、普通紅包、分裂紅包)
2、代碼實現
2.1、配置文件 config.php
//支付寶支付設置
'alipay' => [
'app_id' => '2021001232323232',
'notify_url' => '線上的支付寶異步跳轉地址',
'ali_public_key' => '應用公鑰',
'private_key' => '生成的應用祕鑰',
// 使用公鑰證書模式,請配置下面兩個參數,同時修改ali_public_key爲以.crt結尾的支付寶公鑰證書路徑,如(./cert/alipayCertPublicKey_RSA2.crt)
// 'app_cert_public_key' => './cert/appCertPublicKey.crt', //應用公鑰證書路徑
// 'alipay_root_cert' => './cert/alipayRootCert.crt', //支付寶根證書路徑
'log' => [ // optional
'file' => './logs/alipay.log',
'level' => 'info', // 建議生產環境等級調整爲 info,開發環境爲 debug
// 'type' => 'daily', // optional, 可選 daily.
'max_file' => 30, // optional, 當 type 爲 daily 時有效,默認 30 天
],
'sign_type' => "RSA2",
'http' => [ // optional
'timeout' => 5.0,
'connect_timeout' => 5.0,
// 更多配置項請參考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
],
// 'mode' => 'dev', // optional,設置此參數,將進入沙箱模式
],
2.2、業務層代碼
2.2.1、根據業務創建訂單
/**
* @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=是)支付成功的操作 ,因爲 異步調用 有時候會出現問題,我們也需要在 調用支付寶查詢訂單 的時候通過這個字段去判斷是否處理從而去 更新訂單表。
2.2.2、用戶統一的支付訂單
/**
* @ApiTitle (用戶支付訂單)
* @ApiMethod (POST)
*/
public function payAmount()
{
//支付類型 10-APP微信支付 11-微信小程序支付 12-H5調起微信支付 13-生成微信支付二維碼 20-APP支付寶支付 21-PC支付寶支付 22-H5支付寶支付
$payType = intval(input('pay_type')) ?? 0;
$orderNumber = input('order_number') ?? 0; //內部訂單號
if (!$orderNumber) $this->error('訂單號不得爲空!');
//相關的業務類型判斷
// ....
// 計算相關的資金價格 $amount
//操作備註
$payTypeText = '';
$accountType = 0;
if ($payType == 10 || $payType == 11 || $payType == 12 || $payType == 13) {
$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 ($accountType == 1) { //10-APP微信支付 11-微信小程序支付 12-H5調起微信支付 13-生成微信支付二維碼
$result = (new WeChatService())->unify($payType, $subject, $orderNumber, $amount);
if(!$result) $this->error('調起微信支付失敗!');
$result['order_number'] = $orderNumber;
$result['amount'] = $amount;
$this->success('調起微信支付成功', $result); //支付成功
} elseif ($accountType == 2) {
//20-APP支付寶支付 21-PC支付寶支付 22-H5支付寶支付
//訂單內容
$order = [
'out_trade_no' => $orderNumber,
'total_amount' => $amount,
'subject' => $subject,
];
$ailpayConfig = config('alipay');
if ($payType == 20) $alipay = Pay::alipay($ailpayConfig)->app($order); //app支付
if ($payType == 21) {
//PC支付
$ailpayConfig['return_url'] = '支付成功界面url';
$alipay = Pay::alipay($ailpayConfig)->web($order);
}
if ($payType == 22) {
//手機網站支付
$ailpayConfig['return_url'] = '支付成功界面url?order_num=' . $orderNumber . '&money=' . $amount;
$alipay = Pay::alipay($ailpayConfig)->wap($order);
}
$res = $alipay->getContent();
$this->success('調起支付寶支付成功', $res);
}
}
PS:PC支付和手機H5支付的時候需要填寫 支付成功界面的url
- 其中
$res
是返回給前端調起支付寶的參數,如下:
app_id=202100116232323232&format=JSON&charset=utf-8&sign_type=RSA2&version=1.0¬ify_url=http%3A%2F%2Ftest.sulinks.com%2Fapi%2FPayment%2FalipayNotify×tamp=2020-07-09+16%3A20%3A50&biz_content=%7B%22out_trade_no%22%3A%22A15938403231190%22%2C%22total_amount%22%3A%220.01%22%2C%22subject%22%3A%22%5Cu652f%5Cu4ed8%5Cu5b9d%5Cu65b9%5Cu5f0f%5Cu7eed%5Cu8d39%5Cu94c2%5Cu91d1VIP%5Cuff0c%5Cu5145%5Cu503c%5Cu91d1%5Cu989d%5Cuff1a0.01%5Cu5143%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&method=alipay.trade.app.pay&sign=bvIwgGf%2FByYOjhNX%2B%2B0JmlPBwOwK%2BguZekrB1JZ6PJ61srGauandLwnDlj01u%2FyFo%2Fn5PNHyao%2FdDOCQCE5UxObqe03gw5PYv3oFFy42NEzTqD8J6cX91IMfSnxptQmN746lSqSmETyEHOR7LUNP%2BSajq58oOlF5Awke5XagBb5aW55R%2Ft5KwAOUiv%2FUCk6C2cEPUS2%2FfJAf8RdjkkYCKoaDCCcFwRoPhlW2YjuUu6IeMtdWtTGYTifoIUpf8WNliJA0j6HE6iB7%2BVLs0iskjiBc0hAP6i06i3H5DNz7%2FY8cvBxln573DXKdnLABzGu4TFb3UQSDl0JTXfmyhTgcHA%3D%3D
2.2.3、查詢訂單
/**
* @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的時候有返回
if ($res['trade_state'] === '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);
} else {
goto S;
}
}
}
} 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
用於判斷異步接口是否調用處理,沒有則調用一次
2.2.4、支付寶異步操作
/**
* @ApiTitle (支付寶異步接口)
* @ApiMethod (POST)
* @ApiRoute (/api/Payment/alipayNotify)
* @ApiInternal
*/
public function alipayNotify()
{
$alipay = Pay::alipay(config('alipay'));
$data = $alipay->verify(); // 是的,驗籤就這麼簡單!
$state = $data->trade_status; //訂單狀態
$outTradeNo = $data->out_trade_no; //自定義訂單號
$tradeNo = $data->trade_no; //支付寶訂單號
$totalAmount = $data->total_amount; //充值總金額
$appId = $data->app_id; //收款方的APPID
$payTime = $data->gmt_payment; //交易付款時間
//獲取對應訂單的資金流水信息
$res = (new UserAccountModel)->where('order_number', $outTradeNo)->find();
// 請自行對 trade_status 進行判斷及其它邏輯進行判斷,在支付寶的業務通知中,只有交易通知狀態爲 TRADE_SUCCESS 或 TRADE_FINISHED 時,支付寶纔會認定爲買家付款成功。
// 1、商戶需要驗證該通知數據中的out_trade_no是否爲商戶系統中創建的訂單號;
// 2、判斷total_amount是否確實爲該訂單的實際金額(即商戶訂單創建時的金額);
// 3、校驗通知中的seller_id(或者seller_email) 是否爲out_trade_no這筆單據的對應的操作方(有的時候,一個商戶可能有多個seller_id/seller_email);
// 4、驗證app_id是否爲該商戶本身。
// 5、其它業務邏輯情況。
if (!in_array($state, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) return $alipay->success()->send();
if (!$res) return $alipay->success()->send();
if ($res['money'] != $totalAmount) return $alipay->success()->send();
if ($appId != config('alipay.app_id')) return $alipay->success()->send();
//支付成功
$this->paySuccess($outTradeNo, $tradeNo, $payTime);
Log::debug('Alipay notify', $data->all());
return $alipay->success()->send();// laravel 框架中請直接 `return $alipay->success()`
}
- 其中
$this->paySuccess($outTradeNo, $tradeNo, $payTime);
用於 支付成功調用的接口 - 該接口主要根據(訂單號查詢到的)訂單業務類型 去調不同的方法,同時需要加入 併發鎖
2.2.5、支付成功的方法
/**
* @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);
是充值會員成功的方法
2.2.6、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]);
}
}
大功告成,其中調試也會遇到問題,我們可以在日誌中進行查看,日誌在配置中可以進行修改。
如果有什麼問題可以留言,歡迎互相交流共進步。
另外還有詳細的 TP5 實現APP/二維碼/小程序/H5等微信支付(詳細步驟)