# 第九章 后端订单管理

通过前面章节的学习,我们实现了对商品和运营模块的管理,有了这些功能以后我们就能为线上APP的基础业务提供支持了,这时候想必已经有源源不断的订单开始产生了,那么问题就来了,老板想知道每天成交了多少订单,营业额有多少,业务部门的小伙伴需要处理订单发货,售后部门的小姐姐需要跟踪订单信息等等。看到这里读者应该可以很明显可以感觉到,前面章节我们更像是在对一些基础资料的管理,做一些普通CURD(增删改查),很多读者已经开始厌倦那枯燥无味的开发体验了,好消息是,从本章节开始,我们除了CURD(黑人问号.jpg),还会涉及到一些跟订单业务相关的实用功能模块编写,具体是什么暂不透露,毕竟我们还是要先从CURD开始。

在正式开始之前,我们需要再次为zerg数据库中导入一些用于测试用的数据,百度网盘:https://pan.baidu.com/s/1GEw_6Wv_QLT2l7ZLx4tPuw 密码:kpem

导入其中的order表,已购买《ThinkPHP5.0+小程序商城构建全栈应用》视频课程并产生过订单数据的同学可以略过此步。

# 分页查询所有订单

要管理订单自然就得先看到订单,让我们首先来实现一个查询所有订单的接口,这里同样需要实现分页查询,别说不用,被老板听到会不高兴。在控制层下面新增一个Order控制类,并新增一个getOrders()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**
     * 分页查询所有订单记录
     */
    public function getOrders()
    {
        $params = Request::get();
        $orders = OrderModel::getOrdersPaginate($params);
        if ($orders['total_nums'] === 0) {
            throw new OrderException([
                'code' => 404,
                'msg' => '未查询到相关订单',
                'error_code' => '70007'
            ]);
        }
        return $orders;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

控制层内方法的实现和前面我们分页查询商品时一模一样,获取参数,查询,查询不到数据时抛出一个异常,这里读者记得自行创建OrderException自定义异常类。通过查看数据库,我们知道订单记录存放于order表中,所以这里我们需要创建一个Order模型类来映射数据库中的order表,并在模型类中定义一个模型方法getOrdersPaginate(),把控制层方法中获取到的GET参数传递进去:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time'];
    // 告诉模型这个字段是json格式的数据
    protected $json = ['snap_address', 'snap_items'];
    // 设置json数据返回时以数组格式返回
    protected $jsonAssoc = true;


    public static function getOrdersPaginate($params)
    {
        // 存放数组查询条件的数组
        $query = [];
        // 插入一个按订单创建时间范围查询的数组查询条件
        $query[] = ['create_time', 'between time', [$params['start'], $params['end']]];
        // 判断是否传递了订单号查询条件
        if (array_key_exists('order_no', $params)) {
            $query[] = ['order_no', '=', $params['order_no']];
        }
        // 判断是否传递按收货人姓名的查询条件
        if (array_key_exists('name', $params)) {
            $query[] = ['snap_address->name', '=', $params['name']];
        }
        // paginate()方法用于根据url中的参数,计算查询要查询的开始位置和查询数量
        list($start, $count) = paginate();
        // 应用条件查询
        $orderList = self::where($query);
        // 调用模型的实例方法count计算该条件下会有多少条记录
        $totalNums = $orderList->count();
        // 调用模型的limit方法对记录进行分页并获取查询结果
        $orderList = $orderList->limit($start, $count)
            // 排序
            ->order('create_time desc')
            ->select();
        // 组装返回结果
        $result = [
            'collection' => $orderList,
            'total_nums' => $totalNums
        ];

        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

相比之前其他模型类里的实现,这里多了一些陌生的参数和代码,首先模型中的两个成员变量,$json$jsonAssoc,这是因为在数据库中order表的snap_addresssnap_items字段存储的数据是json格式,我们需要通过模型的$json变量来告诉框架这两个字段存储的是json数据,这样后续如果需要对这些字段做增删改查的时候就可以像操作普通数据那样利用框架的模型类来操作,不然我们就需要先读取然后转换格式再做模型操作。$jsonAssoc用于设置让框架在输出$json中声明的字段时以数组格式返回,这样也是为了方便我们在查询出数据后操作。

如果不加$jsonAssoc这个参数,而且不做格式转换就直接返回给前端,你会发现存储了json数据的字段输出结果时会有很多转义字符,这是因为lin-cms-tp5默认会将所有返回前端的数据都做一次json序列化把数组变成json格式,如果数据本来就是json格式的,再做一次json序列化就会多出很多转义字符。

解释完两个陌生的变量之后,让我们来看看下面的一坨代码:

        // 查询条件构成的数组
        $query = [];
        // 插入一个按订单创建时间查询的查询条件
        $query[] = ['create_time', 'between time', [$params['start'], $params['end']]];
        // 判断是否传递了订单号查询条件
        if (array_key_exists('order_no', $params)) {
            $query[] = ['order_no', '=', $params['order_no']];
        }
        // 判断是否传递按收货人姓名的查询条件
        if (array_key_exists('name', $params)) {
            $query[] = ['snap_address->name', '=', $params['name']];
        }
1
2
3
4
5
6
7
8
9
10
11
12

这一坨代码主要是用于构造一个数组查询条件,如果order_no或者name参数存在于当前请求的参数中就代表需要进一步的条件查询,构造好的数组查询条件后面会传递给模型的where()方法实现条件查询,这里代码可以实现我们目前的需求,即在查询的时候,可以选择性的传入收货人姓名或者订单号来查询指定的订单,但是仔细想一想,如果售后部门的小姐姐们现在需要增加其他查询条件,那我们没理由不满足她,实现的方式也是很简单,就是继续加if判断,但是,加着加着你就会发现这个模型方法里面会充斥着很多if代码块,可读性很差。再者我们通过仔细观察这些if语句,其实里面做的事情本质上是一样的,就是字段名不一样,而且像这种条件查询,我们几乎在任何中查询都可能会用到,我们每次都去写这种if语句属于无意义的重复劳动(除非你的公司以代码行数做KPI),这时候我们就要考虑来封装一下了。还记得前面我们在BaseModel中封装了一个拼接图片url地址的方法,模型类通过继承实现了方法复用,这里我们已经继承了BaseModel,那么我们就在父类中来实现一个自动构造数组查询条件的方法,打开模型层下的BaseModel类,新增一个静态方法equalQuery(),并加入以下代码:

<?php


namespace app\api\model;


use think\Model;

class BaseModel extends Model
{

    protected function prefixImgUrl($value, $data){...}

    /**
     * 构造条件为相等的数组查询条件
     * @param $field 要检索的参数名数组
     * @param $params 前端提交过来的所有GET参数数组
     * @return array 构造好后的查询条件
     */
    protected static function equalQuery($field, $params)
    {
        $query = [];
        foreach ($field as $value) {
            if (is_array($value)) {
                if (array_key_exists($value[0], $params)) {
                    $query[] = [$value[1], '=', $params[$value[0]]];
                }
            } else {
                if (array_key_exists($value, $params)) {
                    $query[] = [$value, '=', $params[$value]];
                }
            }
        }
        return $query;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

equalQuery方法接收两个参数,在方法内,我们首先遍历一下$field,拿到每一个需要判断是否存在的参数名,如果存在,就构造一个数组查询条件并插入$query数组中,这里我们还会判断一下$field的元素是不是一个数组用于兼容json数据查询条件的构造,最后把$query返回回去。

更多关于where()数组条件查询介绍点击查看、TP5 JSON数据介绍点击查看

接下来看看模型方法里是怎么调用的:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time'];
    // 告诉模型这个字段是json格式的数据
    protected $json = ['snap_address', 'snap_items'];
    // 设置JSON数据返回数组
    protected $jsonAssoc = true;


    public static function getOrdersPaginate($params)
    {
        // 需要判断是否存在的参数名。
        $field = ['order_no', ['name', 'snap_address->name']];
        // 把需要判断的参数名数组和当前请求获取到的参数数组传递进去,返回构造好的数组查询条件
        $query = self::equalQuery($field, $params);
        // 插入一个按订单创建时间查询的查询条件
        $query[] = ['create_time', 'between time', [$params['start'], $params['end']]];

        // 判断是否传递了订单号查询条件
        // if (array_key_exists('order_no', $params)) {
        //     $query[] = ['order_no', '=', $params['order_no']];
        // }
        // // 判断是否传递按收货人姓名的查询条件
        // if (array_key_exists('name', $params)) {
        //     $query[] = ['snap_address->name', '=', $params['name']];
        // }
        
        .....................................................
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里我们先把原来的if()语句都注释掉,因为已经不需要了。在使用equalQuery()方法前我们定义一个了$field变量,变量值是一个数组,数组中的元素就是那两个我们需要判断是否存在的参数名。

由于json数据查询的构造语句比较特殊,所以这里我们给$field数组的第二个元素传递了一个数组,数组内第一个元素代表参数名,第二元素代表要查询的json属性

这样子,以后无论需要增加什么字段值相等的查询条件,我们只需要把对应的参数名添加到$field数组中,equalQuery方法就会自动帮我们遍历并构造数组查询条件,解决完可选参数的问题,我们把目光又停留在了这个时间条件上。在真实业务场景中,按指定时间范围查询也是一个必不可少的的功能,这里我们同样存在一个问题就是每次都要先格式化下查询条件(本专栏当前的场景下),然后构造一个数组查询条件,我们是不是也可以把这类按时间范围查询的查询条件构造过程也封装成一个方法呢?答案是可以的,打开我们的BaseModel类,我们再新增一个静态方法betweenTimeQuery()

<?php


namespace app\api\model;


use think\Model;

class BaseModel extends Model
{

    protected function prefixImgUrl($value, $data){...}

    /** 构造查询条件相等的查询语句*/
    protected static function equalQuery($field, $params){...}

    /**
     * @param $startField 开始时间的参数名
     * @param $endField 结束时间的参数名
     * @param $params  前端提交过来的所有GET参数数组
     * @param string $dbField 要查询的表字段名,默认是create_time
     * @return array
     */
    protected static function betweenTimeQuery($startField, $endField, $params, $dbField = 'create_time')
    {
        $query = [];
        if (array_key_exists($startField, $params) && array_key_exists($endField, $params)) {
            if (!empty($params[$startField]) && !empty($params[$endField])) {
                $query = array($dbField, 'between time', array($params[$startField], $params[$endField]));
            }
        }
        return $query;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

实现的方式与前面的equalQuery()大同小异,这里我们先判断传入的参数名存不存在于当前请求提交过来的参数中,如果有就构造一个数组查询条件,我们还给这个方法提供第三个可选参数$dbField,这是因为有时候我们可能会想要根据其他类型的时间来查询,这里默认按表记录的创建时间,最后把$query返回回去,接下来看看模型方法里是怎么调用的:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time'];
    // 告诉模型这个字段是json格式的数据
    protected $json = ['snap_address', 'snap_items'];
    // 设置JSON数据返回数组
    protected $jsonAssoc = true;


    public static function getOrdersPaginate($params)
    {
         // 查询条件构成的数组
        $field = ['order_no', ['name', 'snap_address->name']];
        $query = self::equalQuery($field, $params);
        $query[] = self::betweenTimeQuery('start', 'end', $params);

        // 插入一个按订单创建时间查询的查询条件
        // $query[] = ['create_time', 'between time', [$params['start'], $params['end']]];

        // 判断是否传递了订单号查询条件
        // if (array_key_exists('order_no', $params)) {
        //     $query[] = ['order_no', '=', $params['order_no']];
        // }
        // // 判断是否传递按收货人姓名的查询条件
        // if (array_key_exists('name', $params)) {
        //     $query[] = ['snap_address->name', '=', $params['name']];
        // }
        
        .....................................................
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

这里我们把原来格式化时间条件和构造数组查询条件的语句都注释掉了,直接调用我们刚刚封装好的betweenTimeQuery()方法,现在的getOrdersPaginate()方法完整是这样的:

<?php


namespace app\api\model;


class Order extends BaseModel
{
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time'];
    // 告诉模型这个字段是json格式的数据
    protected $json = ['snap_address', 'snap_items'];
    // 设置JSON数据返回数组
    protected $jsonAssoc = true;


    public static function getOrdersPaginate($params)
    {
        $field = ['order_no', ['name', 'snap_address->name']];
        $query = self::equalQuery($field, $params);
        $query[] = self::betweenTimeQuery('start', 'end', $params);
        // paginate()方法用于根据url中的参数,计算查询要查询的开始位置和查询数量
        list($start, $count) = paginate();
        // 应用条件查询
        $orderList = self::where($query);
        // 调用模型的实例方法count计算该条件下会有多少条记录
        $totalNums = $orderList->count();
        // 调用模型的limit方法对记录进行分页并获取查询结果
        $orderList = $orderList->limit($start, $count)
            ->order('create_time desc')
            ->select();
        // 组装返回结果
        $result = [
            'collection' => $orderList,
            'total_nums' => $totalNums
        ];

        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

对比之前的代码简洁了许多,同时我们封装的两个构造数组查询条件的方法在后面还会反复用到,可以大大精简我们的代码量和减少重复工作。在构造完所有查询条件之后,我们就可以把$query直接传入模型的where()方法了,这样模型就会根据我们的查询条件做相应的条件查询了。后面的代码就与前面我们做分页查询商品数据时一样,获取总记录数量和分页查询并拼装返回结果。

为什么作者会知道生成一个这样的数组就可以实现多字段多条件的where查询,没有为什么,就是看官方的开发文档,了解这个where方法支持什么调用方式觉得合适就拿来用,所以看开发文档很重要,你要先知道功能支持什么才有自由发挥的机会。

模型方法定义完之后,让我们回到控制器方法中:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**
     * 分页查询所有订单记录
     * @validate('OrderForm')
     */
    public function getOrders()
    {
        $params = Request::get();
        $orders = OrderModel::getOrdersPaginate($params);
        if ($orders['total_nums'] === 0) {
            throw new OrderException([
                'code' => 404,
                'msg' => '未查询到相关订单',
                'error_code' => '70007'
            ]);
        }
        return $orders;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里我们别忘了给这个控制器方法加上参数校验,使用注解验证器声明要使用OrderForm自定义验证器类,接着在验证层目录下新建一个order子目录,在子目录下新增一个OrderForm类并添加如下代码:

<?php


namespace app\api\validate\order;


use LinCmsTp5\validate\BaseValidate;

class OrderForm extends BaseValidate
{
    protected $rule = [
        'page' => 'require|number',
        'count' => 'require|number|between:1,15',
        'start|开始时间' => 'require|date',
        'end|结束时间' => 'require|date',
        'name|收货人姓名' => 'chs',
        'order_no|商品订单号' => 'alphaNum|length:16'
    ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里我们使用了一些TP框架内置的验证规则来对参数进行校验,为了更好的提示,我们在部分参数名后面加了|中文描述,这个作用是让验证器在返回验证不通过的信息时,以|后面的中文替代参数名,比如这里如果start参数验证不通过,提示信息会变成开始时间不能为空,如果不加就是提示start不能为空。在定义完控制器方法和模型之后,就需要给这个方法添加一条路由规则了,打开route.php文件,在v1路由分组下新增一个order路由分组,在order分组下添加一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        Route::group('order', function () {
            // 分页查询所有订单
            Route::get('', 'api/v1.Order/getOrders');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

这里我们给定一个时间范围,注意TP的时间查询支持多种格式,框架会自动识别,可以是标准日期的字符串也可以是时间戳,然后查询第0页的2条数据,点击发送:

{
    "collection": [
        {
            "id": 20,
            "order_no": "C903906920662485",
            "user_id": 62,
            "create_time": "2019-09-03 14:04:52",
            "total_price": "0.02",
            "status": 3,
            "snap_img": "http://chanmeifei.cn/course/images/product-tea@1.png",
            "snap_name": "红袖枸杞 6克*3袋等",
            "total_count": 2,
            "update_time": "2019-09-06 21:07:16",
            "snap_items": [
                {
                    "id": 4,
                    "name": "红袖枸杞 6克*3袋",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-tea@1.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                },
                {
                    "id": 6,
                    "name": "小红的猪耳朵 120克",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-cake@2.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                }
            ],
            "snap_address": {
                "name": "沁雪",
                "mobile": "18824908020",
                "province": "广东省",
                "city": "广州市",
                "country": "海珠区",
                "detail": "烟雨路9号",
                "update_time": "1970-01-01 08:00:00"
            },
            "prepay_id": null
        },
        {
            "id": 19,
            "order_no": "C903906634023384",
            "user_id": 62,
            "create_time": "2019-09-03 14:04:23",
            "total_price": "0.02",
            "status": 1,
            "snap_img": "http://chanmeifei.cn/course/images/product-rice@2.png",
            "snap_name": "绿豆 125克等",
            "total_count": 2,
            "update_time": "2019-09-03 14:04:23",
            "snap_items": [
                {
                    "id": 13,
                    "name": "绿豆 125克",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-rice@2.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                },
                {
                    "id": 14,
                    "name": "芝麻 50克",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-rice@3.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                }
            ],
            "snap_address": {
                "name": "沁雪",
                "mobile": "18824908020",
                "province": "广东省",
                "city": "广州市",
                "country": "海珠区",
                "detail": "烟雨路9号",
                "update_time": "1970-01-01 08:00:00"
            },
            "prepay_id": null
        }
    ],
    "total_nums": 20
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

没有报错,这里我们再试试增加一个查询条件,就是name为沁尘的订单:

点击发送:

{
    "collection": [
        {
            "id": 16,
            "order_no": "C902915874689080",
            "user_id": 58,
            "create_time": "2019-09-02 10:33:07",
            "total_price": "0.01",
            "status": 1,
            "snap_img": "http://chanmeifei.cn/course/images/product-dryfruit@2.png",
            "snap_name": "春生龙眼 500克",
            "total_count": 1,
            "update_time": "2019-09-02 10:33:07",
            "snap_items": [
                {
                    "id": 5,
                    "name": "春生龙眼 500克",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-dryfruit@2.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                }
            ],
            "snap_address": {
                "name": "沁尘",
                "mobile": "18824908371",
                "province": "广东省",
                "city": "广州市",
                "country": "海珠区",
                "detail": null,
                "update_time": "1970-01-01 08:00:00"
            },
            "prepay_id": null
        },
        {
            "id": 15,
            "order_no": "C902915748696636",
            "user_id": 58,
            "create_time": "2019-09-02 10:32:54",
            "total_price": "0.01",
            "status": 1,
            "snap_img": "http://chanmeifei.cn/course/images/product-dryfruit@2.png",
            "snap_name": "春生龙眼 500克",
            "total_count": 1,
            "update_time": "2019-09-02 10:32:54",
            "snap_items": [
                {
                    "id": 5,
                    "name": "春生龙眼 500克",
                    "main_img_url": "http://chanmeifei.cn/course/images/product-dryfruit@2.png",
                    "count": 1,
                    "totalPrice": 0.01,
                    "price": "0.01",
                    "counts": 1
                }
            ],
            "snap_address": {
                "name": "沁尘",
                "mobile": "18824908371",
                "province": "广东省",
                "city": "广州市",
                "country": "海珠区",
                "detail": null,
                "update_time": "1970-01-01 08:00:00"
            },
            "prepay_id": null
        }
    ],
    "total_nums": 16
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

没有报错,这里只查询出了在这个时间范围内收货人姓名为沁尘的订单。在真实业务场景中,除了常规的查询指定时间范围的订单记录,往往售后或者客服部门在处理用户咨询或者投诉的时候需要根据用户提供的订单号或者姓名在后台中查询相应订单以便开展下一步工作,通过本小节的学习实践,我们就为这些需求的实现打下了一个坚实基础,接下来我们将从各种业务场景出发,在接口返回的结果上开展各种业务操作。

# 订单发货

在上一小节中,我们实现了查询所有订单的功能,在返回的数据中我们可以看到每条记录中的status属性的值都不尽相同,有些是1,有些是2等等,这里的1和2就是一个订单状态的枚举值,每个枚举值都有对应的含义,通过查看数据库中order表的status字段注释,我们可以知道:1:未支付, 2:已支付,3:已发货 , 4: 已支付,但库存不足。那么本小节我们要实现的功能就是给那些已支付或者已支付但库存不足的(假设在其后某个时间把货品补齐了)进行发货,毕竟收了钱就要发货对吧?要实现发货其实很简单,就是通过模型方法,把对应的订单记录状态值修改成已发货状态即可,但这里为了更加贴近真实的业务场景,我们决定在此基础上再扩展延伸一下。在真实业务场景中,发货意味着同时会产生物流信息,商家把商品交给物流公司,然后物流公司再送到客户的手上,那么在订单商品运输期间客户可能会对该订单产生一些售后服务需求。我们的售后部门小姐姐除了需要操作发货,还需要跟踪核实订单商品的物流情况,可能某一天就有个人客人打电话来投诉:“五年,你知道我这五年怎么过的吗?你知道吗?每天在家等快递!”所以,本小节要实现的订单发货接口,我们打算做两件事情:1.修改订单状态;2.生成对应订单的发货记录。其中第2点生成的发货记录可用于内部追溯业务操作情况,还可以用于为后续查询订单物流信息提供一些必要的参数(物流公司编码、物流单号)。明确了要实现什么功能之后,让我们来动手实现一下,在控制层下的Order控制器类下面新增一个deliverGoods()控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}

    /**
     * 订单发货
     * @auth('订单发货','订单管理')
     * @param('id','订单id','require|number')
     * @param('comp','快递公司编码','require|alpha')
     * @param('number','快递单号','require|alphaNum')
     */
    public function deliverGoods($id)
    {
        $params = Request::post();
        
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在实现业务逻辑之前,我们先实现点常规代码,首先是给这个控制器方法加上注解权限控制,毕竟发货这事不能随便谁都可以。然后我们给这个接口添加了注解参数校验,我们要求调用这个接口时,必须传递的3个参数并利用TP5内置验证规则对参数值的内容做了一些限制。前面我们给这个接口定了两个小目标,接下来要做的事就是实现它,考虑到发货这件事涉及到两个子任务(修改订单状态、生成发货记录)的实现,这里我们就来给发货这个事情封装一个方法,在项目根目录下的application\api目录下,新建一个service目录,这里我们给他划分一下,叫服务层。在service目录中,新建一个Order类并加入以下代码:

<?php


namespace app\api\service;

use app\api\model\Order as OrderModel;
use app\lib\exception\order\OrderException;

class Order
{
    private $order;

    public function __construct($orderId)
    {
        // 根据传入的id查询出对应记录,拿到对应记录的模型实例
        $order = OrderModel::get($orderId);
        if (!$order) Throw new OrderException(['code' => 404, 'msg' => '指定的订单不存在']);
        $this->order = $order;
    }

    /**
     * @param string $company 快递公司编码
     * @param string $number 快递单号
     * @return bool
     */
    public function deliverGoods($company, $number)
    {
        
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

service(服务层)目录下的Order服务类用于提供和订单有关的服务功能,考虑到跟订单有关的服务功能都是与具体某个订单有关的,为了不用重复写订单查询和空记录异常处理的代码,我们给这个Order服务类定义了一个构造方法,要求在实例化这个类时必须传入一个订单id,在构造方法中,我们调用Order模型类查询这个id的订单记录,并把结果赋值给成员变量$order。接着我们定义了一个deliverGoods()方法用于实现订单发货的服务功能,这个方法接收两个参数,一个是快递公司的编码,另一个是快递单号,方法内,首先我们要先判断一下实例化Order服务类时传入的订单id所对应的订单状态是否是已支付或者已支付但库存不足的状态:

<?php


namespace app\api\service;

use app\api\model\Order as OrderModel;
use app\lib\exception\order\OrderException;

class Order
{
    private $order;

    public function __construct($orderId){...}

    /**
     * @param string $company 快递公司编码
     * @param string $number 快递单号
     * @return bool
     */
    public function deliverGoods($company, $number)
    {
        // 判断订单的状态是否是已支付或者已支付但库存不足的状态
        if ($this->order->status !== 2 && $this->order->status !== 4) {
            Throw new OrderException(['msg' => '当前订单不允许发货,请检查订单状态', 'error_code' => '70008']);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这里我们通过$this->order来拿到构造方法中查询到的订单模型实例,然后判断实例里面的status属性值是否是2或者4,逻辑很简单,但这里if语句乍一看有点懵。前面我们通过status字段注释可以知道1:未支付, 2:已支付,3:已发货 , 4: 已支付,但库存不足,但这仅限于表字段刚好写了注释的情况下,假如设计数据库的同学没有写字段注释的习惯或者忘记了然后跑路了,这时候你突然接手了这个项目,这方法里的第一个if语句就可以把你卡主了。所以类似于这种有枚举值的情况,我们不能直接就使用表字段值的枚举值来判断,原因有两点:第一,可读性差,在没有查看表字段注释或者文档的情况下是无法理解2和4在这里分别代表什么意思,即便是最熟悉的0和1,在某些情况下也有特别的含义(嘿嘿嘿);第二,可维护性差,当枚举值的含义发生改变时,如果你的项目中已经大量直接使用了旧的枚举值,那么修改起来就会很难受。所以我们需要在一个统一的地方用一系列常量来映射订单状态的枚举值,这样我们可以直接通过调用常量来取得对应的枚举值,同时因为常量名是可以自定义的,我们可以给常量起个辨识度高的名字;如果枚举值有调整,我们也只需要在常量定义的地方修改其常量的值即可。现在我们就来定义一下这些常量,在项目根目录下的application\lib目录下新建一个enum文件夹,在enum文件下新建一个OrderStatusEnum类并加入以下代码:

<?php

namespace app\lib\enum;


class OrderStatusEnum
{
    // 待支付
    const UNPAID = 1;

    // 已支付
    const PAID = 2;

    // 已发货
    const DELIVERED = 3;

    // 已支付,但库存不足
    const PAID_BUT_OUT_OF = 4;

    // 已处理PAID_BUT_OUT_OF
    const HANDLED_OUT_OF = 5;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这里我们定义了5个常量,常量的值与order表中status字段的注释说明保持一致,定义好这几个常量之后,我们就来使用一下,回到Order服务类下的deliverGoods()方法,修改一下原来的if语句:

<?php


namespace app\api\service;

use app\api\model\Order as OrderModel;
use app\lib\exception\order\OrderException;
use app\lib\enum\OrderStatusEnum; // 注意命名空间的引入


class Order
{
    private $order;

    public function __construct($orderId){...}

    /**
     * @param string $company 快递公司编码
     * @param string $number 快递单号
     * @return bool
     */
    public function deliverGoods($company, $number)
    {
        // 判断订单的状态是否是已支付或者已支付但库存不足的状态
        if ($this->order->status !== OrderStatusEnum::PAID && $this->order->status !== OrderStatusEnum::PAID_BUT_OUT_OF) {
            Throw new OrderException(['msg' => '当前订单不允许发货,请检查订单状态', 'error_code' => '70008']);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里我们首先引入了OrderStatusEnum枚举类(PHP没有枚举类这种东西,我们以类的形式模拟实现),然后通过::的方式调用一下我们刚刚定义了的几个常量,这里常量值就分别是2和4,通过这种方式,这里的if语句的含义就很好理解了。在做完订单状态的判断之后,如果订单是允许发货的情况,那接下来我们就需要来创建一条发货记录和改变订单状态了,因为要创建发货记录,所以这里我们还需要先在数据库中新增一张专门存放发货记录的表,这个表作者已经为大家准备好了,读者可以访问百度网盘:https://pan.baidu.com/s/1GEw_6Wv_QLT2l7ZLx4tPuw 密码:kpem 下载并导入deliver_record.sqlzerg数据库中。有了表之后,我们自然就需要创建一个模型来映射这张表,在模型层下新增一个DeliverRecord模型类:

<?php


namespace app\api\model;


class DeliverRecord extends BaseModel
{
    // 开启自动写入时间戳
    public $autoWriteTimestamp = true;
    // 隐藏字段显示
    protected $hidden = ['update_time'];

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

表和模型都准备好了之后,让我们回到Order服务类下的deliverGoods()方法:

<?php


namespace app\api\service;

use app\api\model\Order as OrderModel;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use app\lib\enum\OrderStatusEnum;
use app\lib\token\Token;
use think\Db;
use think\Exception;


class Order
{
    private $order;

    public function __construct($orderId){...}

    /**
     * @param string $company 快递公司编码
     * @param string $number 快递单号
     * @return bool
     */
    public function deliverGoods($company, $number)
    {
        // 判断订单的状态是否是已支付或者已支付但库存不足的状态
        if ($this->order->status !== OrderStatusEnum::PAID && $this->order->status !== OrderStatusEnum::PAID_BUT_OUT_OF) {
            Throw new OrderException(['msg' => '当前订单不允许发货,请检查订单状态', 'error_code' => '70008']);
        }
        // 启动事务
        Db::startTrans();
        try {
            // 创建一条发货单记录
            DeliverRecordModel::create([
                'order_no' => $this->order->order_no,
                'comp' => $company,
                'number' => $number,
                'operator' => Token::getCurrentName()
            ]);
            // 改变订单状态
            $this->order->status = OrderStatusEnum::DELIVERED;
            // 调用模型sava()方法更新记录
            $this->order->save();
            // 提交事务
            Db::commit();
            return true;
        } catch (Exception $ex) {
            // 回滚事务
            Db::rollback();
            throw new OrderException(['msg' => '订单发货不成功', 'error_code' => '70009']);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

这里我们启用了一个事务来保证订单状态修改和生成发货记录这两个数据库操作的一致性,事务里面首先使用DeliverRecord模型创建了一条发货记录,包含了商品订单号、快递公司编码、快递单号,发货人。这里的发货人其实就是请求了订单发货接口的CMS用户,这里要拿到这个用户的信息也很简单,因为订单发货的接口是有接口权限控制的需要校验令牌,在请求携带的令牌中就包含了当前请求用户的信息,所以这里我们利用lin-cms-tp5令牌类里提供的一个方法getCurrentName()获取下当前请求用户的用户名并赋值给operator字段。在创建完发货单记录之后,给$this->order的status属性赋值,然后调用save()方法执行更新操作。至此,我们的两个小目标就全部实现了,接下来让我们回到控制层中来调用一下这个Order服务类:

<?php


namespace app\api\controller\v1;

use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\lib\exception\order\OrderException;
use think\facade\Request;

class Order
{
    /**分页查询所有订单*/
    public function getOrders(){...}

    /**
     * 订单发货
     * @auth('订单发货','订单管理')
     * @param('id','订单id','require|number')
     * @param('comp','快递公司编码','require|alpha')
     * @param('number','快递单号','require|alphaNum')
     */
    public function deliverGoods($id)
    {
        $params = Request::post();
        // $orderService = new OrderService($id);
        // $result = $orderService->deliverGoods($params['comp'], $params['number']);
        // 简写为
        $result = (new OrderService($id))->deliverGoods($params['comp'], $params['number']);

        return writeJson(201, $result, '发货成功');
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里我们首先实例化了OrderService类并把前端提交过来的订单id传递进去,接着调用了服务类的下面的deliverGoods()方法,同样把相应的参数传递进去,控制器方法定义好之后,让我们来为这个方法定义一条路由路由,打开route.php,在order路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        ............................
        Route::group('order', function () {
            // 分页查询所有订单
            Route::get('', 'api/v1.Order/getOrders');
             // 订单发货
            Route::post('shipment/:id', 'api/v1.Order/deliverGoods');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

本接口涉及到接口权限控制,不要忘记在Postman中添加令牌

在测试之前提醒大家,订单发货接口提交的comp和number参数请务必提供真实有效的快递信息,否则会影响后续订单物流查询小节的学习,其中快递公司编码作者已经整理了一份,读者可以对照查阅,务必保持一致:

编码 名称 编码 名称
sf 顺丰 sto 申通
yt 圆通 yd 韵达
tt 天天 ems EMS
zto 中通 ht 汇通
qf 全峰 db 德邦
gt 国通 rfd 如风达
jd 京东 zjs 宅急送
youzheng 邮政快递 bsky 百世

这里我们尝试给订单id为2的订单发货,使用顺丰快递:

没有报错,提示发货成功了,那么按照我们前面在服务层中实现的逻辑,如果是已发货的,重复操作发货的话应该是会报错的,让我们再次点击发送:

{
    "msg": "当前订单不允许发货,请检查订单状态",
    "error_code": "70008",
    "request_url": "POST /v1/order/shipment/2"
}
1
2
3
4
5

报错了,说明我们的订单发货接口已经可以正常工作啦。

最后更新: 2020-08-02 03:50:30
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页