前言:

一个商城中最复杂的业务是什么,可能大家都有自己的看法,在我看来下单算是最复杂也必须加倍谨慎的地方。今天就介绍下我的下单接口。也能帮自己梳理一番。
首先需要交代下需求。


我的需求就是,在订单生成的同时,还要生成订单快照,保留订单下单时的订单信息。即便是之后商品改名或者降价等变化,也不影响订单的数据。

那么,下面我就来理一下本次下单接口的思路

下单接口流程

  1. 接收客户端传递过来的商品和数量的数据
  2. 验证数据
  3. 检查库存
  4. 如库存充足,创建订单及订单快照

这是一个比较粗略的思路,我们一步一步的来整理这些业务逻辑。

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代表orderprotected $products;//根据订单的商品id,查询出数据库中商品的数据protected $uid;//用户的id
}

为什么要定义这个成员变量呢?首先我们最重要的库存检测,其实说白了,就是订单商品的数量同数据库中对应的商品库存数据经行比较而已。那么 oProducts和 o P r o d u c t s 和 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' => []];//便利订单商品数组,便利出单个商品$oProductforeach ($this->oProducts as $oProduct) {//将单个商品传入对比库存方法中,当遍历结束就等于每个订单商品都和数据库中的库存做了比较$pStatus = $this->getProductStatus($oProduct['product_id'], $oProduct['count'], $this->products);//如果某一个商品的库存不足的话,整个订单的pass状态就变成了falseif ($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还是没有被赋值,也就还等于-1throw 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. 订单保存

订单保存分两步走:
1. 保存订单主表中的数据
2. 保存子订单里的数据
所以这里使用了事务来保证数据的完整性

       /*** 将订单数据存入数据库* @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;}

总结:这次的代码比较复杂,可能这篇博客的可读性,不够强。不过没关系。我反正也把我这套接口代码开源了。如果有兴趣的朋友,直接克隆下完整的代码可能收获会更多。我也非常的希望能有朋友能指出我的错误。先谢过了
项目地址:码云

以上

商城API开发之下单接口相关推荐

  1. api中文文档 mws_Amzon MWS API开发之订单接口

    /// ///获得账户信息/// protectedAccountConfig Account {get{returnAccountConfig.Instance; } }privateMarketp ...

  2. 淘宝客微信机器人接口API开发

    淘宝客微信机器人API接口,淘客机器人API,微信机器人API 微信二次开发完整API功能 需要文档的自取wkteam.gitbook.io 今天给大家介绍微信个人号自动回复机器人的二次开发!使用微信 ...

  3. 个人微信开发协议sdk接口API分享

    个人微信开发协议sdk接口API分享 1.基础消息类型 1.客户端发送的心跳包 HeartBeatReq = 1001; 2.消息接收确认回复(接收或拒绝接收) MsgReceivedAck = 10 ...

  4. 做微信h5支付的统一下单接口开发,虽然已经生成了mweb_url支付链接,但是访问时出现错误提示:‘商家参数格式有误,请联系商家解决’,但是检查h5支付提交的参数,都没有错误。 微信h5支付开发错误

    做微信h5支付的统一下单接口开发,虽然已经生成了mweb_url支付链接,但是访问时出现错误提示:'商家参数格式有误,请联系商家解决',但是检查h5支付提交的参数,都没有错误. 后面看到官方文档, 说 ...

  5. Dota2数据Dota2接口电竞api开发比分网分享

    Dota2数据Dota2接口电竞api开发比分网分享@TOC 数据来自marz数据alan@marzesport.com 1.获取赛事 接口:{{host1}}/api/series/9870? {& ...

  6. 股票API下单接口是怎样传入交易数据的?

    股票API下单接口传入交易数据的方法有四种,但主要使用的还是csvdatasetconfig和函数.不过基于当前的线程组或者请求创建CSV数据文件配置组件.如果这份数据只有这一个请求会用,那么就基于请 ...

  7. 驰骋工作流引擎-API开发接口-重要的部分.

    驰骋工作流引擎-API开发接口-重要的部分. - 多看文档,少走弯路,学会使用ccbpm的api接口,让您的sdk表模式开发无忧. 登录与门户API 首先要进行代码集成与组织机构的集成 其次在自己的系 ...

  8. 【拼多多API 开发系列】百亿补贴商品详情接口,代码封装

    为了进行电商平台 PDD 的API开发,首先我们需要做下面几件事情. 1)开发者注册一个账号 2)然后为每个 PDD 应用注册一个应用程序键(App Key) . 3)下载 PDD API的SDK并掌 ...

  9. lol数据英雄联盟接口LOL接口电竞api开发比分网分享@

    英雄联盟数据LOL接口电竞api开发比分网分享@TOC 数据来自marz数据alan@marzesport.com 各大赛区的lol数据都有 1.获取赛事 接口:{{host1}}/api/serie ...

最新文章

  1. 清华唐杰:GPT-3表示能力已经接近人类了
  2. 鸿蒙系统今日发布 中国人自己的操作系统,鸿蒙系统今日发布,中国人自己的操作系统...
  3. Notepad++ 添加Json格式化插件
  4. MySQL 和 PostgreSQL 对比
  5. [Ceoi2016|BZOJ4936] Match
  6. 每天一个linux命令--定时启动
  7. {{view 视图层}}微信小程序
  8. Identity Server 4 - Hybrid Flow - Claims
  9. JS学习笔记1-JavaScript 输出
  10. OSPF——NSSA区域及完全NSSA区域(含配置命令)、第七类LSA类型 LSA-7详解
  11. 三维重建_基于图像的三维模型重建_稠密点云重建
  12. 为什么这么好玩?领略《塞尔达传说:旷野之息》精妙设计(上)
  13. 【笔记总结】高中英语——其二:名词性从句
  14. 1487 C. Minimum Ties
  15. 华硕笔记本升级固态SSD过程
  16. 代码不朽笔记: 编写简单的代码单元
  17. trunc()用法和add_months()
  18. STM32F103RBT6 串口1正常接收,发送过程也很正常,但TXD引脚没有波形
  19. Pandas 数据挖掘 分析
  20. 【JAVASE】Java泛型实例化

热门文章

  1. java-net-php-python-jspm宅急送管理系统计算机毕业设计程序
  2. 编程基础:计算机相关知识
  3. 编程黑科技:能玩几个小时的《魂斗罗》居然只有128KB
  4. RPMBUILD 打包
  5. sap服务器数据库配置文件,安装和配置 SAP 和数据库
  6. solarwinds安装升级NPM和其他Orion平台产品
  7. 如何将SpringBoot项目部署到阿里云Linux服务器中
  8. bzoj4479: [Jsoi2013]吃货jyy 欧拉回路+状态压缩Dp
  9. pulsar在docker-compose中运行及超级管理API使用
  10. 罗斯蒙特流量计电极的维护