牛肉社团后端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
cow/addons/shopro/library/traits/ActivityCache.php

660 lines
22 KiB

<?php
namespace addons\shopro\library\traits;
use addons\shopro\exception\Exception;
use addons\shopro\library\Redis;
use addons\shopro\model\Activity;
use addons\shopro\model\ActivityGoodsSkuPrice;
use addons\shopro\model\Goods;
use addons\shopro\model\GoodsSkuPrice;
use addons\shopro\model\OrderItem;
use addons\shopro\model\ScoreGoodsSkuPrice;
/**
* 活动 redis 缓存
*/
trait ActivityCache
{
protected $zsetKey = 'zset-activity';
protected $hashPrefix = 'hash-activity:';
protected $hashGoodsPrefix = 'goods-';
protected $hashGrouponPrefix = 'groupon-';
public function hasRedis($is_interrupt = false) {
$error_msg = '';
try {
$redis = $this->getRedis();
// 检测连接是否正常
$redis->ping();
} catch (\BadFunctionCallException $e) {
// 缺少扩展
$error_msg = $e->getMessage() ? $e->getMessage() : "缺少 redis 扩展";
} catch (\RedisException $e) {
// 连接拒绝
\think\Log::write('redis connection redisException fail: ' . $e->getMessage());
$error_msg = $e->getMessage() ? $e->getMessage() : "redis 连接失败";
} catch (\Exception $e) {
// 异常
\think\Log::write('redis connection fail: ' . $e->getMessage());
$error_msg = $e->getMessage() ? $e->getMessage() : "redis 连接异常";
}
if ($error_msg) {
if ($is_interrupt) {
throw new \Exception($error_msg);
} else {
return false;
}
}
return true;
}
public function getRedis() {
if (!isset($GLOBALS['SPREDIS'])) {
$GLOBALS['SPREDIS'] = (new Redis())->getRedis();
}
return $GLOBALS['SPREDIS'];
}
/**
* 将活动设置到 redis 中
*
* @param [type] $activity
* @param array $goodsList
* @return void
*/
public function setActivity($activity, $goodsList = []) {
$redis = $this->getRedis();
// hash 键值
$hashKey = $this->getHashKey($activity['id'], $activity['type']);
// 删除旧的可变数据,需要排除销量 key
if ($redis->EXISTS($hashKey)) {
// 如果 hashKey 存在,删除规格
$hashs = $redis->HGETALL($hashKey);
foreach ($hashs as $hashField => $hashValue) {
// 是商品规格,并且不是销量
if (strpos($hashField, $this->hashGoodsPrefix) !== false && strpos($hashField, '-sale') === false) {
// 商品规格信息,删掉
$redis->HDEL($hashKey, $hashField);
}
}
}
$redis->HMSET($hashKey, [
'id' => $activity['id'],
'title' => $activity['title'],
'type' => $activity['type'],
'richtext_id' => $activity['richtext_id'],
'richtext_title' => $activity['richtext_title'],
'starttime' => $activity['starttime'],
'endtime' => $activity['endtime'],
'rules' => is_array($activity['rules']) ? json_encode($activity['rules']) : $activity['rules'],
'goods_ids' => $activity['goods_ids']
]
);
if (in_array($activity['type'], ['groupon', 'seckill'])) {
// 拼团或者秒杀,记录商品规格信息 (里面包含活动库存,价格等信息)
foreach ($goodsList as $goods) {
unset($goods['sales']); // 规格销量单独字段保存 goods-id-id-sale key
$goods_sku_key = $this->getHashGoodsKey($goods['goods_id'], $goods['sku_price_id']);
// 获取当前规格的销量,修改库存的时候,需要把 stock 加上这部分销量
$cacheSale = $redis->HGET($hashKey, $goods_sku_key . '-sale');
$goods['stock'] = $goods['stock'] + $cacheSale;
$redis->HSET($hashKey, $this->getHashGoodsKey($goods['goods_id'], $goods['sku_price_id']), json_encode($goods));
}
}
// 将 hash 键值存入 有序集合,score 为 id
$redis->ZADD($this->zsetKey, $activity['starttime'], $hashKey);
}
/**
* 获取所有活动(前端:秒杀商品列表,拼团商品列表)
*
* @param array $activityTypes 为空将查询所有类型的活动
* @param string $status
* @param string $format_type // 格式化类型,默认clear,清理多余的字段,比如拼团的 团信息
* @return void
*/
public function getActivityList($activityTypes = [], $status = 'all', $format_type = 'normal') {
$redis = $this->getRedis();
// 获取对应的活动类型的集合
$activityHashList = $this->getActivityHashKeysByType($activityTypes);
$activityList = [];
if (!$activityHashList) { // 没有获取到,返回空数组
return $activityList;
}
foreach ($activityHashList as $activityHashKey) {
// 查询活动状态
if ($status != 'all') {
$activity_status = $this->getActivityStatusByHashKey($activityHashKey);
if ($status != $activity_status) {
continue;
}
}
// 格式化活动
$activity = $this->formatActivityByType($activityHashKey, $format_type);
if ($activity) {
$activityList[] = $activity;
}
}
return $activityList;
}
/**
* 查询商品列表,详情时,获取这个商品对应的秒杀拼团等活动
*
* @param [type] $goods_id
* @param Array $activityTypes
* @param integer $activity_id
* @return void
*/
public function getGoodsActivity($goods_id, $activityTypes = [], $activity_id = 0) {
// 获取商品第一条活动的 hash key
$activityHashKey = $this->getActivityHashKeyByGoods($goods_id, $activityTypes, 'first', $activity_id);
// 如果存在活动
if ($activityHashKey) {
// 获取活动并且按照商品的要求格式化
return $this->formatActivityByType($activityHashKey, 'goods', ['goods_id', $goods_id]);
}
return null;
}
/**
* 查询商品列表,详情时,获取这个商品对应的满减,满折等活动
*
* @param int $goods_id 特定商品 id
* @param Array $activityTypes 要查询的活动数组
* @param string $type 查单条,还是全部 all|first
* @param int $activity_id
* @return void
*/
public function getGoodsActivityDiscount($goods_id, $activityTypes = [], $type = 'all', $activity_id = 0) {
// 获取活动的 hash key
$activityHashKey = $this->getActivityHashKeyByGoods($goods_id, $activityTypes, $type, $activity_id);
// 如果存在活动
if ($activityHashKey) {
if (is_array($activityHashKey)) {
$activities = [];
foreach ($activityHashKey as $key => $hashKey) {
$activity = $this->formatActivityByType($hashKey, 'discount');
if ($activity) {
$activities[] = $activity;
}
}
return $activities;
} else {
// 获取活动所有信息
return $activity = $this->formatActivityByType($activityHashKey, 'discount');
}
}
return $type == 'all' ? [] : null;
}
/**
* 通过活动的键值,获取活动完整信息
*
* @param [type] $activityHashKey
* @return array
*/
public function getActivityByHashKey($activityHashKey)
{
$redis = $this->getRedis();
// 取出整条 hash 记录
$activityHash = $redis->HGETALL($activityHashKey);
return $activityHash;
}
// 删除活动缓存
public function delActivity($activity) {
$redis = $this->getRedis();
$hashKey = $this->getHashKey($activity['id'], $activity['type']);
// 删除 hash
$redis->DEL($hashKey);
// 删除集合
$redis->ZREM($this->zsetKey, $hashKey);
}
/**
* 通过商品获取该商品参与的活动的hash key
*
* @param [type] $goods_id
* @param Array $activityType
* @param string $type 全部还是第一条
* @param integer $activity_id
* @return void
*/
private function getActivityHashKeyByGoods($goods_id, $activityType = [], $type = 'first', $activity_id = 0) {
$redis = $this->getRedis();
// 获取对应类型的活动集合
$activityHashList = $this->getActivityHashKeysByType($activityType, $activity_id);
$activityHashKeys = [];
if (!$activityHashList) { // 没有获取到,返回空数组
return $activityHashKeys;
}
foreach ($activityHashList as $activityHashKey) {
if (strpos($activityHashKey, 'seckill') === false && strpos($activityHashKey, 'groupon') === false) {
// 不是拼团,秒杀,要校验活动时间,如果不在活动时间,跳过
// 获取活动状态
$activity_status = $this->getActivityStatusByHashKey($activityHashKey);
if ($activity_status != 'ing') {
continue;
}
}
// 判断这条活动是否包含该商品
$goods_ids = array_filter(explode(',', $redis->HGET($activityHashKey, 'goods_ids')));
if (in_array($goods_id, $goods_ids) || empty($goods_ids)) {
$activityHashKeys[] = $activityHashKey;
if ($type == 'first') { // 只取第一条
break;
}
}
}
if ($activity_id && !$activityHashKeys) {
// 查询特定活动,没找到,抛出异常, 活动不存在
new Exception('活动不存在');
}
return $type == 'first' ? ($activityHashKeys[0] ?? '') : $activityHashKeys;
}
/**
* 获取活动的状态
*/
private function getActivityStatusByHashKey($activityHashKey) {
$redis = $this->getRedis();
$starttime = $redis->HGET($activityHashKey, 'starttime');
$endtime = $redis->HGET($activityHashKey, 'endtime');
if ($starttime < time() && $endtime > time()) {
$status = 'ing';
}else if ($starttime > time()) {
$status = 'nostart';
}else if ($endtime < time()) {
$status = 'ended';
}
return $status;
}
/**
* 获取活动类型数组的所有活动hashkeys
*
* @param array|string $activityTypes
* @return array
*/
private function getActivityHashKeysByType($activityTypes = [], $activity_id = 0) {
$redis = $this->getRedis();
$activityTypes = is_array($activityTypes) ? $activityTypes : [$activityTypes];
$activityTypes = array_values(array_filter($activityTypes)); // 过滤空值
// 获取活动集合
$hashList = $redis->ZRANGE($this->zsetKey, 0, 999999999);
// 优先判断 activity_id,可以唯一确定 活动key, 不需要判断 activityTypes
if ($activity_id) {
$activityHashKeys = [];
foreach ($hashList as $hashKey) {
$suffix = ':' . $activity_id;
// 判断是否是要找的活动id, 截取 hashKey 后面几位,是否为当前要查找的活动 id
if (substr($hashKey, (strlen($hashKey) - strlen($suffix))) == $suffix) {
$activityHashKeys[] = $hashKey;
break;
}
}
return $activityHashKeys;
}
// 判断是否传入了 需要的活动类型,默认取全部活动
if ($activityTypes) {
// 获取对应的活动类型的集合
$activityHashKeys = [];
foreach ($hashList as $hashKey) {
// 循环要查找的活动类型数组
foreach ($activityTypes as $type) {
if (strpos($hashKey, $type) !== false) { // 是要查找的类型
$activityHashKeys[] = $hashKey;
break;
}
}
}
} else {
// 全部活动
$activityHashKeys = $hashList;
}
return $activityHashKeys;
}
// ------------------------格式化活动---------------------
/**
* 格式化活动
*
* @param string array $activityHash 活动 key 或者活动完整信息
* @param string $type 格式化方式
* @param array $data 额外参数
* @return void
*/
public function formatActivityByType($activityHash, $type = 'normal', $data = []) {
switch($type) {
case 'normal' :
// 正常模式,只移除销量,团信息,保留全部商品规格数据
$activity = $this->getActivityFormatNormal($activityHash, $data);
break;
case 'clear' :
// 简洁模式,只保留活动表基本信息
$activity = $this->getActivityFormatClear($activityHash, $data);
break;
case 'goods' :
// 按照前端商品方式格式化
$activity = $this->getActivityFormatGoods($activityHash, $data);
break;
case 'discount' :
$activity = $this->getActivityFormatDiscount($activityHash, $data);
break;
default :
$activity = $this->getActivityFormatNormal($activityHash, $data);
break;
}
return $activity;
}
/**
* 正常模式,只移除销量, 团信息,保留全部商品规格数据
*
* @param string $activityHashKey
* @param array $data 额外数据,商品 id
* @return void
*/
private function getActivityFormatNormal($activityHashKey, $data = []) {
// 传入的是活动的key
$activityHash = $this->getActivityByHashKey($activityHashKey);
$activity = [];
foreach ($activityHash as $key => $value) {
// 包含 -sale 全部跳过
if (strpos($key, '-sale') !== false) {
continue;
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
// 拼团的参团人数,团用户,移除
continue;
} else if ($key == 'rules') {
$activity[$key] = json_decode($value, true);
} else {
// 普通键值
$activity[$key] = $value;
}
}
if ($activity) {
// 处理活动状态
$activity['status_code'] = Activity::getStatusCode($activity);
}
return $activity ?: null;
}
/**
* 简洁模式,只保留活动表基本信息
*
* @param string $activityHashKey
* @param array $data 额外数据,商品 id
* @return void
*/
private function getActivityFormatClear($activityHashKey, $data = []) {
$activityHash = $this->getActivityByHashKey($activityHashKey);
$activity = [];
foreach ($activityHash as $key => $value) {
// 包含 -sale 全部跳过
if (strpos($key, $this->hashGoodsPrefix) !== false) {
continue;
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
// 拼团的参团人数,团用户,移除
continue;
} else if ($key == 'rules') {
$activity[$key] = json_decode($value, true);
} else {
// 普通键值
$activity[$key] = $value;
}
}
if ($activity) {
// 处理活动状态
$activity['status_code'] = Activity::getStatusCode($activity);
}
return $activity ?: null;
}
/**
* 获取并按照商品展示格式化活动数据
*
* @param string $activityHashKey hash key
* @param array $data 额外数据,商品 id
* @return array
*/
private function getActivityFormatGoods($activityHashKey, $data = [])
{
$goods_id = $data['goods_id'] ?? 0;
// 传入的是活动的key
$activityHash = $this->getActivityByHashKey($activityHashKey);
$activity = [];
// 商品前缀
$goodsPrefix = $this->hashGoodsPrefix . ($goods_id ? $goods_id . '-' : '');
foreach ($activityHash as $key => $value) {
// 包含 -sale 全部跳过
if (strpos($key, '-sale') !== false) {
continue;
} else if (strpos($key, $goodsPrefix) !== false) {
// 商品规格信息,或者特定商品规格信息
$goods = json_decode($value, true);
// 计算销量库存数据
$goods = $this->calcGoods($goods, $activityHashKey);
// 商品规格项
$activity['activity_goods_sku_price'][] = $goods;
} else if ($goods_id && strpos($key, $this->hashGoodsPrefix) !== false) {
// 需要特定商品时,移除别的非当前商品的数据
continue;
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
// 拼团的参团人数,团用户,移除
continue;
} else if ($key == 'rules') {
$activity[$key] = json_decode($value, true);
} else {
// 普通键值
$activity[$key] = $value;
}
}
if ($activity) {
// 处理活动状态
$activity['status_code'] = Activity::getStatusCode($activity);
}
return $activity ?: null;
}
/**
* 获取并按照折扣格式展示格式化活动数据
*
* @param string $activityHashKey hash key
* @param array $data 额外数据
* @return void
*/
private function getActivityFormatDiscount($activityHashKey, $data = [])
{
$activityHash = $this->getActivityByHashKey($activityHashKey);
$activity = [];
foreach ($activityHash as $key => $value) {
if ($key == 'rules') {
$rules = json_decode($value, true);
// 存在折扣
if (isset($rules['discounts']) && $rules['discounts']) {
// 处理展示优惠,full 从小到大
$discounts = $rules['discounts'] ?? [];
$discountsKeys = array_column($discounts, null, 'full');
ksort($discountsKeys);
$rules['discounts'] = array_values($discountsKeys); // 优惠按照 full 从小到大排序
}
$activity[$key] = $rules;
} else {
// 普通键值
$activity[$key] = $value;
}
}
if ($activity) {
// 处理活动状态
$activity['status_code'] = Activity::getStatusCode($activity);
}
return $activity ?: null;
}
/**
* 计算每个规格的真实库存、销量
*
* @param [type] $goods
* @param [type] $activityHashKey
* @return void
*/
private function calcGoods($goods, $activityHashKey)
{
$redis = $this->getRedis();
// 销量 key
$saleKey = $this->getHashGoodsKey($goods['goods_id'], $goods['sku_price_id'], true);
// 缓存中的销量
$cacheSale = $redis->HGET($activityHashKey, $saleKey);
$stock = $goods['stock'] - $cacheSale;
$goods['stock'] = $stock > 0 ? $stock : 0;
$goods['sales'] = $cacheSale;
return $goods;
}
// 拼接 hash key
private function getHashKey($activity_id, $activity_type) {
// 示例 hash-activity:groupon:25
return $this->hashPrefix . $activity_type . ':' . $activity_id;
}
// 拼接 hash 表中 goods 的 key
private function getHashGoodsKey($goods_id, $sku_price_id, $is_sale = false)
{
// 示例 商品规格:goods-25-30 or 商品规格销量:goods-25-30-sale
return $this->hashGoodsPrefix . $goods_id . '-' . $sku_price_id . ($is_sale ? '-sale' : '');
}
// 拼接 hash 表中 groupon 的 key
private function getHashGrouponKey($groupon_id, $goods_id, $type = '')
{
return $this->hashGrouponPrefix . $groupon_id . '-' . $goods_id . ($type ? '-' . $type : '');
}
// 获取 key 集合
public function getKeys($detail, $activity)
{
// 获取 hash key
$activityHashKey = $this->getHashKey($activity['activity_id'], $activity['activity_type']);
$goodsSkuPriceKey = '';
$saleKey = '';
if (isset($detail['goods_sku_price_id']) && $detail['goods_sku_price_id']) {
// 获取 hash 表中商品 sku 的 key
$goodsSkuPriceKey = $this->getHashGoodsKey($detail['goods_id'], $detail['goods_sku_price_id']);
// 获取 hash 表中商品 sku 的 销量的 key
$saleKey = $this->getHashGoodsKey($detail['goods_id'], $detail['goods_sku_price_id'], true);
}
// 需要拼团的字段
$grouponKey = '';
$grouponNumKey = '';
$grouponUserlistKey = '';
if (isset($detail['groupon_id']) && $detail['groupon_id']) {
// 获取 hash 表中团 key
$grouponKey = $this->getHashGrouponKey($detail['groupon_id'], $detail['goods_id']);
// 获取 hash 表中团当前人数 key
$grouponNumKey = $this->getHashGrouponKey($detail['groupon_id'], $detail['goods_id'], 'num');
// 获取 hash 表中团当前人员列表 key
$grouponUserlistKey = $this->getHashGrouponKey($detail['groupon_id'], $detail['goods_id'], 'userlist');
}
return compact('activityHashKey', 'goodsSkuPriceKey', 'saleKey', 'grouponKey', 'grouponNumKey', 'grouponUserlistKey');
}
}