Laravel uniapp多端发步小程序支付后端设计
环境
laravel5.7
微信支付2.0(即md5加密方式)
百度小程序收银台
场景
uniapp 多端发步的小程序,后端兼容
概述
本篇从表设计,到下单支付,支付回调完成各小程序的支付。
代码设计尤其支付回调返回支付结果那部分不是很满意,及文件偏零散,因开发是逐渐进行的,很多是线上更新的,因此无法做大的改动,仅当抛砖引玉。
备注
该系统用户注册时皆有一个 platform
字段,http
请求的 header
中皆携带 platform
头,以区分平台,详情参考系列文章 uniapp 小程序 Laravel+jwt 权限认证完整系列
。
同理,支付表设计也需有 platform
字段来区分哪个平台支付的订单。
支付表设计
CREATE TABLE `test_pays` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付表',
`user_id` int(10) unsigned NOT NULL,
`platform` varchar(15) NOT NULL COMMENT 'MP-BAIDU\\MP-WEIXIN',
`channel` varchar(50) NOT NULL COMMENT '支付场景:如 award 红包',
`body` varchar(10) NOT NULL COMMENT '支付描述',
`order_no` varchar(32) NOT NULL COMMENT '商户订单号',
`prepay_id` varchar(255) DEFAULT NULL COMMENT '微信支付预支付id,有效期2小时',
`expire_at` timestamp DEFAULT NULL COMMENT '微信小程序2小时后该订单无效需重新下单',
`transaction_id` varchar(32) DEFAULT NULL COMMENT '各支付平台交易ID',
`pay_amount` int(11) unsigned NOT NULL COMMENT '总价,单位分,价格的100倍',
`pay_status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '0-未支付,1-已支付',
`created_at` timestamp NULL DEFAULT NULL COMMENT '订单创立时间',
`notify_at` timestamp NULL DEFAULT NULL COMMENT '回调时间',
`verifyed` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '调用查询接口校验',
`is_del` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否有效',
PRIMARY KEY (`id`),
KEY `order_no` (`order_no`) USING BTREE,
KEY `user_id` (`user_id`) USING BTREE,
KEY `status_verify` (`pay_status`,`verifyed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='支付表';
下单
以红包为例,要点:平台 platform、支付频道 channel
$pay_data = [
'user_id' => $user->u_id,
'platform' => $user->u_platform, // MP-BAIDU OR MP-WEIXIN
'channel' => Pay::AWARD,// 'award'
'award_id' => $award->id,
'body' => '红包',
'order_no' => build_order_no(),
'pay_amount'=> 100 * $post['price'],
'created_at'=> date('Y-m-d H:i:s',time()),
];
$pay = Pay::create($pay_data);
发起支付
PayController
// 小程序支付 http 接口 GET/POST v1/pay/buildorderinfo/{payid}
public function buildOrderInfo($pay_id)
{
$pay = Pay::findOrFail($pay_id);
if( $pay->pay_status == 1 ){ return Y::json(1001,'您已经支付,请不要重复支付。'); }
if ($pay->expire_at && strtotime($pay->expire_at) < time()) {
return Y::json(1002,'订单已超时,请重新下单');
}
$platform = $pay->platform; // 分发
switch ($platform) {
case 'MP-BAIDU':
$data = $this->baiduPay($pay);
break;
case 'MP-WEIXIN':
$data = $this->weixinPay($pay);
break;
default:
$data = false;
break;
}
if($data === false){
return Y::json(1002,'发生错误,无法支付,请联系客服反馈。');
}
return Y::success('success',$data);
}
// 百度
protected function baiduPay($order)
{
// 组装 order
$orderInfo['dealId'] = config('payment.baidu.deal_id');//百度收银台的财务结算凭证
$orderInfo['appKey'] = config('payment.baidu.app_key');//支付能力开通后分配的支付appKey
$orderInfo['tpOrderId'] = $order->order_no;//商户订单ID
$orderInfo['totalAmount'] = $order->pay_amount;//订单金额,单位为人民币分
$sign = (new BaiduSign() )->genSignWithRsa($orderInfo);//appKey+dealId+totalAmount+tpOrderId进行RSA加密后的签名
if(!$sign){
\Log::info('pay-baiduPay:sign not get!');
return false;
}
$orderInfo['dealTitle'] = $order['body'];//订单的名称
$orderInfo['signFieldsRange'] = 1;//固定值为1
$orderInfo['rsaSign'] = $sign;
return ['orderInfo'=>$orderInfo];
}
// 微信
protected function weixinPay($order) {
$prepay_id = $order->prepay_id;
// 无prepay_id 代表用户第一次发起支付,有的话代表发起支付但没有支付,过一段时间重新发起支付
if (!$prepay_id) {
if ($order->expire_at && strtotime($order->expire_at) < time()) {
return false;
}
$data['body'] = $order->body;
$data['out_trade_no'] = $order->order_no;
$data['total_fee'] = $order->pay_amount;
$data['openid'] = $order->user->u_openid;
// 统一下单获取 prepay_id
$prepay_id = (new \App\Library\Order\WxOrder())->unifiedorder($data);
if( !$prepay_id ) {return false;}
$order->prepay_id = $prepay_id;
$order->expire_at = date('Y-m-d H:i:s', time() + 2*3600);
$order->save();
}
$signObj = new \App\Library\Pay\WxSign();
$orderInfo['appId'] = config('payment.wx.appid');
$orderInfo['timeStamp'] = time();
$orderInfo['signType'] = 'MD5';
$orderInfo['package'] = 'prepay_id=' . $prepay_id;
$orderInfo['nonceStr'] = $signObj->getNonceStr();
$orderInfo['paySign'] = $signObj->getSignWithMd5($orderInfo);
return ['orderInfo'=>$orderInfo];
}
涉及的类
涉及篇幅,且这些类不涉及主要流程,在此略过。
// 构建支付签名、验签类
App\Library\Pay\BaiduSign.php
App\Library\Pay\WxSign.php
// 订单api,与支付平台交互,下单、查询等
App\Library\Order\BdOrder.php
App\Library\Order\WxOrder.php
// 微信2.0支付由于是xml格式交互,工具trait提供xml与array的转换
App\Library\UtilTrait\WxUtil.php
支付回调
回调核心类
// 回调五步走的流程抽象类
app\Library\Notify\NotifyAbstract.php
// 商户逻辑契约类
app\Library\Notify\PayNotifyInterface.php
各平台实现类 extends NotifyAbstract
app\Library\Notify\BdNotify.php
app\Library\Notify\WxNotify.php
商户业务逻辑类
app\Service\PaymentNotify.php
验签类
App\Library\Pay\BaiduSign.php
App\Library\Pay\WxSign.php
核心类1. 商户业务逻辑契约类
interface
<?php
namespace App\Library\Notify;
interface PayNotifyInterface
{
/**
* 异步回调检验完成后,回调客户端的业务逻辑
* 业务逻辑处理,必须实现该类。
*
* @param array $data 经过处理后返回的统一格式的回调数据
* @return boolean or errorMsg
*/
public function notifyProcess(array $data);
}
核心类2. 通用回调处理类
abstract
<?php
namespace App\Library\Notify;
use App\Library\Notify\PayNotifyInterface;
/**
* 各小程序支付平台的不同实现 需要继承该类
* 获取异步通知的原始数据-》验签-》执行商户逻辑-》返回应答
*/
abstract class NotifyAbstract
{
/**
* 入口
* @param PayNotifyInterface $notify
* final 不能被重写
*/
final public function handle(PayNotifyInterface $notify)
{
// 获取异步通知的原始数据
$notifyData = $this->getNotifyData();
if ($notifyData === false) {
return $this->replyNotify('获取通知数据失败');
}
// 验签
$checkRet = $this->checkNotifyData($notifyData);
if ($checkRet === false) {
return $this->replyNotify(false, '返回数据验签失败,可能数据被篡改');
}
// 回调商户的业务逻辑
$flag = $this->callback($notify, $notifyData);
// 应答
return $this->replyNotify($flag);
}
/**
* 回调商户的业务逻辑,根据返回的 true 或者 false
* @param PayNotifyInterface $notify
* @param array $notifyData 原始回调数据
*
* @return boolean
*/
protected function callback(PayNotifyInterface $notify, array $notifyData)
{
$data = $this->getRetData($notifyData); // 提取必要数据返回商户
if ($data === false) {
return false;
}
return $notify->notifyProcess($data); //处理商户业务逻辑
}
/**
* 获取回调通知数据 进行简单处理 返回数组
*
* 如果获取数据失败,返回false
*
* @return array|false
*/
abstract public function getNotifyData();
/**
* 检查异步通知的数据是否合法
*
* 如果检查失败,返回false
*
* @param array $data 由 $this->getNotifyData() 返回的原始回调数组
* @return boolean
*/
abstract public function checkNotifyData(array $data);
/**
* 向客户端返回必要统一的数据
* @param array $data 由 $this->getNotifyData() 返回的原始回调数组
* @return array|false
*/
abstract protected function getRetData(array $data);
/**
* 根据返回结果,回答支付机构。是否回调通知成功
* @param boolean $flag 每次返回的bool值
* @param string $msg 通知信息,错误原因
* @return mixed
*/
abstract protected function replyNotify($flag, $msg = '');
}
百度小程序回调实现类 各支付平台继承 NotifyAbstract
<?php
namespace App\Library\Notify;
use App\Library\Notify\NotifyAbstract;
use App\Library\BaiduSign;
/**
回调原始数据
{"unitPrice":"3","orderId":"82058950219732","payTime":"1589357710","dealId":"43111113","tpOrderId":"20200513576982804164","count":"1","totalMoney":"3","hbBalanceMoney":"0","userId":"4226669093","promoMoney":"0","promoDetail":"","hbMoney":"0","giftCardMoney":"0","payMoney":"3","payType":"1117","returnData":"{\"pay_type\":\"commonpub\"}","partnerId":"6011111","rsaSign":"TdHNFqt\/0bPwA2cQ8nbNG9iJL8GkEHG0Iwfk4iVYhUZ3lRYEhYx7qzYaL3easzoglTtVedsUv+WtrNmLx6ufcf2EcS86HXP2wmf5wPNxfZJk+XDzkgwqYHU1u9pWNcxn2hwdLB1NONpRogBHlsY112wAPpRGVZJtjtuzYf+2Kcs=","status":"2"}
*/
class BdNotify extends NotifyAbstract
{
// 获取原始回调数据 转成数组 array | false
public function getNotifyData()
{
$data = $_POST;
if (empty($data) || ! is_array($data)) {
return false;
}
return $data;
}
// 支付状态 及 验签 bool
public function checkNotifyData(array $data)
{
if( $data['status'] != 2 ){
return false;
}
$flag = (new BaiduSign() )->checkSignWithRsa($data);
return $flag;
}
// 获取必要字段 返回商户业务逻辑 array | false
protected function getRetData(array $data)
{
// 注意 这里向业务逻辑返回统一的格式
$retData = [
'notify_time' => date('Y-m-d H:i:s',time()),
'order_no' => $data['tpOrderId'],
'pay_amount' => $data['totalMoney'],
'transaction_id'=> $data['orderId'],
'trade_state' => $data['status'],
'raw_data' => $data, //原始数据
];
return $retData;
}
// 应答支付平台
protected function replyNotify($flag, $msg = '')
{
$ret['errno'] = 0;
if ($flag === true) {
$ret['msg'] = 'success';
$ret['data'] = ['isConsumed'=>2];
} elseif ($flag === false) {
$ret['msg'] = 'fail';
$ret['data'] = ['isConsumed'=>2,'isErrorOrder'=>1];
} else {
$ret['errno'] = 100;
$ret['msg'] = 'fail';
}
return $ret;
}
}
微信小程序回调实现类 各支付平台继承 NotifyAbstract
<?php
namespace App\Library\Notify;
use App\Library\Notify\NotifyAbstract;
use App\Library\Pay\WxSign;
class WxNotify extends NotifyAbstract
{
use \App\Library\UtilTrait\WxUtil;
// 获取原始回调数据 转成数组
public function getNotifyData()
{
$xml = file_get_contents("php://input");
$data = $this->xmlToArray($xml);
if (empty($data) || ! is_array($data)) {
return false;
}
return $data;
}
// 验签 RETURN bool
public function checkNotifyData(array $data)
{
$check_result = (new WxSign() )->checkSignWithMd5($data);
if ($check_result !== true) {
return false;
}
// 交易是否成功
if ( $data['return_code'] == 'SUCCESS' && $data['result_code'] == 'SUCCESS') {
return true;
} else {
\Log::info( $data['return_msg'] );
return false;
}
}
// 获取必要字段 返回统一格式
protected function getRetData(array $data)
{
$retData = [
'notify_time' => date('Y-m-d H:i:s',time()),
'order_no' => $data['out_trade_no'],
'pay_amount' => $data['total_fee'],
'transaction_id'=> $data['transaction_id'],
'trade_state' => '',
'raw_data' => $data,
];
return $retData;
}
// 应答支付平台
protected function replyNotify($flag, $msg = '')
{
if ($flag === true) {
$ret['return_code'] = 'SUCCESS';
$ret['return_msg'] = 'OK';
$ret = $this->arrayToXml($ret);
return $ret;
} elseif ($flag === false) {
$ret['return_code'] = 'FAIL';
$ret['return_msg'] = $msg;
$ret = $this->arrayToXml($ret);
return $ret;
}
return $flag;
}
}
/**
* [2021-09-07 11:20:02] production.INFO: array (
'appid' => '',
'bank_type' => 'OTHERS',
'cash_fee' => '1',
'fee_type' => 'CNY',
'is_subscribe' => 'N',
'mch_id' => '',
'nonce_str' => 'FXQGH7O89R5EYD0I61NSTKJ',
'openid' => 'oi7Px0MW-ffaFWfUUhLaWg',
'out_trade_no' => '20210907830006540966',
'result_code' => 'SUCCESS',
'return_code' => 'SUCCESS',
'sign' => 'BFC99CB8DCED2A27970C42BC5F7D7D2C',
'time_end' => '20210907110554',
'total_fee' => '1',
'trade_type' => 'JSAPI',
'transaction_id' => '4200001120202109075732278811',
)
*/
在回调方法中调用
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Library\Notify\BdNotify; // 百度回调处理
use App\Library\Notify\WxNotify; // 微信回调处理
use App\Service\PaymentNotify; // 业务逻辑类
class NotifyController extends Controller
{
/**
* 支付回调
*/
public function notify($platform)
{
if (!in_array($platform, ['baidu','weixin'])) {
return '暂不支持';
}
$callback = new PaymentNotify();
switch ($platform) {
case 'baidu':
$ret = (new BdNotify())->handle($callback);
break;
case 'weixin':
$ret = (new WxNotify())->handle($callback);
break;
default:
# code...
break;
}
return $ret;
}
/**
* 支付审核
*/
public function audit($platform)
{
// code..
}
/**
* 退款
*/
public function refund($platform)
{
if (!in_array($platform, ['baidu'])) {
return '暂不支持';
}
$callback = new \App\Service\RefundNotify();
switch ($platform) {
case 'baidu':
$ret = (new \App\Library\Notify\BdRefund())->handle($callback);
break;
default:
# code...
break;
}
return $ret;
}
}
应用 编写商户业务逻辑类实现契约类
<?php
namespace App\Service;
use App\Library\Notify\PayNotifyInterface; //实现契约
use Illuminate\Support\Facades\Log;
use App\Model\Pay;
class PaymentNotify implements PayNotifyInterface
{
/**
* @param $data 经过统一处理过的回调数据
*/
public function notifyProcess(array $data)
{
$platform = $pay->platform;
if (!in_array($platform, ['MP-BAIDU','MP-WEIXIN'])) {
\Log::info('PaymentNotify unknow platform');
return '不存在的支付平台';
}
$pay = Pay::where('order_no',$data['order_no'])->first();
// 如果没有该订单数据或者没有channel字段 代表从创建订单处就出错
if( !$pay || !$pay->channel ){
\Log::info('PaymentNotify order not found or less channel');
return false;
}
// 如果数据库已经更新了支付状态(支付状态为1,代表改变支付状态之前已经执行了业务逻辑) 但收银台并未核销 则核销已付款状态订单
if( $pay->pay_status == 1 && $platform == 'MP-BAIDU'){
$order_detail = ( new BdOrder() )->queryOrderDetail($data['order_no'], true);
if(is_array($order_detail)){
// 查询支付状态 如果1支付成功&&-1未退费
if($order_detail['payStatus']['statusNum']==1 && $order_detail['refundStatus']['statusNum']==-1){
return true;
}
}
}
if( $pay->pay_status == 1 && $platform == 'MP-WEIXIN'){
if ($pay->verifyed == 1) {
return true;
}
}
// 如果支付状态非0 或者 金额不符 该订单为错误订单
if ( $pay->pay_status != 0 ) {
\Log::info('PaymentNotify pay_status != 0');
return '订单状态不是未支付';
}
if ( $pay->pay_amount != $data['pay_amount'] ) {
\Log::info('PaymentNotify pay_amount not match');
return false;
}
DB::beginTransaction();
try {
// ----------分发----------
$channel = $pay->channel;
switch ($channel) {
case PAY::CASE1:
$result = $this->case1($pay, $data);
break;
case PAY::CASE2:
$result = $this->case2($pay, $data);
break;
default:
$result = false;
}
if($result !== true){
DB::rollBack(); //回滚解锁
return 'fail';
}
// ----------更新订单状态----------
$pay->transaction_id = $data['transaction_id']; // 平台交易号
$pay->notify_time = $data['notify_time']; // 回调时间
$pay->raw_data = json_encode($data['raw_data']); // 原始回调数据
$pay->pay_status = 1; // 已支付
$ret = $pay->save();
if( $ret !== true ){
DB::rollBack(); //回滚解锁
return 'fail';
}
DB::commit(); //提交解锁
}catch(\Exception $e){
Log::error($e->getMessage());
DB::rollBack(); //回滚解锁
return 'fail';
}
return true;
//百度小程序平台测试环境下可以返回false 这样支付的钱能退回来
//if ($platform == 'MP-BAIDU') {
// return false;
//}
}
public function case1($data)
{
// 改变状态
$test = Test::findOrFail($pay->test_id);
$test->is_pay = 1; // *已支付
$test->pay_time = $data['notify_time']; // 支付时间
$test->status = 1;
$ret = $test->save();
if( $ret !== true ){
return false;
}
return true;
}
public function case2($data)
{
}
}
/**
返回给商户业务逻辑的数据
{"notify_time":"2020-05-13 16:15:10",
"order_no":"20200513576982804164",
"pay_amount":"3",
"transaction_id":"82058950219732",
"trade_state":"2",
"raw_data"(原始数据):{"unitPrice":"3","orderId":"82058950219732","payTime":"1589357710","dealId":"431111113","tpOrderId":"20200513576982804164","count":"1","totalMoney":"3","hbBalanceMoney":"0","userId":"4211111093","promoMoney":"0","promoDetail":"","hbMoney":"0","giftCardMoney":"0","payMoney":"3","payType":"1117","returnData":{"pay_type":"commonpub"},"partnerId":"6111101","rsaSign":"TdHNFqt\/0bPwA2cQ8nbNG9iJL8GkEHG0Iwfk4iVYhUZ3lRYEhYx7qzYaL3easzoglTtVedsUv+WtrNmLx6ufcf2EcS86HXP2wmf5wPNxfZJk+XDzkgwqYHU1u9pWNcxn2hwdLB1NONpRogBHlsY112wAPpRGVZJtjtuzYf+2Kcs=","status":"2"}}
*/
商户主动查询支付状态
系统建立一个定时任务 command
每分钟主动查询支付状态,结果记录在 pays
表中的 verifyed
字段中。
商户在用户下单后、及一些特殊业务场景主动查询支付状态,
本作品采用《CC 协议》,转载必须注明作者和本文链接
测试稳定版更新
9.12日更新