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 协议》,转载必须注明作者和本文链接
welcome come back
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 2

测试稳定版更新

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
95
粉丝
24
喜欢
156
收藏
348
排名:324
访问:2.9 万
私信
所有博文
社区赞助商