最近正在做小程序商城相关的事情,遇到了用户下单-支付的过程,其中遇到了很多bug,踩了很多坑终于跳出来了,在这里做一个总结吧。

处理图:

java微信小程序支付一直发回调信息怎么办_微信

流程:

// 用户在选择商品后,向API提交包含它所选择商品的相关信息

// API在接收到信息后,需要检查订单相关商品的库存量

// 有库存,把订单数据存入数据库中 = 下单成功,返回客户端消息,告诉客户端可以支付了

// 调用支付接口,进行支付

// 还需要再次进行库存量检测

// 服务器这边就可以调用微信的支付接口进行支付

// 小程序根据服务器返回的结果拉起微信支付

// 微信会返回给我们一个支付结果(异步)

// 成功:也需要进行库存量的检测

// 成功:进行库存量的扣除

因为这里逻辑处理较为复杂,所以单独建立一个service模块来处理更加复杂的逻辑问题。

1.获取Token

小程序发送一个code码给后端服务器,服务器返回给小程序一个Token令牌

Token控制器:

控制器前的(new TokenGet())->goCheck();为编写的统一验证参数的方法

<?php
namespace app\api\controller\v1;

use app\api\validate\TokenGet;
use app\api\service\UserToken;


class Token
{
    public function getToken($code = '')
    {
        (new TokenGet())->goCheck();
        $ut = new UserToken($code);
        // 返回给微信服务器一个令牌
        $token = $ut->get();
        return ['token' => $token];
    }
}

service模块中的Token类封装处理:

<?php
namespace app\api\service;

use think\Cache;
use app\lib\enum\ScopeEnum;
use app\lib\exception\ForbiddenException;
use app\lib\exception\TokenException;
use think\Exception;


class Token
{
    // 生成Token
    public static function generateToken()
    {
        //32个字符组成一组随机字符串
        $randChars = getRandChar();
        //用三组字符串,进行md5加密
        $timestamp = time();
        //salt 盐
        $salt = config('secure.token_salt');
        return md5($randChars . $timestamp . $salt);
    }
    // 通用方法获取缓存中的具体信息
    public static function getCurrentTokenVar($key)
    {
        $token = request()->header('token');
        $vars = Cache::get($token);
        if (!$vars) {
            throw new TokenException();
        } else {
            if (!is_array($vars)) {
                $vars = json_decode($vars, true);
            }
            if (array_key_exists($key, $vars)) {
                return $vars[$key];
            } else {
                throw new Exception('尝试获取Token的变量不存在');
            }

        }
    }
    //获取当前用户uid
    public static function getCurrentUid()
    {
        return self::getCurrentTokenVar('uid');
    }
    // 判断权限-管理员和注册用户(可用)
    public static function needPrimaryScope()
    {
        $scope = self::getCurrentTokenVar('scope');
        if ($scope) {
            if ($scope >= ScopeEnum::User) {
                return true;
            } else {
                throw new ForbiddenException();
            }
        } else {
            throw new TokenException();
        }
    }
    // 判断权限-管理员(不可用),用户(可用)
    public static function needExclusiveScope()
    {
        $scope = self::getCurrentTokenVar('scope');
        if ($scope) {
            if ($scope == ScopeEnum::User) {
                return true;
            } else {
                throw new ForbiddenException();
            }
        } else {
            throw new TokenException();
        }
    }
    public static function isValidOperate($chekedUID)
    {
        if (!$chekedUID) {
            throw new Exception('检查UID时必须传入一个被检查的UID');
        }
        $currentOperateUID = self::getCurrentUid();
        if ($currentOperateUID == $chekedUID) {
            return true;
        }
        return false;
    }
}

2.用户UserToken类继承Token

<?php
namespace app\api\service;

use think\Exception;
use app\api\model\User as UserModel;
use app\lib\exception\WeChatException;
use app\lib\enum\ScopeEnum;

