作者

处理订单号的问题

本节课程主要是在之前的小米商城基础上完善一个常见的小功能。之前我们创建订单的订单号使用的是订单表的 id 字段,正规的大型项目都不会这么做,所以需要写一个随机生成订单号的方法。

1、 在 orders 表中添加一个订单号字段 out_trade_noVARCHAR 类型,长度 255

2、在 app\Models\Shop\Order 模型中添加生成订单号的方法,代码如下:

/***
 * 生成订单号
 * @return bool|string
 * @throws \Exception
 */
public static function make_orderNo()
{
    // 订单流水号前缀
    $prefix = date('YmdHis');
    for ($i = 0; $i < 10; $i++) {
        // 随机生成 6 位的数字
        $no = $prefix . str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
        // 判断是否已经存在
        if (!static::query()->where('out_trade_no', $no)->exists()) {
            return $no;
        }
    }
    \Log::warning('find order no failed');

    return false;
}

3、在订单控制器里的创建订单逻辑中添加订单号的插入,部分代码如下:

// 往订单表插入数据
$order = Order::create([
    'out_trade_no' => Order::make_orderNo(),
    'customer_id' => session('wechat.customer.id'),
    'total_price' => $total_price
]);

这样,重新生成订单后,订单表会插入生成的订单号。

关闭未支付订单

上一节我们实现了创建订单的功能,在创建订单的同时我们减去了对应商品的库存,恶意用户可以通过下大量的订单又不支付来占用商品库存,让正常的用户因为库存不足而无法下单。因此我们需要有一个关闭未支付订单的机制,当创建订单之后一定时间内没有支付,将关闭订单并退回减去的库存。

对于这个需求我们可以用 Laravel 提供的延迟任务(Delayed Job)功能来解决。当我们的系统触发了一个延迟任务时,Laravel 会用当前时间加上任务的延迟时间计算出任务应该被执行的时间戳,然后将这个时间戳和任务信息序列化之后存入队列,Laravel 的队列处理器会不断查询并执行队列中满足预计执行时间等于或早于当前时间的任务。

1、先来加载订单确认页面

routes/web.php 中添加路由:

Route::prefix('order')->group(function () {
    .
    .
    Route::get('pay/{id}', 'OrderController@pay'); // 订单确认
});

当下单成功后,通过 js 跳转到订单确认页,在 checkout.blade.php 中:

@section('js')
    <script>
        $(function () {
            $('.ui-button').click(function () {
                var address_id = $('#address').data('id');
                if (address_id == '') {
                    alert('请选择一个收货地址');
                    return false;
                }
                $.ajax({
                    type: 'POST',
                    url: "/order",
                    data: {address_id: address_id},
                    success: function (data) {
                        if (data.status == 0) {
                            alert(data.info)
                            return false;
                        } else {
                            alert(data.info);
                            location.href = '/order/pay/' + data.order_id;
                        }
                    }
                })
            })
        })
    </script>
@endsection

OrderController 中增加对应方法,代码如下:

/***
 * 订单确认页
 * @param $id
 */
public function pay($id)
{
    /**
     * 第 1 步:查询订单并发送给前端
     */
    $order = Order::with('address')->find($id);

    return view('wechat.order.show_pay', compact('order'));
}

修改 show_pay.blade.php 页面代码:

@extends('layouts.wechat.app')

