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 前端工作流等。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 2

测试稳定版更新

3年前 评论

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