小程序中Token交互及用户下订单及支付的处理
最近正在做小程序商城相关的事情,遇到了用户下单-支付的过程,其中遇到了很多bug,踩了很多坑终于跳出来了,在这里做一个总结吧。
处理图:
流程:
// 用户在选择商品后,向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;
}
}
本机电脑上用开发者工具测试的时候会出现一个二维码,如果放到服务器用真机测试,则会拉起支付页面。