@section('content')
    <div id="wrapper">
        <div class="page-order-pay" data-log="在线支付">
            <div class="box box1">
                <div class="p1"><span class="icon-checked"></span><span>订单提交成功</span></div>
                <div class="p2">
                    <span style="color: #FF5722">请在30分钟内完成支付,超时订单将自动关闭。</span>
                </div>
                <div class="p2"><p class="count" style="color: #D92E2E"></p></div>
            </div>
            <div class="box box2">
                <div class="p">订单金额:{{$order->total_price}}元 &nbsp;&nbsp; 订单编号:{{$order->out_trade_no}}</div>
                <div class="p h_box">
                    <div>收货信息:</div>
                    <div class="flex_1">{{$order->address->name}} {{$order->address->tel}}
                        <br>{{$order->address->province}} {{$order->address->city}} {{$order->address->area}} {{$order->address->detail}}
                    </div>
                </div>
                <div class="p">发票类型:个人电子发票 <p>发票抬头:个人</p></div>
            </div>
            <div class="box box3">
                <div class="head"><span>请选择支付方式</span></div>
                <div class="list">
                    <div class="item">
                        <div data-log="A0-支付宝" class="inner">
                            <div class="p">支付宝</div>
                            <div class="p right">大额支付推荐使用支付宝快捷支付</div>
                        </div>
                    </div>
                    <div class="item active">
                        <div data-log="A1-小米钱包" class="inner">
                            <div class="p">微信</div>
                            <div class="p right">欢迎使用微信支付</div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="box box4">
                <div class="p p1">
                    <p>本次需支付:<span class="hot">{{$order->total_price}}元</span></p>
                </div>
            </div>
            <div class="box box5"><a href="javascript:;" data-log="bottom-bankgo"
                                     class="ui-button"><span>立即支付</span></a>
            </div>
        </div>
    </div>
@stop

@section('js')
    <script>
        window.onload = function () {
            countDown();

            function addZero(i) {
                return i < 10 ? "0" + i : i + "";
            }

            function countDown() {
                var nowtime = new Date();
                var endtime = new Date({!! json_encode($order->created_at) !!});
                // endtime.setSeconds(endtime.getSeconds() + 30); // 设置30秒
                endtime.setMinutes(endtime.getMinutes() + 30); // 设置30分钟
                var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
                var m = parseInt(lefttime / 60 % 60);
                var s = parseInt(lefttime % 60);
                m = addZero(m);
                s = addZero(s);
                document.querySelector(".count").innerHTML = `剩余支付时间:  ${m}${s} 秒`;
                if (lefttime <= 0) {
                    document.querySelector(".count").innerHTML = "订单已失效";
                    return;
                }
                setTimeout(countDown, 1000);
            }
        }
    </script>
@endsection

上面的这段 js 参考文章 https://blog.csdn.net/LightLinV/article/details/88602302

这里的 js 里面使用 {!! json_encode($order->created_at) !!} 来解析读取订单创建的时间,其实就是把控制器传过来的 $order->created_at 变量变成一个 JSON 字符串,赋值给 JSendtime 变量。

此时,正常步骤下单,出现的页面样式如下:

现在我们看到的时间其实是一个假的时间显示,并不能实现真正的功能。接下来,我们来实现超时未支付关闭订单的功能。

首先我们在 orders 表中增加一个字段 closedTINYINT 类型,长度 1 ,默认值 1。其中 1 代表正常订单,0 代表失效订单。

2、创建任务

我们通过 make:job 命令来创建一个任务:

php artisan make:job CloseOrder

创建的任务类保存在 app/Jobs 目录下,编辑 CloseOrder.php 中添加代码:

<?php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Models\Shop\Order;

class CloseOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $order;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Order $order, $delay)
    {
        $this->order = $order;
        // 设置延迟的时间,delay() 方法的参数代表多少秒之后执行
        $this->delay($delay);
    }

    /**
     * 定义这个任务类具体的执行逻辑
     * 当队列处理器从队列中取出任务时,会调用 handle() 方法
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // 判断对应的订单是否已经被支付
        // 如果已经支付则不需要关闭订单,直接退出
        if ($this->order->pay_time) {
            return;
        }
        // 通过事务执行 sql
        \DB::transaction(function () {
            // 将订单的 closed 字段标记为 false,即关闭订单
            $this->order->update(['closed' => false]);
            // 循环遍历订单中的商品,将订单中的数量加回到商品的库存中去
            foreach ($this->order->order_products as $item) {
                $item->product->addStock($item->num);
            }
        });
    }
}

然后在 app\Models\Shop\Product 商品模型里面新增方法:

/***
 * 增加库存
 * @param $num
 * @return int
 */
