Laravel自动取消订单

    最近的业务需要做15分钟内未完成支付就自动取消订单的功能,类似的功能还有很多,例如:订单完成后自动确认收货、完成付款5分钟后发送短信通知等等,这些看起来就像定时任务,即多久后去做什么事。

    实现上述功能的简单办法有两种:一种是Laravel的任务调度+crontab定时去执行任务;另一种是使用Redis的键过期事件去执行任务。对于业务量小的场景且不严格要求实时性的,推荐第一种方法;如果业务复杂且数据量大,又要求具有实时性,还要简单(即方便又准确),那么第二种方法就很适合了。

 

创建数据表:

    我们先准备一张订单表吧:

DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_num` int(11) NOT NULL DEFAULT 0 COMMENT '订单编号',
  `price` decimal(8, 2) NOT NULL DEFAULT 0.00 COMMENT '价格',
  `order_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '订单状态',
  `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除逻辑标识符',
  `created_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间',
  `updated_time` int(11) NOT NULL DEFAULT 0 COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `i2`(`created_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

    虽然直接创建数据表是挺方便的,但是在多人合作开发中并不建议这样做,最好还是使用Laravel中的数据库迁移(migrate)来创建数据表吧:

// 打开终端,cd到项目根目录,执行以下命令
php artisan make:migration create_tb_order_table

    项目根目录database中的migrations文件夹里多了一个“2019_07_09_203633_create_tb_order_table.php”文件,前面的数字是创建时间, 打开上述文件,修改up方法。

public function up()
{
    Schema::create('tb_order', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('order_num')->default(0)->comment('订单编号');
        $table->decimal('price', 8, 2)->default(0)->comment('价格');
        $table->tinyInteger('order_status')->default(0)->comment('订单状态');
        $table->tinyInteger('is_deleted')->default(0)->comment('删除逻辑标识符');
        $table->integer('created_time')->default(0)->comment('创建时间');
        $table->integer('updated_time')->default(0)->comment('更新时间');
        $table->index('created_time');
    });
}

    执行下面命令:

php artisan migrate

    这样,数据表就创建完成了。

 

一、Laravel+crontab定时任务:

    先到项目根目录创建任务文件OrderCancel.php:

php artisan make:command OrderCancel

    再修改 app\Console\Commands\OrderCancel.php:

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Exception;
use Illuminate\Support\Facades\Log;

class OrderCancel extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'order:cancel';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '15分钟未支付,取消订单';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        try {
            $time = time();
            $orders = DB::table('order')
                ->where('is_deleted', 0)
                ->where('order_status', 0)
                ->where('created_time', '<=', $time - 15*60)
                ->get();

            foreach ($orders as $order) {
        	DB::table('order')->where('id', $order->id)->increment('is_deleted');
                Log::info('订单取消成功:'. $order->id);
            }
        }
        catch (\Exception $e) {
            Log::info('未知错误:' . $e->getMessage());
        }
    }
}

    接着,我们就可以弄个计划任务了,执行系统命令 crontab -e ,在里面添加内容,:wq 保存,执行 crontab -l 查看内容:

* * * * * php /项目根目录/artisan schedule:run >> /dev/null 2>&1

    如果发现任务调度不执行,可能php的路径并不识别,修改为:

* * * * * /usr/bin/php /项目根目录/artisan schedul:run >> /dev/null 2>&1

    注意:/usr/bin/php是php文件路径,如果不知道自己的php路径,执行系统命令:which php 就可以找到路径。

    另外,我在这里还会出现一个问题:执行crontab -e,添加完内容后,发现 crontab -l 里面并没有内容,定时任务还是不执行,真头疼!!!

    后面发现解决方法:原来 crontab -e 会在 /var/spool/cron/crontabs 中创建一个与用户名相同名字的文件(如果是root用户就会创建一个root文件),只要把root文件移动到 /var/spool/cron 并且添加上面的内容就行了。

mv /var/spool/cron/crontabs/root /var/spool/cron/

     然后在 app\Console\Kernel.php 中的 $command属性和 schedule 方法里,加入下面两行:

<?php
namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\OrderCancel; // 引入OrderCancel类

class Kernel extends ConsoleKernel
{
    protected $commands = [
        \App\Console\Commands\OrderCancel::class,
    ];

    protected function schedule(Schedule $schedule)
    {
        // 采用自定义的命令:OrderCancel类中的$signature属性->order:cancel
        $schedule->command('order:cancel')->everyMinute(); // 每分钟执行一次
        // 也可以这样写,采用类型进行调用
        // $schedule->command(OrderCancel::class)->everyMinute();
    }

    protected function commands()
    {
        $this->load(__DIR__.'/Commands');
        require base_path('routes/console.php');
    }
}

    保存测试下就行了。执行下面命令测试(只执行一次)。

php artisan order:cancel

 

二、Laravel+Redis键过期事件:

    接着介绍使用Redis来做自动取消订单的功能,先确保Redis的版本大于2.8,接着修改redis的配置文件,启用键过期的通知:

notify-keyspace-events "Ex"

    重启Redis,给Laravel安装redis扩展,到项目根目录执行以下命令:

composer require predis/predis

    在 .env 文件中配置好Redis的相关信息:

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
REDIS_RW_TIMEOUT=-1

    修改 config\database.php 中Redis的配置

'redis' => [
        'client' => 'predis',
        'default' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => env('REDIS_DB', 0),
	    'read_write_timeout' => env('REDIS_RW_TIMEOUT', -1), // 读写超时设定
        ],
],

    然后就可以开始监听 ORDER_UNPAID:ORDER_ID 的过期事件了,同样先建立任务文件 OrderExpireListen.php :

php artisan make:command OrderExpireListen

    修改 app\Console\Commands\OrderExpireListen.php :

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;


class OrderExpireListen extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'order:expire';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '监听订单创建,15分钟内未付款就取消订单';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $db = config('database.redis.default.database', 0); // 监听的数据库
        $pattern = '__keyevent@'. $db . '__:expired'; // 监听事件:键过期事件
        Redis::subscribe([$pattern], function ($channel) { // 订阅键过期事件,$channel为返回的键名
        	$key_type = str_before($channel,':');
        	switch ($key_type) {
                case 'ORDER_UNPAID':
                    $order_id = str_after($channel,':');    // 取出订单 ID
                    $order = DB::table('order')->find($order_id);
		    if ($order && $order->order_status == 0) {
                        // 这里我们直接修改数据库
			$result = DB::table('order')->where('id', $order_id)->update(['is_deleted'=>1]);
			Log::info('订单ID:' . $order_id . ':' . $result ? '取消成功' : '取消失败');
            
                        // 如果业务量大的话,我们可以把取消订单的操作放到队列中去执行
                        // Job::dispatch($order_id);
		    }
                    break;
                case 'ORDER_OTHEREVENT':
                    break;
                default:
                    break;
            }
        });
    }
}

    之后我们用 supervisor 进行管理就行了,让它用守护进程在后台执行,或者直接在项目根目录执行以下命令也可以:

php artisan order:expire &

   最后我们测试一下:

// Redis::set(key, value, 'EX', seconds)
Redis::set('ORDER_UNPAID:1', 1, 'EX', 3); // 设置ORDER_UNPAID:1这个键3s后过期

   查看一下日志文件,在 /storage/logs/laravel.log :

    至此,两种方法已经介绍完毕,都是比较简单的实现,根据实际业务需求选择更好的方法吧。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值