Laravel限时特价

    在维护电商项目的时候,产品大大突然提出做一个限时特价的功能,简单4个字概括了他的需求(Excuse me?能说清楚点吗?),相信很多做电商网站的程序猿都会遇到这样的需求。

 

一、理解需求:

    限时特价!某个时间区段内,在起始时间点去做一件事,在终止时间点去做另一件事,最后事物恢复为最初始的状态(即和之前相比没有产生变化)。也就是说,站在用户的角度,在起始时间点用户看到产品的价格产生了变化,在终止时间点看到产品恢复了原价。概括一下,在某个时间段里产品价格不同,过后产品恢复价格。

 

二、确定方案:

    集合还是独立集合就是建立一条特价任务,把产品加入到任务里,这样做方便管理(例如手机有3种颜色,都需要做降价处理),不用特意弄3条特价任务,但价格是一样的。独立就是单独为每件产品新建一条特价任务,这样做灵活性较大,自由分配价格(例如手机有3种颜色,只有一种颜色需要做降价处理)。两种方案各有优劣,根据具体的业务情况选择更好方案。

    另外,还有一个问题,就是需不需要改变产品的价格。

  1. 即限时特价生效期间,我们直接改动了产品的价格,失效后再恢复成原价格。这种方法非常的简单粗暴,可能会有人提问:生效期间我们又去改产品的价格怎么办?产品是不是又变成了不同的价格呢?这个问题很简单,在产品表中加入“is_limit_time”字段,用来表示是否为限时特价生效期间(生效期间不允许更改价格)。或者又有人会问,这种做法可以吗?答案是根据实际业务情况,一般的电商项目都会有“产品价格变动记录表”,每次价格发生变动都会被记录在此表中,所以这种做法是被允许的。
  2. 另一种就是不直接改变产品的价格。给用户显示价格之前我们先确定这件产品的“is_limit_time”字段是否为1,如果为1,则去找最近的一条特价任务,如果存在并且处于生效期间,就直接把特价任务里的价格返回给用户,否则返回原价格。这种做法就需要在业务逻辑中用代码去实现了。

    可选方案:1、集合+改变价格(难度大); 2、集合+不改变价格(难度最大); 3、独立+改变价格(难度小); 4、独立+不改变价格(难度适中)。

    最终方案:根据实际的业务情况,我最终选择了第4种方案。难度适中。

 

三、解决需求:

    为了更高效的去完成这个需求,我们使用Redis的键过期事件来实现限时特价的功能。(虽然使用crontab定时任务也可以,但是不推荐这种做法,麻烦,管理不方便,数据量大时性能差。)

1、创建数据表:

    首先我们先建立两张数据表:tb_product表和tb_product_price_mission表,即产品表和产品价格任务表。

// 到项目根目录执行以下2条命令
php artisan make:migration create_tb_product_table
php artisan make:migration create_tb_product_price_mission_table

  分别修改 database/migrations 的 create_tb_product_table.php 和 create_tb_product_price_mission_table.php 中的 up 方法:

// create_tb_product_table.php
public function up()
{
    Schema::create('tb_product', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name')->default('')->comment('产品名称');
        $table->decimal('price', 8, 2)->default(0)->comment('价格');
        $table->tinyInteger('is_limit_time')->default(0)->comment('限时特价是否生效');
        $table->tinyInteger('is_deleted')->default(0)->comment('删除逻辑标识符');
        $table->integer('created_time')->default(0)->comment('创建时间');
        $table->integer('updated_time')->default(0)->comment('更新时间');
    });
}

// create_tb_product_price_mission_table.php
public function up()
{
    Schema::create('tb_product_price_mission', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('product_id')->default(0)->comment('产品ID');
        $table->decimal('mission_price', 8, 2)->default(0)->comment('任务价格');
        $table->tinyInteger('is_happened')->default(0)->comment('是否生效:-1已失效,0未生效,1生效中');
        $table->integer('start_time')->default(0)->comment('开始时间');
        $table->integer('end_time')->default(0)->comment('结束时间');
        $table->integer('created_time')->default(0)->comment('创建时间');
    });
}

    执行migrate命令创建数据表:

php artisan migrate

   至此,两张数据表就已经创建完成了。

 

2、配置Redis

    先给Laravel安装Redis的扩展:

composer require predis/predis

    修改Redis的配置文件,启动键过期事件通知:

notify-keyspace-events "Ex"

    重启Redis,在 .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), // 读写超时设定
        ],
],

 

3、代码实现:

    先创建任务文件 ProductPriceMission.php :