public function addStock($num)
{
    return $this->increment('stock', $num);
}

这个方法是用于延迟队列任务执行成功后,退回对应库存的

app\Models\Shop\OrderProduct.php 中添加代码如下:

public function product()
{
    return $this->belongsTo(Product::class);
}

3、触发任务

当事务提交成功后,执行任务处理器。在 Orders 控制器的 store 方法中,添加如下代码:

.
.
DB::commit();
$this->dispatch(new CloseOrder($order, config('app.order_ttl')));
return ['status' => 1, 'info' => '您已成功下单,请尽快完成支付', 'order_id' => $order->id]; // 事务执行成功后,返回当前订单的id

CloseOrder 构造函数的第二个参数延迟时间我们从配置文件中读取,为了方便我们测试,把这个值设置成 1800 秒,即 30 分钟,若想测试 30秒,可以自行设置成 30

config/app.php 中,添加代码:

'order_ttl' => 1800,

4、 测试

首先确保本次项目已安装了 predis ,若没有安装,终端执行命令:

composer require predis/predis

默认情况下,Laravel 生成的 .env 文件里把队列的驱动设置成了 sync(同步),在同步模式下延迟任务会被立即执行,所以需要先把队列的驱动改成 redis

QUEUE_CONNECTION=redis

然后在  REDIS_HOST=127.0.0.1 这行上面加上 REDIS_CLIENT=predis

redis-server  // 启动 `redis` 服务

php artisan queue:work  // 启动队列处理器

进入商品表,任意选择一个商品并将其加入购物车,记住库存数量,提交订单。

等待 30 分钟后,如果队列处理器无反应,记得清除配置缓存:php artisan config:clear 测试建议改成 30

经测试,发现下单后,对应商品的库存减少了,30 分钟后,当任务执行成功,发现订单表的 closed 字段有原来的 1 修改成了 0,并且库存还原。

小米商城公众号版,删除购物车调整样式问题

插件参考地址:https://sweetalert.js.org/guides/#getting-started

resources/views/wechat/cart/index.blade.php 引入这个库,我这里直接引入网络资源:

<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>

<script>
    $(function () {
        // 删除
        $('.delete').click(function () {
            swal({
                title: "确定要删除该商品么?",
                text: "删除后不可恢复,请慎重操作!",
                icon: "warning",
                buttons: ['取消', '确定'],
                dangerMode: true,
            })
                .then((willDelete) => {
                    if (willDelete) {
                        var id = $(this).parents('li').data('id');
                        var _this = $(this);
                        $.ajax({
                            type: "DELETE",
                            url: "/cart/" + id,
                            success: function (data) {
                                // console.log(data);return;
                                _this.parents('li').remove();
                                swal("操作成功!");
                                $("#num").text("" + data.number + "件 金额:");
                                $("#total_price").text((data.total_price).toFixed(2));
                            }
                        })
                    } else {
                        swal("您已取消删除!");
                    }
                });
        })
    })
</script>

完整样式如图:

小米商城分离版,加入购物车调整样式问题

插件参考地址:https://sweetalert.js.org/guides/#getting-started

1、安装包文件

cnpm install --save sweetalert

Show.vue 页面中引入该包

<script>
    import swal from 'sweetalert';
    .
    .
</script>

给点击按钮绑定事件:

<a href="javascript:;" @click="buy">立即购买</a>
methods: {
    init() {
        let id = this.$route.params.id;
        this.axios.get(`/api/product/${id}`)
            .then((res) => {
                // console.log(res)
                this.product = res.data.product
            })
    },
    buy() {
        let product_id = this.product.id;
        this.axios.post(`/api/cart`, {product_id: product_id}).then((res) => {
            if (res.data.success == true) {
                swal({
                    title: res.data.msg,
                    icon: "success",
                });
                this.$router.push({name: 'index'})
            }
        })
    },
},