class UserToken extends Token
{
    protected $code;
    protected $wxAppID;
    protected $wxAppSecret;
    protected $wxLoginUrl;
    public function __construct($code)
    {
        $this->code = $code;
        $this->wxAppID = config('wx.app_id');
        $this->wxAppSecret = config('wx.app_secret');
        $this->wxLoginUrl = sprintf(config('wx.login_url'), $this->wxAppID, $this->wxAppSecret, $this->code);
    }
    // 向微信服务器发送curl请求,得到code码并生成令牌返回
    public function get()
    {
        // 发送http请求方法 默认为GET
        $result = http($this->wxLoginUrl);
        $wxResult = json_decode($result, true);
        if (empty($wxResult)) {
            throw new WeChatException(['msg' => 'Code参数错误,获取session_key及openID时异常:微信内部错误']);
        } else {
            $loginFail = array_key_exists('errcode', $wxResult);
            if ($loginFail) {
                $this->processLoginError($wxResult);
            } else {
                return $this->grantToken($wxResult);
            }
        }
    }
    // 颁发令牌
    private function grantToken($wxResult)
    {
        // 拿到openid
        // 数据库中比对,这个openid是否存在
        // 如果存在:则不处理,如果不存在:新增一条记录
        // 生成令牌,准备缓存数据,写入缓存
        // 把令牌返回 到客户端去
        // key:令牌
        // value:wxResult, uid, scope
        $openid = $wxResult['openid'];
        $user = UserModel::getByOpenID($openid);
        if ($user) {
            $uid = $user->id;
        } else {
            $uid = $this->newUser($openid);
        }
        // 组合缓存数据
        $cachedValue = $this->prepareCachedValue($wxResult, $uid);
        // 调用存缓存方法
        $token = $this->saveToCache($cachedValue);
        return $token;
    }
    // 写入缓存
    private function saveToCache($cachedValue)
    {
        // 获取32位加密令牌
        $key = self::generateToken();
        $value = json_encode($cachedValue);
        $expire_in = config('setting.token_expire_in');
        // 利用tp自带缓存
        $result = cache($key, $value, $expire_in);
        if (!$result) {
            throw new TokenException([
                'msg' => '服务器缓存异常',
                'errorCode' => 10005
            ]);
        }
        return $key;
    }
    // 组合准备写入缓存的数据
    private function prepareCachedValue($wxResult, $uid)
    {
        $cachedValue = $wxResult;
        $cachedValue['uid'] = $uid;
        $cachedValue['scope'] = ScopeEnum::User;
        return $cachedValue;
    }
    // user表新增一条信息,返回用户id号
    private function newUser($openid)
    {
        $user = UserModel::create(['openid' => $openid]);
        return $user->id;
    }
    // 认证异常
    private function processLoginError($wxResult)
    {
        throw new WeChatException([
            'msg' => $wxResult['errmsg'],
            'errorCode' => $wxResult['errcode'],
        ]);
    }
}

3.用户下订单Order控制器:

<?php

namespace app\api\controller\v1;

use think\Controller;
use app\lib\enum\ScopeEnum;
use app\api\service\Token as TokenService;
use app\api\service\Order as OrderService;
use app\lib\exception\ForbiddenException;
use app\lib\exception\TokenException;
use app\api\validate\OrderPlace;


class Order extends BaseController
{
    // 判断权限的前置操作
    protected $beforeActionList = [
        'checkExclusiveScope' => ['only' => 'placeOrder']
    ];
    // 用户在选择商品后,向API提交包含它所选择商品的相关信息
    // API在接收到信息后,需要检查订单相关商品的库存量
    // 有库存,把订单数据存入数据库中 = 下单成功,返回客户端消息,告诉客户端可以支付了
    // 调用支付接口,进行支付
    // 还需要再次进行库存量检测
    // 服务器这边就可以调用微信的支付接口进行支付
    // 小程序根据服务器返回的结果拉起微信支付
    // 微信会返回给我们一个支付结果(异步)
    // 成功:也需要进行库存量的检测
    // 成功:进行库存量的扣除

