前言:
一个商城中最复杂的业务是什么,可能大家都有自己的看法,在我看来下单算是最复杂也必须加倍谨慎的地方。今天就介绍下我的下单接口。也能帮自己梳理一番。
首先需要交代下需求。
我的需求就是,在订单生成的同时,还要生成订单快照,保留订单下单时的订单信息。即便是之后商品改名或者降价等变化,也不影响订单的数据。
那么,下面我就来理一下本次下单接口的思路
下单接口流程
- 接收客户端传递过来的商品和数量的数据
- 验证数据
- 检查库存
- 如库存充足,创建订单及订单快照
这是一个比较粗略的思路,我们一步一步的来整理这些业务逻辑。
1.接收数据并验证
看过我之前写过的验证器的朋友一定知道,我的项目里使用的独立验证器。只不过,这次验证的数据比较不同,它是一个二维数组。
我们来看一下客户端传递过来的数据结构
products=>[
['product_id'=>1,'count'=>5],
['product_id'=>3,'count'=>2]
];
我们不难看出,这次客户端传递过来的实际上是一个二维数组,我们验证的其实是它里面的product_id和count。那么这样的验证器该如何写呢?
下面看代码,首先我们写两个验证规则,这个名字叫rule的成员变量会在check的时候自动引入,所以我们不能去改它的名字,至于singleRule嘛,就是我自己想的名字了,引入靠我们自己,所以叫什么名字也无所谓了
protected $singleRule = [
'product_id' => 'require|positiveInt',
'count' => 'require|positiveInt'
];
protected $rule = [
'products' => 'require|checkProducts'
];
首先,当我们控制器调用我之前写好的通用验证方法时,就会按按照rule对products这个二维数组进行验证,验证规则有两个,一个是require我就不介绍了,另一个是我写的自定验证方法 checkProducts
protected function checkProducts($value)
{
if (!is_array($value)) {
throw new ParameterException(['message' => 'products must be array']);
}
foreach ($value as $v) {
$this->checkProduct($v);
}
return true;
}
验证器会将products二维数组传入我们写好的自定义方法。我们首先排除客户端传递过来的不是数组的情况,如果数据结构不对,直接抛出参数错误的异常。
随后,我们使用foreach遍历数组,通过遍历后的一维数组$v的结构是
['product_id'=>1,'count'=>5]
这时我又将这个一维数组传入一个叫checkProduct的方法,看见这个名字大家一定都想到了,这是一个单独验证某一个商品的验证方法。
protected function checkProduct($v)
{
//为什么要new BaseValidate呢?这里是因为我们传入的singleRule中有自定义方法,这个自定义方法就写在BaseValidate方法中
$validateObj = new BaseValidate($this->singleRule);
$result = $validateObj->batch()->check($v);
if (!$result) {
throw new ParameterException(['msg'=>$validateObj->getError()]);
}
return true;
}
这里我们使用验证器的另外一种用法,直接new一个验证器,传入规则。再调用这个对象上的check方法进行验证。而这个规则就是我们之前已经写好的成员变量singleRule。(这里new的是我们自己写的baseValidate类,主要原因是我们在验证规则中写了自定义的验证规则【positiveInt】验证正整数。如果不使用自定义验证规则,new Validate类也可以)
那么现在我们就将验证器写好了,只需要控制器中和其他接口一样,使用订单验证器对象调用goCheck方法就好了,一气呵成。
2. 检查库存
这是今天的重点,为什么说是重点呢,因为检查库存不仅仅是在下单的时候要用,在支付的时候也要用。
首先比较复杂的逻辑,我一般会把它封装到service层中。很明显我需要一个下单的服务类。那么这个类中我首先会创建3个成员变量。
class Order
{
protected $oProducts;//订单中的商品和数量数组,o代表order
protected $products;//根据订单的商品id,查询出数据库中商品的数据
protected $uid;//用户的id
}
为什么要定义这个成员变量呢?首先我们最重要的库存检测,其实说白了,就是订单商品的数量同数据库中对应的商品库存数据经行比较而已。那么$oProducts 和 $products 我们将这两个数据保存起来。更直观,也更方便调用,不用在方法之间反复传递。
那么我们现在直接来看OrderService层的下单方法,我的习惯是一个服务层,只提供一个对外公开的方法,尽量将这个方法抽象出多个私有的方法,让逻辑更清晰,更立体
public function place($uid, $oProducts)
{
$this->oProducts = $oProducts;
$this->uid = $uid;
$this->products = $this->getProductByOrder($oProducts);
//调用检测库存的方法
$status = $this->getOrderStatus();
//如果订单检测不通过
if (!$status['pass']) {
//因为库存不足的原因下单失败,所以也就没有order_id
$status['order_id'] = -1;
//订单中有库存不足的商品,返回到控制器中
return $status;
}
//库存充足,创建订单
//根据整理的订单数据,创建订单快照数据
$snap = $this->snapOrder($status);
//结合订单快照数据,在数据库中创建订单
$result = $this->createOrder($snap);
$result['pass'] = true;
return $result;
}
大家可以先不着急看这些方法是怎么实现的,首先看看这个下单方法的流程;
1.先来看第一个,根据订单商品获取数据库商品数据方法
/**
* 通过订单数据(二维数组)
* @param $oProducts
* @return mixed
* @throws \think\exception\DbException
*/
private function getProductByOrder($oProducts)
{
//获取订单中所有的商品id号(使用抽离二维数据中的一列数据方法)
$oPids = array_column($oProducts, 'product_id');
//将ids到数据库中去查询
$products = Product::all($oPids)
//选择要显示的字段
->visible(['id', 'name', 'price', 'main_img_url', 'stock']);
//因为我在配置多条数据查询数据时,返回的是collection数据集对象,所以需要toArray转为数组
return $products->toArray();
}
2.订单数据和数据库商品对比方法(也就是检测库存方法)
/**
* 获取到订单下商品的库存状态,和订单相关数据
* @return array
* @throws OrderException
*/
private function getOrderStatus()
{
//设计一个返回参数的数据结构
$status = [
//订单状态
'pass' => true,
//订单总金额
'orderPrice' => 0,
//订单商品总数
'orderCount' => 0,
//订单商品详情信息(在订单记录里体现)
'pStatusArray' => []
];
//便利订单商品数组,便利出单个商品$oProduct
foreach ($this->oProducts as $oProduct) {
//将单个商品传入对比库存方法中,当遍历结束就等于每个订单商品都和数据库中的库存做了比较
$pStatus = $this->getProductStatus($oProduct['product_id'], $oProduct['count'], $this->products);
//如果某一个商品的库存不足的话,整个订单的pass状态就变成了false
if ($pStatus['haveStock'] == false) {
$status['pass'] = false;
}
//计算出订单总价
$status['orderPrice'] += $pStatus['totalPrice'];
//计算出订单商品数量
$status['orderCount'] += $oProduct['count'];
//将每个商品对比结果存到订单商品详情数组中
array_push($status['pStatusArray'], $pStatus);
}
return $status;
}
3.单个商品库存检测
其实这个方法应该数据上一个检测库存的子方法,因为,当我们提交订单时,订单中可能有多种商品,那么我们要检测库存,就必须将每种商品单独拉出来检测库存,如果订单中所有的商品都有库存才算订单库存充足
/**
* 获取某一个商品的状态
* @param $oPid
* @param $oCount
* @param $products
* @return array
* @throws OrderException
*/
private function getProductStatus($oPid, $oCount, $products)
{
//设计返回数据结构
$pStatus = [
//下单商品的id
'id' => 0,
//下单商品的名字
'name' => '',
//订单的商品数量
'count' => 0,
//下单商品库存是否足够的bool
'haveStock' => false,
//订单中单个商品的总价 单价*数量
'totalPrice' => 0
];
//商品的键值(用于匹配到商品时,记录数据库查出的商品数组中的某一个商品数组的键值)
$pIndex = -1;
//循环根据订单查询出数据库商品数据
for ($i = 0; $i < count($products); $i++) {
//当订单id 和数据库商品数据的某一条id匹配时
if ($oPid == $products[$i]['id']) {
//将商品键值改为当前循环控制变量
$pIndex = $i;
}
}
if ($pIndex == -1) {
//客户端传来的id 我们先通过all方法查询出数据库中的数据得到了products数组,如果说数据库中没有这个商品,那么
//products里面肯定也不会有这条数据,在for循环中也不回匹配到。所有$pIndex还是没有被赋值,也就还等于-1
throw new OrderException(
['msg' => '商品' . $oPid . '没有找到哦兄弟']
);
}
//将数据分别存入我们设计好的数据结构中
$pStatus['id'] = $oPid;
$pStatus['name'] = $products[$pIndex]['name'];
$pStatus['count'] = $oCount;
//拿商品的数量和数据库中的库存数量进行对比,库存足就是true,反之false
$pStatus['haveStock'] = $oCount <= $products[$pIndex]['stock'] ? true : false;
//某一类商品的总价
$pStatus['totalPrice'] = $oCount * $products[$pIndex]['price'];
return $pStatus;
}
4.当商品库存检测通过,我们就需要创建订单快照
这个订单快照有什么用呢?就是记录订单在下单的时候,商品的一些信息,这个保存起来,在客户就可以翻看自己的购买记录.而且这些订单数据,是下单时候完整的保存的保存到数据库中.这意味着,后来这个曾经购买的商品降价或则改名等等都是不会影响订单快照的内容的
注意下面的订单详细数据和用户的地址,是以序列化数组的形式存储起来的
/**
* 创建订单快照
* @param $status
* @return array
* @throws
*/
private function snapOrder($status)
{
//设计返回的数据结构
$snap = [
//快照总金额
'total_price' => 0,
//快照总商品总数
'total_count' => 0,
//快照首个商品名字
'snap_name' => null,
//快照首个商品图片
'snap_img' => '',
//快照订单下订单详情
'snap_item' => [],
//快照用户地址
'snap_address' => []
];
//运用订单状态中预存的总价格
$snap['total_price'] = $status['orderPrice'];
//用户订单状态中预存的总数量
$snap['total_count'] = $status['orderCount'];
//取根据订单查到的商品信息数组中的第一个商品的名字
$snap['snap_name'] = $this->products[0]['name'];
//同上,第一个的图片url
$snap['snap_img'] = $this->products[0]['main_img_url'];
//通过uid查询地址的方法
$snap['snap_address'] = serialize($this->getUserAddress());
//通过订单状态中预存的商品数据数组
$snap['snap_items'] = serialize($status['pStatusArray']);
//为了方便客户端,如果商品数量大于1 就加个等 字.可以省略
$snap['total_count'] > 1 && $snap['snap_name'] .= '等';
return $snap;
}
4.1 获取用户地址方法
首先这是个很简单业务逻辑,如果用户没有地址数据,那么我们就应该不让他下订单.获取也很简单,就是通过用户id去表里查就是了
/**
* 通过用户的id获取到用户的地址数据
* @return array
* @throws
*/
private function getUserAddress()
{
$userAddress = UserAddress::where(['user_id' => $this->uid])->find();
if (!$userAddress) {
throw new UserException([
'msg' => 'UserAddress is not found place order is fail',
'errCode' => 80001
]);
}
return $userAddress->toArray();
}
5. 订单保存
订单保存分两步走:
- 保存订单主表中的数据
- 保存子订单里的数据
所以这里使用了事务来保证数据的完整性
/**
* 将订单数据存入数据库
* @param $nsap
* @return array
* @throws
*/
private function createOrder($nsap)
{
//开启事务
Db::startTrans();
try {
//生成订单号,使用公共方法
$orderNo = makeOrderNo();
$orderObj = new OrderModel();
//订单号
$orderObj->order_no = $orderNo;
//用户id
$orderObj->user_id = $this->uid;
//订单总价
$orderObj->total_price = $nsap['total_price'];
//订单快照首图
$orderObj->snap_img = $nsap['snap_img'];
//订单快照,用户地址
$orderObj->snap_address = $nsap['snap_address'];
//订单快照,订单详细数据
$orderObj->snap_items = $nsap['snap_items'];
//订单快照,首个商品的名字
$orderObj->snap_name = $nsap['snap_name'];
//商品总数
$orderObj->total_count = $nsap['total_count'];
$orderObj->save();
//创建关联表数据
$orderId = $orderObj->id;
$create_time = $orderObj->create_time;
//子订单模型实例化对象(因为订单和商品是一个多对多的关系,所需要一个子订单表来存储)
$orderProductObj = new OrderProduct();
//引用赋值将order_id 加入到Oproducts中
foreach ($this->oProducts as &$oProduct) {
$oProduct['order_id'] = $orderId;
}
//多条数据一并保存
$orderProductObj->saveAll($this->oProducts);
//提交事务
Db::commit();
return [
'order_no' => $orderNo,
'order_id' => $orderId,
'create_time' => $create_time
];
} catch (Exception $ex) {
//回滚事务
Db::rollback();
throw $ex;
}
}
控制器的调用
将所有的业务都封装好了之后,控制器就比较轻松了
/**
* 下单方法
* @url http://local.jxshop.com/api/v1/order/place
* @http POST
*/
public function placeOrder()
{
OrderPlace::instance()->goCheck();
$uid = TokenService::getCurrentId();
//通过接收数组 需要在后面加个/a
$oProducts = input('post.products/a');
$orderServiceObj = new OrderService();
$result = $orderServiceObj->place($uid, $oProducts);
return $result;
}
总结:这次的代码比较复杂,可能这篇博客的可读性,不够强。不过没关系。我反正也把我这套接口代码开源了。如果有兴趣的朋友,直接克隆下完整的代码可能收获会更多。我也非常的希望能有朋友能指出我的错误。先谢过了
项目地址:码云
以上