加入购物车成功后样式如图:

2、写个定时执行,让弹框自动消失

首先在 main.js 中定义全局辅助函数,如下:

Vue.config.productionTip = false
Vue.prototype.axios = axios;

//设置flash自动隐藏提示框
Vue.prototype.setFlash=function(){
    var hideFlash = function () {
        $('.swal-overlay').fadeOut("slow");  // 这里的swal-overlay是弹出框自带的class
    }
    setTimeout(hideFlash, 2000);
}

然后在 Show.vue 中的加入成功后的代码中调用辅助函数:

.
.
if (res.data.success == true) {
    swal({
        title: res.data.msg,
        icon: "success",
    });

    this.setFlash();   // 调用辅助函数

    this.$router.push({name: 'index'})
}

小米商城分离版用户地址省市区问题

1、新增地址

(1)、安装插件,参考地址:https://distpicker.pigjian.com/

cnpm install v-distpicker --save   // 安装包文件

import VDistpicker from 'v-distpicker'  // 新增页面引入插件

// 注册组件
export default {
    components: {VDistpicker},
    .
    .
}

(2)、保存地址按钮:

<a href="javascript:;" class="ui-button" @click="storeAddress()"><span>保存地址</span></a>

(3)、完整 js 代码如下:

export default {
    components: {VDistpicker},
    data() {
        return {
            address: {},
            show: false
        }
    },
    methods: {
        storeAddress() {
            var status = true;
            $("input").each(function () {
                var val = $(this).val();
                if (val == "") {
                    status = false;
                }
            });
            if (status == false) {
                alert('您的填写的地址不完整!');
                return false;
            }
            var data = $("input").serialize();
            this.axios.post(`/api/address`, data)
                .then((response) => {
                    this.$router.go(-1);
                })
        },
        is_show() {
            this.show = true
        },
        show_modal(data) {
            this.show = !this.show;
            this.address.pca = data.province.value + ' ' + data.city.value + ' ' + data.area.value
        },
    },
}

(4)、完整 html 代码如下:

<div id="wrapper">
    <div class="page-address-edit" data-log="地址">

        <div class="edit-box">
            <ul class="ui-list">
                <li class="ui-list-item">
                    <div class="label">收货人:</div>
                    <div class="ui-input"><input placeholder="真实姓名" name="name" maxlength="15" type="text"></div>
                </li>
                <li class="ui-list-item">
                    <div class="label">手机号码:</div>
                    <div class="ui-input"><input placeholder="手机号" name="tel" maxlength="13" type="tel"></div>
                </li>
                <li class="ui-list-item">
                    <div class="label">所在地区:</div>
                    <div class="ui-input">
                        <input placeholder="省 市 区" name="pca" @click="is_show" maxlength="20" type="text"
                               v-model="address.pca"
                               readonly="readonly">
                    </div>
                </li>
                <li class="ui-list-item">
                    <div class="label">街道地址:</div>
                    <div class="ui-input"><input placeholder="详细地址" name="detail" maxlength="120" type="text"></div>
                </li>
            </ul>
            <div class="save-button">
                <a href="javascript:;" class="ui-button" @click="storeAddress()"><span>保存地址</span></a>
            </div>
        </div>


        <div class="ui-mask" v-if="show"></div>
        <div class="ui-pop" v-if="show">
            <div class="ui-pop-content">
                <div class="region-list" id="city">
                    <v-distpicker type="mobile" @selected="show_modal"></v-distpicker>
                </div>
            </div>
            <div class="ui-pop-title">选择所在地区</div>
            <div class="ui-pop-close"><a><span class="icon-10chahaokuang"></span></a></div>
        </div>

        <div class="popup-risk-check"></div>
    </div>
</div>