php artisan make:command ProdcutPriceMission

    我们监听“price_mission_start_time:product_id-mission_id”和 “price_mission_end_time:product_id-mission_id” 两个键的过期事件,修改 app\Console\Commands\ProductPriceMission.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 ProductPriceMission extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'product_price:mission';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '监听特价任务创建,开始时间到了使任务生效,结束时间到了使任务失效';
 
    /**
     * 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 'price_mission_start_time':
                    $product_id = str_before(str_after($channel,':'), '-');    // 取出产品 ID
                    $mission_id = str_after(str_after($channel,':'), '-');    // 取出任务 ID
                    $product = DB::table('product')->find($product_id);
                    $mission = DB::table('product_price_mission')->find($mission_id);
		    if ($product && $mission) {
                        // 这里我们直接修改数据库
			$result_product = DB::table('product')->where('id', $product_id)->update(['is_limit_time'=>1]);
			$result_mission = DB::table('product_price_mission')->where('id', $mission_id)->update(['is_happened'=>1]);
			Log::info('产品ID:' . $product_id . ':' . $result_product ? '生效成功' : '生效失败');
            
                        // 如果业务量大的话,我们可以把生效的操作放到队列中去执行
                        // Job::dispatch($product_id, $mission_id, $is_limit_time = 1, $is_happened = 1);
		    }
                    break;

                case 'price_mission_end_time':
                    $product_id = str_before(str_after($channel,':'), '-');    // 取出产品 ID
                    $mission_id = str_after(str_after($channel,':'), '-');    // 取出任务 ID
                    $product = DB::table('product')->find($product_id);
                    $mission = DB::table('product_price_mission')->find($mission_id);
		    if ($product && $mission) {
                        // 这里我们直接修改数据库
			$result_product = DB::table('product')->where('id', $product_id)->update(['is_limit_time'=>0]);
			$result_mission = DB::table('product_price_mission')->where('id', $mission_id)->update(['is_happened'=>-1]);
			Log::info('产品ID:' . $product_id . ':' . $result_product ? '失效成功' : '失效失败');
            
                        // 如果业务量大的话,我们可以把失效的操作放到队列中去执行
                        // Job::dispatch($product_id, $mission_id, $is_limit_time = 0, $is_happened = -1);
		    }
                    break;

                default:
                    break;
            }
        });
    }
}

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

php artisan product_price:mission &

 

4、测试

    插入数据:

public function insert() 
{
    $time = time();
    $product_data = [
    	'name' => '测试产品',
    	'price' => '10',
    	'created_time' => $time,
    	'end_time' => $time
    ];
    $product_id = DB::table('product')->insertGetId($product_data);
    if ($product_id) {
    	echo '产品ID:'. $product_id . "插入成功\n";
    }

    $mission_data = [
    	'product_id' => $product_id,
    	'mission_price' => '5',
    	'start_time' => $time + 60, // 一分钟后开始生效
    	'end_time' => $time + 120, // 两分钟后失效,即生效时间为60秒
    	'created_time' => $time
    ];
    $mission_id = DB::table('product_price_mission')->insertGetId($mission_data);
    if ($mission_id) {    		
    	echo '任务ID:' . $mission_id . "插入成功";
    }
	
    //设置键的过期时间
    Redis::set('price_mission_start_time:'.$product_id.'-'.$mission_id, $product_id.'-'.$mission_id, 'EX', 60);
    Redis::set('price_mission_end_time:'.$product_id.'-'.$mission_id, $product_id.'-'.$mission_id, 'EX', 120);
}

    查看产品:

public function product() 
{
    $time = time();
    $product_id = 1;
    $product = DB::table('product')->find($product_id);
    
    // 如果is_limit_time为true,查找该产品正处于生效期间的任务价格
    if ($product && $product->is_limit_time) {
    	$mission_price = DB::table('product_price_mission')
    		->where('product_id', $product_id)
    		->where('is_happened', 1)
    		->where('start_time', '<=', $time)
    		->where('end_time', '>', $time)
    		->value('mission_price');
    	$product->price = $mission_price;
    }
    dd($product);
}

    输入:http://laravel.localhost/insert :

     查看产品数据, 输入:http://laravel.localhost/product :

    过一分钟,我们再刷新查看一下:

    再过一分钟,限时特价应该失效了,看看是不是恢复为原价:

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

 

    大家对比下两个时间,是不是刚好只有60秒的生效时间。

 

5、总结

    实际上真正的业务情况比上述的还要复杂,大家可以根据自己实际遇到的情况调整思路。上述办法主要使用Redis的键过期事件来实现,在新增特价任务时就设置了两个key的过期时间,一个负责生效,另一个负责失效,用户每次查看产品数据时都要去检查“is_limit_time”是否为1(即是否在限时特价生效期间,假如不想在产品表加入“is_limit_time”字段,使用缓存也行),如果为1才去产品价格任务表中查找任务价格,否则返回原价。思路比较简单,感觉不太高效,但又想不到更好的方案出来了,呜呜呜~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值