在维护电商项目的时候,产品大大突然提出做一个限时特价的功能,简单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才去产品价格任务表中查找任务价格,否则返回原价。思路比较简单,感觉不太高效,但又想不到更好的方案出来了,呜呜呜~

Laravel限时特价相关推荐

  1. Laravel/Lumen 使用 Redis队列

    一.概述 在Web开发中,我们经常会遇到需要批量处理任务的场景,比如群发邮件.秒杀资格获取等,我们将这些耗时或者高并发的操作放到队列中异步执行可以有效缓解系统压力.提高系统响应速度和负载能力. 二.配 ...

  2. php – Laravel 7 Session Lifetime

    根据Laravel config / session.php /* |----------------------------------------------------------------- ...

  3. Laravel中Redis的配置和使用

    引入redis composer require predis/predis 会在composer.json中引入最新版本的predis composer update 把下载predis 库加入到v ...

  4. Laravel和Thinkphp有什么区别,哪个框架好用

    Laravel和Thinkphp这两个php框架对于php程序员都不陌生,新手可能对Thinkphp比较熟,也是国内比较出名的开源框架,更高级的Laravel一般有点经验的才使用. TP框架: Thi ...

  5. Laravel Dcat Admin 安装

    安装 环境 PHP >= 7.1 Laravel 5.5.0 ~ 7.* Fileinfo PHP Extension 开始安装 如果安装过程中出现 composer 下载过慢或安装失败的情况, ...

  6. Laravel 7发行说明

    版本化方案 Laravel及官方发布的包皆遵循 语义版本化.主要框架版本每六个月发布一次 (~2月和~8月),而次要和补丁版本可能每周发布一次.次要版本和补丁 决不 包含非兼容性更改. 引入 Lara ...

  7. Lumen / Laravel 使用网易邮箱 SMTP 发送邮件

    Laravel 是目前最流行的PHP框架,而Lumen 是 Laravel 的精简版,主要用于接口开发. Laravel 邮件发送服务基于 Symfony 组件 Swift Mailer. 本文记录了 ...

  8. php语+言教程,写给thinkphp开发者的laravel系列教程(九)打印数据-Fun言

    php这种脚本语言调试起来是很方便的: 大多时候看报错打印一下数据就知道问题所在了: php 内置了常用打印函数 print 和 var_dump : 但是打印出来的样式实在让人目瞪狗呆: think ...

  9. php set medias,laravel5.1 -- Integrate FileManager and CKeditor into laravel

    FileManager中文名叫文件管理器,也叫文件浏览器,它给我们提供了一个可视化的界面来管理文件和文件夹.利用FileManager,我们可以对文件进行浏览.增加.打印.修改(文件属性).重命名.搜 ...

最新文章

  1. Linux版本之redhat9---gFtp中文乱码解决方案
  2. 面试:Java 到底是值传递还是引用传递?
  3. java 编写方法和属性,编写一个关于教师的JAVA-BEAN,要求实现其基本属性如年龄,性别等的get和set方法...
  4. 单点登录在项目中的实现 转.
  5. centos7 postgresql9和postgis2.1插件编译部署
  6. RuntimeError: DataLoader worker (pid(s) 13512, 280, 21040) exited unexpectedly
  7. 在布局空间标注的尺寸量不对_卫生间最佳布局尺寸,合理布局做到1毫米都不浪费!...
  8. java中identifiers什么意思_javassist.是什么意思
  9. NYOJ4 - ASCII码排序
  10. 第九周项目3-输出星号图
  11. 2019年5月,国际计量单位实施新定义
  12. Web--用户注册界面
  13. 掌握这几款高保真设计软件,百万年薪不再是梦想
  14. 用c语言编程点菜系统,基于C语言实现点菜系统.pdf
  15. 什么是SEO,为什么要做SEO?
  16. 精品软件 推荐 ABBYY FineReader 世界排名第一的 OCR 文字识别工具
  17. 这些年过上幸福生活的程序员(中篇)
  18. 接口请求中post与put的区别
  19. Docker版NextCloud文件手动扫描同步 定时扫描
  20. php 打开文件方式

热门文章

  1. jsp如何显示ftp服务器的图片,Jsp页面上传图片和缩略图查看
  2. kfold_k-交叉验证KFold
  3. 调用高德的【行政区划查询】接口,组装省市区SQL
  4. Borg Maze最小生成树
  5. 微信小程序实现返回顶部
  6. centos567修改系统日志保留时间为1年
  7. 树莓派的简单应用--语音识别控制智能垃圾箱
  8. Ctrl+Shift+C组合键失效,复制文本格式的组合键失效
  9. C++ 使用_beginthreadex创建线程、线程句柄(等待线程关闭)、线程id的作用(发送线程消息)
  10. Python深浅拷贝!