2、编辑地址:服务--地址管理--编辑地址

(1)、在 routes/api.php 中添加编辑地址路由

Route::get('{id}', 'AddressController@show');
Route::put('update/{id}', 'AddressController@update');

(2)、在 AddressController 中添加代码如下:

/**
 * 查出当前地址
 * Update the specified resource in storage.
 * @param  \Illuminate\Http\Request $request
 * @param  int $id
 * @return \Illuminate\Http\Response
 */
public function show($id)
{
    $address = Address::find($id);
    $pca = $address->province . ' ' . $address->city . ' ' . $address->area;
    $address['pca'] = $pca;
    return $address;
}


/**
 * 执行编辑
 * Update the specified resource in storage.
 * @param  \Illuminate\Http\Request $request
 * @param  int $id
 * @return \Illuminate\Http\Response
 */
public function update(Request $request, $id)
{
    $pca = explode(" ", $request->pca);

    $address = Address::where('id', $id)->update([
        'name' => $request->name,
        'province' => $pca[0],
        'city' => $pca[1],
        'area' => $pca[2],
        'tel' => $request->tel,
        'detail' => $request->detail,
    ]);
    return $address;
}

(3)、前端编辑地址页面代码:

<template>
    <div id="wrapper">
        <div class="page-address-edit" data-log="地址">

            <div class="edit-box">
                <ul class="ui-list">
                    <li class="ui-list-item">
                        <div class="label">收货人:</div>
                        <div class="ui-input"><input placeholder="真实姓名" v-model="address.name" name="name"
                                                     maxlength="15" type="text">
                        </div>
                    </li>
                    <li class="ui-list-item">
                        <div class="label">手机号码:</div>
                        <div class="ui-input"><input placeholder="手机号" v-model="address.tel" name="tel" maxlength="13"
                                                     type="tel"></div>
                    </li>
                    <li class="ui-list-item">
                        <div class="label">所在地区:</div>
                        <div class="ui-input">
                            <input placeholder="省 市 区" @click="is_show" name="pca" maxlength="20" type="text"
                                   readonly="readonly" v-model="address.pca">
                        </div>
                    </li>
                    <li class="ui-list-item">
                        <div class="label">街道地址:</div>
                        <div class="ui-input"><input placeholder="详细地址" v-model="address.detail" name="detail"
                                                     maxlength="120" type="text">
                        </div>
                    </li>
                </ul>
                <div class="save-button">
                    <a href="javascript:;" class="ui-button" @click="updateAddress(address)"><span>保存地址</span></a>
                </div>
            </div>


            <div class="ui-mask" v-if="show"></div>
            <div class="ui-pop" v-if="show">
                <div class="ui-pop-content">
                    <div class="region-list">
                        <v-distpicker type="mobile" @selected="show_modal"></v-distpicker>
                    </div>
                </div>
                <div class="ui-pop-title">选择所在地区</div>
                <div class="ui-pop-close"><a><span class="icon-10chahaokuang"></span></a></div>
            </div>


            <div class="popup-risk-check"></div>
        </div>
    </div>
</template>

<script>
    import VDistpicker from 'v-distpicker'

    export default {
        components: {VDistpicker},
        data() {
            return {
                address: {},
                show: false
            }
        },
        created() {
            let id = this.$route.params.id;
            this.axios.get(`/api/address/${id}`).then((res) => {
                console.log(res)
                this.address = res.data
            })
        },
        methods: {
            is_show() {
                this.show = true
            },
            show_modal(data) {
                this.show = !this.show
                this.address.pca = data.province.value + ' ' + data.city.value + ' ' + data.area.value
            },
            updateAddress(address) {
                var data = $("input").serialize();
                this.axios.put(`/api/address/update/${address.id}`, data).then((res) => {
                    this.$router.push({name: 'manage'});
                })
            }
        }
    }
</script>

转载请注明,来自https://itfun.tv/news/168