商城API开发之下单接口

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

前言:

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


我的订单

订单详细

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

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

下单接口流程

  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代表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. 订单保存

订单保存分两步走:

  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;
    }

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

以上

相关文章
|
3天前
|
API 开发工具 数据库
开发一份API接口,需要注意这些,看你做到了几项
本文介绍了设计API接口时需注意的关键点,包括数字签名、敏感数据加密与脱敏、限流、参数校验、统一返回与异常处理、请求日志记录、幂等设计、数据量限制、异步处理、参数定义、完整文档及开发者对接SDK等内容,旨在帮助开发者设计出安全、稳定、易维护的API接口。
34 6
开发一份API接口,需要注意这些,看你做到了几项
|
10天前
|
JSON 安全 API
如何使用Python开发API接口?
在现代软件开发中,API(应用程序编程接口)用于不同软件组件之间的通信和数据交换,实现系统互操作性。Python因其简单易用和强大功能,成为开发API的热门选择。本文详细介绍了Python开发API的基础知识、优势、实现方式(如Flask和Django框架)、实战示例及注意事项,帮助读者掌握高效、安全的API开发技巧。
37 3
如何使用Python开发API接口?
|
6天前
|
存储 SQL API
探索后端开发:构建高效API与数据库交互
【10月更文挑战第36天】在数字化时代,后端开发是连接用户界面和数据存储的桥梁。本文深入探讨如何设计高效的API以及如何实现API与数据库之间的无缝交互,确保数据的一致性和高性能。我们将从基础概念出发,逐步深入到实战技巧,为读者提供一个清晰的后端开发路线图。
|
5天前
|
JSON 前端开发 API
后端开发中的API设计与文档编写指南####
本文探讨了后端开发中API设计的重要性,并详细阐述了如何编写高效、可维护的API接口。通过实际案例分析,文章强调了清晰的API设计对于前后端分离项目的关键作用,以及良好的文档习惯如何促进团队协作和提升开发效率。 ####
|
3天前
|
JSON API 数据格式
如何使用Python开发1688商品详情API接口?
本文介绍了如何使用Python开发1688商品详情API接口,获取商品的标题、价格、销量和评价等详细信息。主要内容包括注册1688开放平台账号、安装必要Python模块、了解API接口、生成签名、编写Python代码、解析返回数据以及错误处理和日志记录。通过这些步骤,开发者可以轻松地集成1688商品数据到自己的应用中。
18 1
|
4天前
|
JSON API 数据格式
淘宝 / 天猫官方商品 / 订单订单 API 接口丨商品上传接口对接步骤
要对接淘宝/天猫官方商品或订单API,需先注册淘宝开放平台账号,创建应用获取App Key和App Secret。之后,详细阅读API文档,了解接口功能及权限要求,编写认证、构建请求、发送请求和处理响应的代码。最后,在沙箱环境中测试与调试,确保API调用的正确性和稳定性。
|
6天前
|
监控 搜索推荐 安全
探究亚马逊详情API接口:开发与应用
在数字化时代,亚马逊作为全球领先的电商平台,为商家和消费者提供了丰富的商品信息和便捷的购物体验。本文深入探讨了亚马逊详情API接口的获取与运用,帮助开发者和商家实时监控商品数据、分析市场趋势、优化价格策略、分析竞争对手、构建推荐系统及自动化营销工具,从而在竞争中占据优势。文章还提供了Python调用示例和注意事项,确保API使用的安全与高效。
27 3
|
9天前
|
JSON BI API
商城上货API接口的实战案例
在商城上货过程中,API接口扮演着至关重要的角色。以下是对商城上货API接口的实战分析,涵盖其主要功能、类型、安全性以及实战案例等方面。
|
10天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
32 4
|
9天前
|
XML JSON API
【PHP开发专栏】PHP RESTful API设计与开发
随着互联网技术的发展,前后端分离成为Web开发的主流模式。本文介绍RESTful API的基本概念、设计原则及在PHP中的实现方法。RESTful API是一种轻量级、无状态的接口设计风格,通过HTTP方法(GET、POST、PUT、DELETE)操作资源,使用JSON或XML格式传输数据。在PHP中,通过定义路由、创建控制器、处理HTTP请求和响应等步骤实现RESTful API,并强调了安全性的重要性。
17 2

热门文章

最新文章