    /**
     * 下单
     * @url /order
     * @HTTP POST
     */
    public function placeOrder()
    {
        (new OrderPlace())->goCheck();
        $products = input('post.products/a');
        $uid = TokenService::getCurrentUid();
        $order = new OrderService();
        $status = $order->place($uid, $products);
        return $status;
    }
}

4.service模块中的order类:

<?php
namespace app\api\service;

use app\api\model\Product;
use app\lib\exception\OrderException;
use app\api\model\UserAddress;
use app\lib\exception\UserException;
use app\api\model\Order as OrderModel;
use app\api\model\OrderProduct;
use think\Exception;
use think\Db;


class Order
{
    // 订单的商品列表,也就是客户端传过来的products参数
    protected $oProducts;
    // 真实的商品信息(包括库存量)
    protected $products;
    protected $uid;

    public function place($uid, $oProducts)
    {
        // $oProducts和$products作对比
        // products从数据库中查询出来
        $this->oProducts = $oProducts;
        $this->products = $this->getProductsByOrder($oProducts);
        $this->uid = $uid;
        $status = $this->getOrderStatus();
        if (!$status['pass']) {
            $status['order_id'] = -1;
            return $status;  //库存量检测不通过
        }
        // 库存量检测通过后
        // 1.成订单快照信息
        // 2.创建订单写入数据库
        $orderSnap = $this->snapOrder($status);
        //  生成订单
        $order = $this->createOrder($orderSnap);
        $order['pass'] = true;
        return $order;

    }
    //  创建订单写入数据库
    private function createOrder($snap)
    {
        //  开启事务
        Db::startTrans();
        try {
            $orderNo = $this->makeOrderNo();
            $order = new OrderModel();
            $order->user_id = $this->uid;
            $order->order_no = $orderNo;
            $order->total_price = $snap['orderPrice'];
            $order->total_count = $snap['totalCount'];
            $order->snap_img = $snap['snapImg'];
            $order->snap_name = $snap['snapName'];
            $order->snap_address = $snap['snapAddress'];
            $order->snap_items = json_encode($snap['pStatus']);
            $order->save();
            $orderID = $order->id;
            $create_time = $order->create_time;
            foreach ($this->oProducts as &$v) {
                $v['order_id'] = $orderID;
            }
            $orderProduct = new OrderProduct();
            $orderProduct->saveAll($this->oProducts);
            //  提交事务
            Db::commit();
            return [
                'order_no' => $orderNo,
                'order_id' => $orderID,
                'create_time' => $create_time
            ];
        } catch (Exception $e) {
            //  回滚事务
            Db::rollback();
            throw $e;
        }
    }
    //  生成唯一订单号
    public static function makeOrderNo()
    {
        $yCode = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J');
        $orderSn = $yCode[intval(date('Y')) - 2019] . strtoupper(dechex(date('m'))) . date('d') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99));
        return $orderSn;
    }
    //  生成订单快照
    private function snapOrder($status)
    {
        $snap = [
            'orderPrice' => 0,
            'totalCount' => 0,
            'pStatus' => [],
            'snapAddress' => null,
            'snapName' => '',
            'snapImg' => ''
        ];
        $snap['orderPrice'] = $status['orderPrice'];
        $snap['totalCount'] = $status['totalCount'];
        $snap['pStatus'] = $status['pStatusArray'];
        $snap['snapAddress'] = json_encode($this->getUserAddress());
        $snap['snapName'] = $this->products[0]['name'];
        $snap['snapImg'] = $this->products[0]['main_img_url'];
        if (count($this->products) > 1) {
            $snap['snapName'] .= '等';
        }
        return $snap;
    }
    //  获取用户收货地址
    private function getUserAddress()
    {
        $userAddress = UserAddress::where('user_id', $this->uid)->find();
        if (!$userAddress) {
            throw new UserException([
                'msg' => '用户收获地址不存在,下单失败',
                'errorCode' => 60001
            ]);
        }
        return $userAddress->toArray();
    }
    //  对外提供的库存量检测方法
    public function checkOrderStock($orderID)
    {
        $oProducts = OrderProduct::where('order_id', $orderID)->select();
        $this->oProducts = $oProducts;
        $this->products = $this->getProductsByOrder($oProducts);
        $status = $this->getOrderStatus();
        return $status;
    }
    // 获取库存检测后的订单状态等信息
    private function getOrderStatus()
    {
        $status = [
            'pass' => true,
            'orderPrice' => 0,
            'totalCount' => 0,
            'pStatusArray' => []
        ];
        foreach ($this->oProducts as $oProduct) {
            $pStatus = $this->getProductStatus($oProduct['product_id'], $oProduct['count'], $this->products);
            if (!$pStatus['haveStock']) {
                $status['pass'] = false;
            }
            $status['orderPrice'] += $pStatus['totalPrice'];
            $status['totalCount'] += $pStatus['count'];
            array_push($status['pStatusArray'], $pStatus);
        }
        return $status;
    }
    // 循环比较订单参数信息看是否缺货等
    private function getProductStatus($oPID, $oCount, $products)
    {
        $pIndex = -1;
        $pStatus = [
            'id' => null,
            'haveStock' => false,
            'count' => 0,
            'name' => '',
            'totalPrice' => 0
        ];
        for ($i = 0; $i < count($products); $i++) {
            if ($oPID == $products[$i]['id']) {
                $pIndex = $i;
            }
        }
        if ($pIndex == -1) {
            // 客户端传递的productid有可能根本不存在
            throw new OrderException([
                'msg' => 'id为:' . $oPID . '商品不存在,创建订单失败'
            ]);
        } else {
            $product = $products[$pIndex];
            $pStatus['id'] = $product['id'];
            $pStatus['name'] = $product['name'];
            $pStatus['count'] = $oCount;
            $pStatus['totalPrice'] = $product['price'] * $oCount;
            if (($product['stock'] - $oCount) >= 0) {
                $pStatus['haveStock'] = true;
            }
        }
        return $pStatus;
    }
    // 根据订单信息查找真实的商品信息
    private function getProductsByOrder($oProducts)
    {
        // 将订单中商品的id号放到一个数组中
        $oPIDs = [];
        foreach ($oProducts as $item) {
            array_push($oPIDs, $item['product_id']);
        }
        $products = Product::all($oPIDs)->visible(['id', 'price', 'stock', 'name', 'main_img_url'])->toArray();
        return $products;
    }
}

