Laravel6 实现超时未支付订单自动关闭

处理订单号的问题

本节课程主要是在之前的小米商城基础上完善一个常见的小功能。之前我们创建订单的订单号使用的是订单表的 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}}元    订单编号:{{$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 变量。

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

Laravel6 实现超时未支付订单自动关闭_第1张图片

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

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

2、创建任务

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

php artisan make:job CloseOrder

创建的任务类保存在 app/Jobs 目录下,编辑 CloseOrder.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,并且库存还原。

vue.js版

<template>
  <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 }}元    订单编号:{{ order.out_trade_no }}div>
        <div class="p h_box">
          <div>收货信息:div>
          <div class="flex_1">{{ address.name }} {{
              address.tel
            }}<br>{{ address.province + ' ' + address.city + ' ' + address.area + ' ' + address.detail }} (430070)
          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 active">
            <div data-log="A0-支付宝" 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>
template>
<script>
export default {
  data() {
    return {
      order: {},
      address: {},
      created_at: ''
    }
  },
  created() {
    this.init()
    setInterval(()=>{this.countDown()},1000)
  },
  methods: {
    init() {
      let id = this.$route.params.id
      this.axios.get(`api/order/${id}`).then((res) => {
        this.order = res.data.order
        this.created_at = res.data.order.created_at
        this.address = res.data.order.order_address
      })
    },
    addZero(i) {
      return i < 10 ? "0" + i : i + "";
    },
    countDown() {
      var nowtime = new Date();
      var endtime = new Date(this.created_at);
      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 = this.addZero(m);
      s = this.addZero(s);
      document.querySelector(".count").innerHTML = `剩余支付时间:  ${m}${s} 秒`;
      if (lefttime <= 0) {
        document.querySelector(".count").innerHTML = "订单已失效";
        return;
      }
    }
  }
}
script>

到此,本地测试已全部完成,但是别高兴的太早,项目部署上线,如何让这个任务自动执行???接下来才是最精彩的部分。

项目部署

项目正常部署上线,我这里服务器使用的是 Ubuntu 系统。

此处部署过程省略,自行完成!

配置 Supervisor

1、安装

sudo apt-get install supervisor

2、配置Supervisor

安装成功后,Supervisor 配置文件通常存放在 /etc/supervisor/conf.d 目录,在该目录下,可以创建多个配置文件指示 Supervisor 如何监视进程,例如,让我们创建一个开启并监视 queue:work 进程的 laravel-worker.conf 文件:

cd /etc/supervisor/conf.d 
touch laravel-worker.conf
vim laravel-worker.conf

在 laravel-worker.conf 中添加如下代码:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d          ; 进程名称
command=php /var/www/shopCenter/artisan queue:work      ; 在哪个项目执行命令
autostart=true ; 当supervisor启动时,程序自动启动
autorestart=true ; 自动重启
user=root  ; 执行命令的用户名
numprocs=1  ; 进程数
redirect_stderr=true  ; 重定向stderr到stdout
stdout_logfile=/etc/supervisor/logs/worker.log    ; 进程的日志输出写入文件中

3、启动Supervisor

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

如果启动过程中有报错,请参考下图解决:

Laravel6 实现超时未支付订单自动关闭_第2张图片
4、查看Supervisor进程是否有启动

ps -ef | grep artisan

出现如下表示启动成功,由于我们numprocs为1,所以启动1个进程;
在这里插入图片描述
5、终极测试,线上下单前记住当前商品库存,设置订单超时时间,下单成功后等待超时,查看库存和订单状态即可。

参考文献:https://phpartisan.cn/news/82.html

你可能感兴趣的:(PHP,Laravel)