其中,这里对库存量的检测是重点,要进行多次的库存量检测

5.微信支付控制器:

<?php

namespace app\api\controller\v1;

use app\api\validate\IDMustBePositiveInt;
use app\api\service\Pay as PayService;
use app\api\service\WxNotify;



class Pay extends BaseController
{
    // 判断权限的前置操作
    protected $beforeActionList = [
        'checkExclusiveScope' => ['only' => 'getPreOrder']
    ];
    //  请求预订单(微信服务器)
    public function getPreOrder($id = '')
    {
        (new IDMustBePositiveInt())->goCheck();
        $pay = new PayService($id);
        return $pay->pay();
    }
    //  提供给微信的回调接口
    public function receiveNotify()
    {
        //  1.检查库存量,超卖
        //  2.更新订单的status状态
        //  3.减库存
        //  4.如果成功处理,我们返回微信成功处理信息,否则,我们需要返回没有成功处理

        //特点:post,xml格式,不会携带参数
        $notify = new WxNotify();
        $notify->Handle();
    }
}

这里要加载微信提供的SDK其中的参数都有说明

6.service中的pay类:

<?php

namespace app\api\service;

use think\Exception;
use app\api\service\Order as OrderService;
use app\api\model\Order as OrderModel;
use app\lib\exception\OrderException;
use app\lib\exception\TokenException;
use app\lib\enum\OrderStatusEnum;
use think\Loader;
use think\Log;

Loader::import('WxPay.WxPay', EXTEND_PATH, '.Api.php');

class Pay
{
    private $orderID;
    private $orderNO;
    function __construct($orderID)
    {
        if (!$orderID) {
            throw new Exception('订单号不允许为NULL');
        }
        $this->orderID = $orderID;
    }
    public function pay()
    {
        //  订单号可能不存在
        //  订单号是存在的,但是订单号和当前用户不匹配
        //  订单有可能已经被支付过
        //  进行库存量检测
        $this->checkOrderValid();
        $orderService = new OrderService();
        $status = $orderService->checkOrderStock($this->orderID);
        if (!$status['pass']) {
            return $status;  //  库存量检测不通过
        }
        //  检测通过,下一步生成微信预订单
        return $this->makeWxPreOrder($status['orderPrice']);
    }
    //  微信预订单
    private function makeWxPreOrder($totalPrice)
    {
        //  获取openid
        $openid = Token::getCurrentTokenVar('openid');
        if (!$openid) {
            throw new TokenException();
        }
        //  统一下单输入对象
        $wxOrderData = new \WxPayUnifiedOrder();
        $wxOrderData->SetOut_trade_no($this->orderNO);
        $wxOrderData->SetTrade_type('JSAPI');
        $wxOrderData->SetTotal_fee($totalPrice * 100);
        $wxOrderData->SetBody('玻璃商贩');
        $wxOrderData->SetOpenid($openid);
        $wxOrderData->SetNotify_url(config('secure.pay_back_url'));//  接收微信支付回调接口url
        //  调用微信预订单接口
        return $this->getPaySignature($wxOrderData);
    }
    //  向微信服务器发送预订单请求
    private function getPaySignature($wxOrderData)
    {
        //  调用微信预订单接口
        $wxOrder = \WxPayApi::unifiedOrder($wxOrderData);
        if ($wxOrder['return_code'] != 'SUCCESS' || $wxOrder['result_code'] != 'SUCCESS') {
            Log::record($wxOrder, 'error');
            Log::record('获取预支付订单失败', 'error');
        }
        //  记录prepay_id入数据库
        $this->recordPreOrder($wxOrder['prepay_id']);
        //  生成签名并返回签名和原始参数数据
        $signature = $this->sign($wxOrder);
        return $signature;
    }
    //  生成签名
    private function sign($wxOrder)
    {
        $jsApiPayData = new \WxPayJsApiPay();
        $jsApiPayData->SetAppid(config('wx.app_id'));
        $jsApiPayData->SetTimeStamp((string)time());
        $rand = md5(time() . mt_rand(0, 1000));
        $jsApiPayData->SetNonceStr($rand);
        $jsApiPayData->SetPackage('prepay_id=' . $wxOrder['prepay_id']);
        $jsApiPayData->SetSignType('md5');
        $sign = $jsApiPayData->MakeSign();
        $rawValues = $jsApiPayData->GetValues();  //  原始参数的数组
        $rawValues['paySign'] = $sign;
        unset($rawValues['appId']);
        return $rawValues;  //  原始参数和签名的数组
    }
    //  处理prepay_id
    private function recordPreOrder($prepay_id)
    {
        OrderModel::where('id', $this->orderID)->update(['prepay_id' => $prepay_id]);
    }
    //  付款前检查订单各种情况的状态
    private function checkOrderValid()
    {
        $order = OrderModel::where('id', $this->orderID)->find();
        if (!$order) {
            throw new OrderException();
        }
        if (!Token::isValidOperate($order->user_id)) {
            throw new TokenException([
                'msg' => '订单与用户不匹配',
                'errorCode' => 10003
            ]);
        }
        if ($order->status != OrderStatusEnum::UNPAID) {
            throw new OrderException([
                'msg' => '订单已经支付过了',
                'errorCode' => 80003,
                'code' => 400
            ]);
        }
        $this->orderNO = $order->order_id;
        return true;
    }
}

本机电脑上用开发者工具测试的时候会出现一个二维码,如果放到服务器用真机测试,则会拉起支付页面。