- 框架初始化
 - 安装插件
 - 修复PHP8.4报错
This commit is contained in:
2025-04-19 17:21:20 +08:00
commit c6a4e1f5f6
5306 changed files with 967782 additions and 0 deletions

View File

@@ -0,0 +1,576 @@
<?php
namespace addons\shopro\library\activity\traits;
use addons\shopro\facade\Redis;
use app\admin\model\shopro\activity\Activity;
use think\helper\Str;
/**
* 获取活动 redis 基础方法
*/
trait ActivityRedis
{
protected $zsetKey = 'zset-activity'; // 活动集合 key
protected $hashPrefix = 'hash-activity:'; // 活动前缀
protected $hashGoodsPrefix = 'goods-'; // 活动中商品的前缀
protected $hashGrouponPrefix = 'groupon-';
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动相关信息 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/**
* 获取活动完整信息
*
* @param integer $id
* @param string $type
* @return array
*/
public function getActivity($id, $type)
{
$keyActivity = $this->keyActivity($id, $type);
return $this->getActivityByKey($keyActivity);
}
/**
* 通过活动的键值,获取活动完整信息
*
* @param string $activityHashKey
* @return array
*/
public function getActivityByKey($keyActivity)
{
// 取出整条 hash 记录
$activity = Redis::HGETALL($keyActivity);
return $activity;
}
/**
* 删除活动
*
* @param integer $id
* @param string $type
* @return void
*/
public function delActivity($id, $type)
{
$keyActivity = $this->keyActivity($id, $type);
$this->delActivityByKey($keyActivity);
}
/**
* 通过 key 删除活动
*
* @param string $keyActivity
* @return void
*/
public function delActivityByKey($keyActivity)
{
// 删除 hash
Redis::DEL($keyActivity);
// 删除集合
Redis::ZREM($this->zsetKey, $keyActivity);
}
/**
* 获取活动的状态
*
* @param string $keyActivity
* @return string
*/
public function getActivityStatusByKey($keyActivity)
{
$prehead_time = Redis::HGET($keyActivity, 'prehead_time'); // 预热时间
$start_time = Redis::HGET($keyActivity, 'start_time'); // 开始时间
$end_time = Redis::HGET($keyActivity, 'end_time'); // 结束时间
// 获取活动状态
$status = Activity::getStatusCode($prehead_time, $start_time, $end_time);
return $status;
}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动相关信息 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 操作活动 hash ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/**
* 计算每个规格的真实库存、销量
*
* @param array $goods
* @param string $keyActivity
* @return array
*/
private function calcGoods($goods, $keyActivity)
{
// 销量 key
$keyActivityGoods = $this->keyActivityGoods($goods['goods_id'], $goods['goods_sku_price_id'], true);
// 缓存中的销量
$cacheSale = Redis::HGET($keyActivity, $keyActivityGoods);
$stock = $goods['stock'] - $cacheSale;
$goods['stock'] = $stock > 0 ? $stock : 0;
$goods['sales'] = $cacheSale;
return $goods;
}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 操作活动 hash ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 格式化活动内容 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/**
* 格式化活动
*
* @param string array $keyActivity 活动 key 或者活动完整信息
* @param string $type 格式化方式
* @param array $data 额外参数
* @return array
*/
public function formatActivityByKey($keyActivity, $type = 'normal', $data = [])
{
$activity = $this->{'formatActivity' . Str::studly($type)}($keyActivity, $data);
return $activity;
}
/**
* 正常模式,只移除销量, 团信息,保留全部商品规格数据
*
* @param string|array $originalActivity
* @param array $data 额外数据,商品 id
* @return array|null
*/
public function formatActivityNormal($originalActivity, $data = [])
{
if (is_string($originalActivity)) {
// 传入的是活动的key
$keyActivity = $originalActivity;
$originalActivity = $this->getActivityByKey($originalActivity);
} else {
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
}
$activity = [];
foreach ($originalActivity 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'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
$activity['status_text'] = Activity::getStatusText($activity['status']);
}
return $activity ?: null;
}
/**
* 简洁模式,只保留活动表基本信息
*
* @param string $originalActivity
* @param array $data 额外数据,商品 id
* @return array|null
*/
private function formatActivityClear($originalActivity, $data = [])
{
if (is_string($originalActivity)) {
// 传入的是活动的key
$keyActivity = $originalActivity;
$originalActivity = $this->getActivityByKey($originalActivity);
} else {
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
}
$activity = [];
foreach ($originalActivity 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'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
$activity['status_text'] = Activity::getStatusText($activity['status']);
}
return $activity ?: null;
}
/**
* 获取并按照商品展示格式化活动数据
*
* @param string $originalActivity hash key
* @param array $data 额外数据,商品 id
* @return array|null
*/
private function formatActivityGoods($originalActivity, $data = [])
{
$goods_id = $data['goods_id'] ?? 0;
if (is_string($originalActivity)) {
// 传入的是活动的key
$keyActivity = $originalActivity;
$originalActivity = $this->getActivityByKey($originalActivity);
} else {
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
}
$activity = [];
// 商品前缀
$goodsPrefix = $this->hashGoodsPrefix . ($goods_id ? $goods_id . '-' : '');
foreach ($originalActivity 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, $keyActivity);
// 商品规格项
$activity['activity_sku_prices'][] = $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'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
$activity['status_text'] = Activity::getStatusText($activity['status']);
}
return $activity ?: null;
}
/**
* 获取并按照折扣格式展示格式化活动数据
*
* @param string $originalActivity hash key
* @param array $data 额外数据
* @return array|null
*/
public function formatActivityPromo($originalActivity, $data = [])
{
if (is_string($originalActivity)) {
// 传入的是活动的key
$keyActivity = $originalActivity;
$originalActivity = $this->getActivityByKey($originalActivity);
} else {
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
}
$activity = [];
foreach ($originalActivity as $key => $value) {
if ($key == 'rules') {
$rules = json_decode($value, true);
$activity[$key] = $rules;
} else {
// 普通键值
$activity[$key] = $value;
}
}
if ($activity) {
// 处理活动状态
$activity['status'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
$activity['status_text'] = Activity::getStatusText($activity['status']);
}
return $activity ?: null;
}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 格式化活动内容 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动的 keys 数组 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/**
* 获取所有活动的 keys
*
* @return array
*/
public function getKeysActivity()
{
// 获取活动集合
$keysActivity = Redis::ZRANGE($this->zsetKey, 0, 999999999);
return $keysActivity;
}
/**
* 通过活动 id 获取活动的 key(不知道活动类型,只知道 id 的时候用)
*
* @param integer $id
* @return string
*/
public function getKeyActivityById($id)
{
$keysActivity = $this->getKeysActivity();
foreach ($keysActivity as $keyActivity) {
$suffix = ':' . $id;
// 判断是否是要找的活动id, 截取 hashKey 后面几位,是否为当前要查找的活动 id
if (substr($keyActivity, (strlen($keyActivity) - strlen($suffix))) == $suffix) {
$currentKeyActivity = $keyActivity;
break;
}
}
return $currentKeyActivity ?? null;
}
/**
* 通过活动 id 和 活动 type 获取 活动 key
*
* @param integer $activity_id
* @param string $activity_type
* @return string
*/
public function getKeyActivityByIdType($activity_id, $activity_type)
{
$keyActivity = $this->keyActivity($activity_id, $activity_type);
return $keyActivity;
}
/**
* 获取对应活动类型的 活动 keys
*
* @param array $activityTypes
* @param array|string $status 要查询的活动的状态
* @return array
*/
public function getKeysActivityByTypes($activityTypes, $status = 'all')
{
$status = is_array($status) ? $status : [$status];
$activityTypes = is_array($activityTypes) ? $activityTypes : [$activityTypes];
$activityTypes = array_values(array_filter(array_unique($activityTypes))); // 过滤空值
$keysActivity = $this->getKeysActivity();
// 获取对应的活动类型的集合
$keysActivityTypes = [];
foreach ($keysActivity as $keyActivity) {
// 循环要查找的活动类型数组
foreach ($activityTypes as $type) {
$prefix = $this->hashPrefix . $type . ':';
if (strpos($keyActivity, $prefix) === 0) { // 是要查找的类型
$keysActivityTypes[] = $keyActivity;
break;
}
}
}
// 判断活动状态
if (!in_array('all', $status)) {
foreach ($keysActivityTypes as $key => $keyActivity) {
$activity_status = $this->getActivityStatusByKey($keyActivity);
if (!in_array($activity_status, $status)) {
unset($keysActivityTypes[$key]);
}
}
}
return array_values($keysActivityTypes);
}
/**
* 通过商品获取该商品参与的活动的hash key
*
* @param integer $goods_id
* @param Array $activityType
* @param array|string $status 要查询的活动的状态
* @return array
*/
private function getkeysActivityByGoods($goods_id, $activityType = [], $status = 'all')
{
// 获取对应类型的活动集合
$keysActivity = $this->getKeysActivityByTypes($activityType, $status);
$keysActivityGoods = [];
foreach ($keysActivity as $keyActivity) {
// 判断这条活动是否包含该商品
$goods_ids = array_filter(explode(',', Redis::HGET($keyActivity, 'goods_ids')));
if (!in_array($goods_id, $goods_ids) && !empty($goods_ids)) {
continue;
}
$keysActivityGoods[] = $keyActivity;
}
return $keysActivityGoods;
}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动的 keys 数组 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动相关 key ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
/**
* 获取活动 hash 的 key
*
* @param integer $activity_id 活动 id
* @param string $activity_type 活动类型
* @return string
*/
private function keyActivity($activity_id, $activity_type)
{
// 示例 hash-activity:groupon:25
return $this->hashPrefix . $activity_type . ':' . $activity_id;
}
/**
* 获取活动 hash 中 商品相关的 key is_sale 对应的活动商品的销量)
*
* @param integer $goods_id 商品
* @param integer $sku_price_id 规格
* @param boolean $is_sale 对应的活动商品的销量
* @return string
*/
private function keyActivityGoods($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' : '');
}
/**
* 获取活动中 拼团 的团数据的 key
*
* @param integer $groupon_id
* @param integer $goods_id
* @param string $type 空=团 key|num=团人数|users=团用户
* @return string
*/
private function keyActivityGroupon($groupon_id, $goods_id, $type = '')
{
return $this->hashGrouponPrefix . $groupon_id . '-' . $goods_id . ($type ? '-' . $type : '');
}
/**
* 获取活动相关的所有 key
*
* @param array $detail 商品相关数据
* @param array $activity 活动相关数据
* @return array
*/
public function keysActivity($detail, $activity)
{
// 获取 hash key
$keyActivity = $this->keyActivity($activity['activity_id'], $activity['activity_type']);
$keyGoodsSkuPrice = '';
$keySale = '';
if (isset($detail['goods_sku_price_id']) && $detail['goods_sku_price_id']) {
// 获取 hash 表中商品 sku 的 key
$keyGoodsSkuPrice = $this->keyActivityGoods($detail['goods_id'], $detail['goods_sku_price_id']);
// 获取 hash 表中商品 sku 的 销量的 key
$keySale = $this->keyActivityGoods($detail['goods_id'], $detail['goods_sku_price_id'], true);
}
// 需要拼团的字段
$keyGroupon = '';
$keyGrouponNum = '';
$keyGrouponUserlist = '';
if (isset($detail['groupon_id']) && $detail['groupon_id']) {
// 获取 hash 表中团 key
$keyGroupon = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id']);
// 获取 hash 表中团当前人数 key
$keyGrouponNum = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id'], 'num');
// 获取 hash 表中团当前人员列表 key
$keyGrouponUserlist = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id'], 'users');
}
return compact('keyActivity', 'keyGoodsSkuPrice', 'keySale', 'keyGroupon', 'keyGrouponNum', 'keyGrouponUserlist');
}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动相关 key ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}

View File

@@ -0,0 +1,160 @@
<?php
namespace addons\shopro\library\activity\traits;
trait CheckActivity
{
/**
* 检测活动商品必须至少设置一个规格为活动规格
*
* @param array $goodsList
* @return void
*/
public function checkActivitySkuPrice($goodsList)
{
foreach ($goodsList as $key => $goods) {
$activitySkuPrice = $goods['activity_sku_prices'] ?? [];
if (!$activitySkuPrice) {
error_stop('请至少将商品一个规格设置为活动规格');
}
}
}
/**
* 检测满赠规则设置
*
* @param array $discounts
* @return void
*/
public function checkGiftDiscount($discounts)
{
foreach ($discounts as $discount) {
$types = $discount['types'];
if (in_array('coupon', $types) && (!isset($discount['coupon_ids']) || empty($discount['coupon_ids']))) {
// 验证优惠券
error_stop('请选择要赠送的优惠券');
}
if (in_array('money', $types) && (!isset($discount['money']) || empty($discount['money']))) {
// 赠送余额
error_stop('请填写要赠送的余额');
}
if (in_array('score', $types) && (!isset($discount['score']) || empty($discount['score']))) {
// 赠送积分
error_stop('请填写要赠送的积分');
}
if (in_array('goods', $types) && (!isset($discount['goods_ids']) || empty($discount['goods_ids']))) {
// 赠送优惠券
error_stop('请选择要赠送的商品');
}
}
}
/**
* 幸运拼团参与奖校验
*
* @param array $part_gift
* @return void
*/
public function checkLuckyPartGift($part_gift)
{
$types = $part_gift['types'];
if (in_array('coupon', $types) && (!isset($part_gift['coupon_ids']) || empty($part_gift['coupon_ids']))) {
// 验证优惠券
error_stop('请选择要赠送的优惠券');
}
if (in_array('money', $types) && (!isset($discount['money']) || empty($discount['money']))) {
// 赠送余额
error_stop('请填写要赠送的余额');
}
if (in_array('score', $types) && (!isset($discount['score']) || empty($discount['score']))) {
// 赠送积分
error_stop('请填写要赠送的积分');
}
}
/**
* 检测活动商品是否重合
*
* @return void
*/
public function checkActivityConflict($params, $goodsList = [], $activity_id = 0)
{
if ($params['classify'] == 'activity') {
// 活动可以共存,不检测冲突
return true;
}
$start_time = strtotime($params['start_time']);
$end_time = strtotime($params['end_time']);
$prehead_time = isset($params['prehead_time']) && $params['prehead_time'] ? strtotime($params['prehead_time']) : $start_time;
$goodsIds = array_column($goodsList, 'id'); // 获取活动提交过来的所有商品的 id
$goodsList = array_column($goodsList, null, 'id');
// 获取所有时间有交叉的活动
$activities = $this->getActivities($params['type'], [$prehead_time, $end_time]);
foreach ($activities as $key => $activity) {
if ($activity_id && $activity_id == $activity['id']) {
// 编辑的时候,把自己排除在外
continue;
}
$intersect = []; // 两个活动重合的商品Ids
if ($goodsIds) {
$activityGoodsIds = array_filter(explode(',', $activity['goods_ids']));
// 不是全部商品,并且不重合
if ($activityGoodsIds && !$intersect = array_intersect($activityGoodsIds, $goodsIds)) {
// 商品不重合,继续验证下个活动
continue;
}
}
$goods_names = '';
foreach ($intersect as $id) {
if (isset($goodsList[$id]) && isset($goodsList[$id]['title'])) {
$goods_names .= $goodsList[$id]['title'] . ',';
}
}
if ($goods_names) {
$goods_names = mb_strlen($goods_names) > 40 ? mb_substr($goods_names, 0, 37) . '...' : $goods_names;
}
if (!$goodsIds && !$intersect) {
// 没有商品
$msg = '活动时间与 “' . $activity['type_text'] . ' 活动的 ' . $activity['title'] . '” 冲突';
} else {
$msg = ((count($intersect) > 1 || !$goodsIds) ? '部分商品' : '该商品') . ($goods_names ? ' ' . $goods_names . ' ' : '') . '已在 “' . $activity['title'] . '” 活动中设置';
}
error_stop($msg);
}
}
/**
* 获取所有活动
*
* @param string $current_activity_type
* @param array $range 要查询的时间区间
* @return array
*/
private function getActivities($current_activity_type, $range) {
// 获取当前活动的互斥活动
$activityTypes = $this->manager->model->getMutexActivityTypes($current_activity_type);
$activities = $this->manager->getActivitiesByRange($range, $activityTypes);
return $activities;
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace addons\shopro\library\activity\traits;
use think\Db;
use addons\shopro\traits\CouponSend;
use app\admin\model\shopro\activity\GiftLog;
use addons\shopro\service\Wallet as WalletService;
/**
* 赠送赠品 full_gift, groupon_lucky 幸运拼团未拼中)
*/
trait GiveGift
{
use CouponSend;
/**
* 按照规则添加赠送日志
* @param array|object $order
* @param array|object $user
* @param array $info {"full":"100","types":"coupon=优惠券|score=积分|money=余额|goods=商品","coupon_ids":"赠优惠券时存在","total":"赠送优惠券总金额","score":"积分","money":"余额","goods_ids":"商品时存在",gift_num:"礼品份数"}
* @return void
*/
public function addGiftsLog($order, $user, $info)
{
$rules = $info['discount_rule'];
Db::transaction(function () use ($order, $user, $info, $rules) {
$types = $rules['types'];
foreach ($types as $type) {
extract($this->getTypeGift($rules, $type));
$giftLog = new GiftLog();
$giftLog->activity_id = $info['activity_id'];
$giftLog->order_id = $order->id;
$giftLog->user_id = $user->id;
$giftLog->type = $type;
$giftLog->gift = $gift;
$giftLog->value = $value;
$giftLog->rules = $rules;
$giftLog->status = 'waiting';
$giftLog->save();
}
});
}
/**
* 标记礼品为赠送失败
*/
public function checkAndFailGift($order, $fail_msg, $errors = null)
{
// 找到所有没有赠送的礼品,设置为 fail,fail_msg 订单退款
$giftLogs = GiftLog::waiting()->where('order_id', $order->id)->lock(true)->select();
foreach ($giftLogs as $giftLog) {
$giftLog->status = 'fail';
$giftLog->fail_msg = $fail_msg;
$giftLog->errors = $errors;
$giftLog->save();
}
}
/**
* 检查并赠送礼品
*
* @param array|object $order
* @param array|object $user
* @param array $promoInfos
* @param string $event
* @return void
*/
public function checkAndGift($order, $user, $promoInfos, $event)
{
foreach ($promoInfos as $info) {
if ($info['activity_type'] == 'full_gift') {
$this->checkPromoAndGift($order, $user, $info, $event);
}
}
}
/**
* 检查并赠送礼品
*
* @param array|object $order
* @param array|object $user
* @param array $infos
* @param string $event
* @return void
*/
public function checkPromoAndGift($order, $user, $info, $event)
{
if ($info['event'] == $event) {
// 判断领取次数
$rules = $info['discount_rule'];
$gift_num = $rules['gift_num']; // 礼品数量
// 查询已发放数量
$send_num = GiftLog::where('activity_id', $info['activity_id'])->opered()->group('order_id')->count();
$giftLogs = GiftLog::waiting()
->where('activity_id', $info['activity_id'])
->where('order_id', $order->id)
->select();
if ($send_num >= $gift_num) {
// 礼品已经发放完毕
foreach ($giftLogs as $log) {
$log->status = 'fail';
$log->fail_msg = '礼品已经发完了';
$log->save();
}
return false;
}
// 查询当前用户已领取数量 (只算赠送成功的)
$user_send_num = GiftLog::where('user_id', $order->user_id)->where('activity_id', $info['activity_id'])->finish()->group('order_id')->count();
if ($info['limit_num'] > 0 && $user_send_num >= $info['limit_num']) {
// 已经领取过了
foreach ($giftLogs as $log) {
$log->status = 'fail';
$log->fail_msg = '已经领取过了,每人最多领取 ' . $info['limit_num'] . ' 份';
$log->save();
}
return false;
}
// 赠送礼品
foreach ($giftLogs as $giftLog) {
$this->{'gift' . $giftLog->type}($user, $giftLog);
}
}
}
/**
* 赠送优惠券
*
* @param array|object $user
* @param array|object $giftLog
* @return void
*/
public function giftCoupon($user, $giftLog)
{
$couponIds = explode(',', $giftLog->gift);
$result = $this->giveCoupons($user, $couponIds);
$giftLog->status = 'finish';
if ($result['errors']) {
$giftLog->status = 'fail';
$giftLog->fail_msg = $result['success'] ? '优惠券部分发放成功' : '优惠券发放失败';
$giftLog->errors = $result['errors'];
}
$giftLog->save();
}
/**
* 赠送积分
*
* @param array|object $user
* @param array|object $giftLog
* @return void
*/
public function giftScore($user, $giftLog)
{
$score = $giftLog->gift;
// 增加用户积分
WalletService::change($user, 'score', $score, 'activity_gift', [
'activity_id' => $giftLog->activity_id,
'order_id' => $giftLog->order_id,
'user_id' => $giftLog->user_id,
'type' => $giftLog->type,
'gift' => $giftLog->gift,
'value' => $giftLog->value,
]);
$giftLog->status = 'finish';
$giftLog->save();
}
/**
* 赠送余额
*
* @param array|object $user
* @param array|object $giftLog
* @return void
*/
public function giftMoney($user, $giftLog)
{
$money = $giftLog->gift;
// 增加用户余额
WalletService::change($user, 'money', $money, 'activity_gift', [
'activity_id' => $giftLog->activity_id,
'order_id' => $giftLog->order_id,
'user_id' => $giftLog->user_id,
'type' => $giftLog->type,
'gift' => $giftLog->gift,
'value' => $giftLog->value,
]);
$giftLog->status = 'finish';
$giftLog->save();
}
/**
* 赠送商品(暂不开发)
*
* @param array|object $user
* @param array|object $giftLog
* @return void
*/
public function giftGoods($user, $giftLog)
{
$goodsIds = explode(',', $giftLog->gift);
// 赠送商品,暂不开发
$giftLog->status = 'finish';
$giftLog->save();
}
/**
* 获取赠送的 gift 和价值
*
* @param array $rules
* @param string $type
* @return array
*/
private function getTypeGift($rules, $type)
{
$gift = null;
switch ($type) {
case 'coupon':
$gift = $rules['coupon_ids'];
$value = $rules['total'];
break;
case 'score':
$gift = $rules['score'];
$value = $rules['score'];
break;
case 'money':
$gift = $rules['money'];
$value = $rules['money'];
break;
case 'goods':
$gift = $rules['goods_ids'];
$value = $rules['goods_ids'];
break;
}
return compact('gift', 'value');
}
}

View File

@@ -0,0 +1,527 @@
<?php
namespace addons\shopro\library\activity\traits;
use addons\shopro\facade\Redis;
use addons\shopro\facade\ActivityRedis;
use app\admin\model\shopro\activity\Activity;
use app\admin\model\shopro\activity\Groupon as ActivityGroupon;
use app\admin\model\shopro\activity\GrouponLog;
use app\admin\model\shopro\order\Order;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\data\FakeUser;
use addons\shopro\service\order\OrderRefund;
use addons\shopro\service\order\OrderOper;
/**
* 拼团 (普通拼团,阶梯拼团,幸运拼团)
*/
trait Groupon
{
/**
* *、redis 没有存团完整信息,只存了团当前人数,团成员(当前人数,团成员均没有存虚拟用户)
* *、redis userList 没有存这个人的购买状态
* *、团 解散,成团,(因为直接修改了数据库,参团判断,先判断的数据库后判断的 redis
* *、虚拟成团时将虚拟人数存入 redis userList 中因为团中有虚拟人时redis 实际人数 和 团需要人数 都没有计算虚拟人,导致团可以超员
*/
/**
* 判断加入旧拼团
*/
protected function checkAndGetJoinGroupon($buyInfo, $user, $groupon_id)
{
$goods = $buyInfo['goods'];
$activity = $goods['activity'];
// 获取团信息
$activityGroupon = ActivityGroupon::where('id', $groupon_id)->find();
if (!$activityGroupon) {
error_stop('要参与的团不存在');
}
// 判断团所属活动是否正常
if ($activityGroupon->activity_id != $activity['id']) { // 修复,后台手动将活动删除,然后又立即给这个商品创建新的拼团活动,导致参与新活动的旧团错乱问题
error_stop('要参与的活动已结束');
}
if ($activityGroupon['status'] != 'ing') {
error_stop('要参与的团已成团,请选择其它团或自己开团');
}
if ($activityGroupon['current_num'] >= $activityGroupon['num']) {
error_stop('该团已满,请参与其它团或自己开团');
}
if (!has_redis()) {
// 没有 redis 直接判断数据库团信息,因为 current_num 支付成功才会累加,故无法保证超员,
$isJoin = GrouponLog::where('user_id', $user['id'])->where('groupon_id', $activityGroupon->id)->where('is_fictitious', 0)->count();
if ($isJoin) {
error_stop('您已参与该团,请不要重复参团');
}
// 该团可加入
return $activityGroupon;
}
$keys = ActivityRedis::keysActivity([
'groupon_id' => $activityGroupon['id'],
'goods_id' => $activityGroupon['goods_id'],
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
$current_num = Redis::HGET($keyActivity, $keyGrouponNum);
if ($current_num >= $activityGroupon['num']) {
error_stop('该团已满,请参与其它团或自己开团');
}
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,订单失效时删除缓存
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
$userList = json_decode($userList, true);
$userIds = array_column($userList, 'user_id');
if (in_array($user['id'], $userIds)) {
error_stop('您已参与该团,请不要重复参团');
}
return $activityGroupon;
}
/**
* 增加拼团预成员人数
*/
protected function grouponCacheForwardNum($activityGroupon, $activity, $user, $payed = 'nopay')
{
if (!has_redis()) {
return true;
}
$keys = ActivityRedis::keysActivity([
'groupon_id' => $activityGroupon['id'],
'goods_id' => $activityGroupon['goods_id'],
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
// 当前团人数 grouponNumKey 如果不存在,自动创建
$current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, 1);
if ($current_num > $activityGroupon['num']) {
// 再把刚加上的减回来
$current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
error_stop('该团已满,请参与其它团或自己开团');
}
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
$userList = json_decode($userList, true);
$userList = $userList ?: [];
$userList[] = [
'user_id' => $user['id'],
// 'status' => $payed // 太复杂,先不做
];
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
}
// 拼团团成员预成员退回
protected function grouponCacheBackNum($order, $type)
{
if (!has_redis()) {
return true;
}
// 查询拼团商品
$item = OrderItem::where('order_id', $order['id'])->find(); // 拼团订单只有一个商品
// 扩展字段
$order_ext = $order['ext'];
// 团 id
$groupon_id = $order_ext['groupon_id'] ?? 0;
if (!$groupon_id) {
return true; // 商品独立购买,未参团,或者开新团
}
// 查询拼团,必须是拼团中才处理(已结束的(完成或者解散的没意义了)),redis 中没有存 团信息和状态
$groupon = ActivityGroupon::ing()->lock(true)->find($groupon_id);
if (!$groupon) {
return true;
}
// if ($type == 'refund') { // 退款这里不删除拼团记录,当成正常团成员处理
// // 退款,真实删除拼团记录,并减少参团人数
// $this->delGrouponLog($order, $groupon);
// }
$keys = ActivityRedis::keysActivity([
'groupon_id' => $groupon_id,
'goods_id' => $item['goods_id'],
'goods_sku_price_id' => $item['goods_sku_price_id'],
], [
'activity_id' => $item['activity_id'],
'activity_type' => $item['activity_type'],
]);
extract($keys);
if (!Redis::EXISTS($keyActivity)) {
// redis 不存在,可能活动已删除,不处理
return true;
}
// 扣除预参团成员
if (Redis::HEXISTS($keyActivity, $keyGrouponNum)) {
$groupon_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
}
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
$userList = json_decode($userList, true);
$userList = $userList ?: [];
foreach ($userList as $key => $user) {
if ($user['user_id'] == $item['user_id']) {
unset($userList[$key]);
}
}
$userList = array_values($userList);
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
}
/**
* 支付成功真实加入团
*/
protected function joinGroupon($order, $user, \Closure $grouponCb = null)
{
$items = $order->items;
$item = $items[0]; // 拼团只能单独购买
// 扩展字段
$order_ext = $order['ext'];
// 团 id
$groupon_id = $order_ext['groupon_id'] ?? 0;
$buy_type = $order_ext['buy_type'] ?? 'groupon';
// 单独购买,不加入团
if ($buy_type == 'alone') {
return true;
}
if ($groupon_id) {
// 加入旧团,查询团
$activityGroupon = ActivityGroupon::find($groupon_id);
} else {
// 加入新团,创建团
$activityGroupon = $this->joinNewGroupon($order, $user, $item, $grouponCb);
}
// 添加参团记录
$activityGrouponLog = $this->addGrouponLog($order, $user, $item, $activityGroupon);
return $this->checkGrouponStatus($activityGroupon);
}
/**
* 支付成功开启新拼团
*/
protected function joinNewGroupon($order, $user, $item, \Closure $grouponCb = null)
{
// 获取活动
$activity = Activity::where('id', $item['activity_id'])->find();
$rules = $activity['rules'];
// 小于 0 不限结束时间单位小时
$expire_time = 0;
if (isset($rules['valid_time']) && $rules['valid_time'] > 0) {
// 转为 秒
$expire_time = $rules['valid_time'] * 3600;
}
// 小于 0 不限结束时间单位小时
$fictitious_time = 0;
if (isset($rules['is_fictitious']) && $rules['is_fictitious'] && isset($rules['fictitious_time']) && $rules['fictitious_time'] > 0) {
// 转为 秒
$fictitious_time = $rules['fictitious_time'] * 3600;
}
if ($grouponCb) {
// team_num
extract($grouponCb($rules, $item['ext']));
}
// 开团
$activityGroupon = new ActivityGroupon();
$activityGroupon->user_id = $user['id'];
$activityGroupon->goods_id = $item['goods_id'];
$activityGroupon->activity_id = $item['activity_id'];
$activityGroupon->num = $team_num ?? 1; // 避免活动找不到
$activityGroupon->current_num = 0; // 真实团成员等支付完成之后再增加
$activityGroupon->status = 'ing';
$activityGroupon->expire_time = $expire_time > 0 ? (time() + $expire_time) : 0;
$activityGroupon->save();
// 记录团 id
$order->ext = array_merge($order->ext, ['groupon_id' => $activityGroupon->id]);
$order->save();
// 将团信息存入缓存,增加缓存中当前团人数
$this->grouponCacheForwardNum($activityGroupon, $activity, $user, 'payed');
if ($expire_time > 0) {
// 增加自动关闭拼团队列(如果有虚拟成团,会判断虚拟成团)
\think\Queue::later($expire_time, '\addons\shopro\job\GrouponAutoOper@expire', [
'activity' => $activity,
'activity_groupon_id' => $activityGroupon->id
], 'shopro');
}
if ($fictitious_time > 0) {
// 自动虚拟成团时间(提前自动虚拟成团,让虚拟成团更加真实一点,避免在团结束那一刻突然成团了)应小于自动过期时间
\think\Queue::later($fictitious_time, '\addons\shopro\job\GrouponAutoOper@fictitious', [
'activity' => $activity,
'activity_groupon_id' => $activityGroupon->id
], 'shopro');
}
return $activityGroupon;
}
/**
* 增加团成员记录
*/
protected function addGrouponLog($order, $user, $item, $activityGroupon)
{
if (!$activityGroupon) {
\think\Log::error('groupon-notfund: order_id: ' . $order['id']);
return null;
}
// 增加团成员数量
$activityGroupon->setInc('current_num', 1);
// 增加参团记录
$activityGrouponLog = new GrouponLog();
$activityGrouponLog->user_id = $user['id'];
$activityGrouponLog->nickname = $user['nickname'];
$activityGrouponLog->avatar = $user['avatar'];
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
$activityGrouponLog->goods_id = $item['goods_id'];
$activityGrouponLog->goods_sku_price_id = $item['goods_sku_price_id'];
$activityGrouponLog->activity_id = $item['activity_id'];
$activityGrouponLog->is_leader = ($activityGroupon['user_id'] == $user['id']) ? 1 : 0;
$activityGrouponLog->is_fictitious = 0;
$activityGrouponLog->order_id = $order['id'];
$activityGrouponLog->save();
return $activityGrouponLog;
}
/**
* 【此方法即将废除,加入团之后,不删除参团记录】,删除团成员记录(退款:已经真实加入团了,这里扣除)()
*/
protected function delGrouponLog($order, $groupon)
{
$activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
->where('groupon_id', $groupon->id)
->where('order_id', $order->id)
->find();
if ($activityGrouponLog) {
$activityGrouponLog->delete();
// 扣除参团人数
$groupon->setDec('current_num', 1);
}
}
/**
* 订单退款时标记拼团记录为已退款(主动退款和拼团失败退款)
*
* @param \think\Model $order
* @return void
*/
protected function refundGrouponLog($order)
{
$order_ext = $order['ext'];
$groupon_id = $order_ext['groupon_id'] ?? 0;
if (!$groupon_id) {
return true; // 商品独立购买,未参团,或者开新团
}
$activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
->where('groupon_id', $groupon_id)
->where('order_id', $order->id)
->find();
if ($activityGrouponLog) {
// 修改 logs 为已退款
$activityGrouponLog->is_refund = 1;
$activityGrouponLog->save();
}
}
// 虚拟成团,增加虚拟成员,并判断是否完成,然后将团状态改为,虚拟成团成功
protected function finishFictitiousGroupon($activity, $activityGroupon, $invalid = true, $num = 0, $users = [])
{
// 拼团剩余人数
$surplus_num = $activityGroupon['num'] - $activityGroupon['current_num'];
// 团已经满员
if ($surplus_num <= 0) {
if ($activityGroupon['status'] == 'ing') {
// 已满员但还是进行中状态,检测并完成团,起到纠正作用
return $this->checkGrouponStatus($activityGroupon);
}
return true;
}
// 本次虚拟人数, 如果传入 num 则使用 num 和 surplus_num 中最小值, 如果没有传入,默认剩余人数全部虚拟
$fictitious_num = $num ? ($num > $surplus_num ? $surplus_num : $num) : $surplus_num;
$fakeUsers = FakeUser::orderRaw('rand()')->limit($fictitious_num)->select();
if (count($fakeUsers) < $fictitious_num && $num == 0) {
if ($invalid) {
// 虚拟用户不足,并且是自动虚拟成团进程,自动解散团
return $this->invalidRefundGroupon($activityGroupon);
}
return false;
}
// 增加团人数
$activityGroupon->setInc('current_num', $fictitious_num);
if (has_redis()) {
// redis 参数
$keys = ActivityRedis::keysActivity([
'groupon_id' => $activityGroupon['id'],
'goods_id' => $activityGroupon['goods_id'],
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
Redis::HINCRBY($keyActivity, $keyGrouponNum, $fictitious_num); // 增加 redis 参团人数
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
$userList = json_decode($userList, true);
$userList = $userList ?: [];
for ($i =0; $i < $fictitious_num; $i++) {
$userList[] = [
'user_id' => 'fictitiou_' . time() . mt_rand(1000, 9999),
];
}
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
}
for ($i = 0; $i < $fictitious_num; $i++) {
// 先用传过来的
$avatar = isset($users[$i]['avatar']) ? $users[$i]['avatar'] : '';
$nickname = isset($users[$i]['nickname']) ? $users[$i]['nickname'] : '';
// 如果没有,用查的虚拟的
$avatar = $avatar ?: $fakeUsers[$i]['avatar'];
$nickname = $nickname ?: $fakeUsers[$i]['nickname'];
// 增加参团记录
$activityGrouponLog = new GrouponLog();
$activityGrouponLog->user_id = 0;
$activityGrouponLog->nickname = $nickname;
$activityGrouponLog->avatar = $avatar;
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
$activityGrouponLog->goods_id = $activityGroupon['goods_id'];
$activityGrouponLog->goods_sku_price_id = 0; // 没有订单,所以也就没有 goods_sku_price_id
$activityGrouponLog->activity_id = $activityGroupon['activity_id'];
$activityGrouponLog->is_leader = 0; // 不是团长
$activityGrouponLog->is_fictitious = 1; // 虚拟用户
$activityGrouponLog->order_id = 0; // 虚拟成员没有订单
$activityGrouponLog->save();
}
return $this->checkGrouponStatus($activityGroupon);
}
/**
* 团过期退款,或者后台手动解散退款
*/
protected function invalidRefundGroupon($activityGroupon, $user = null)
{
$activityGroupon->status = 'invalid'; // 拼团失败
$activityGroupon->save();
// 查询参团真人
$logs = GrouponLog::with(['order'])->where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 0)->select();
foreach ($logs as $key => $log) {
$order = $log->order;
if ($order && in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) {
$refundNum = OrderItem::where('order_id', $order->id)->where('refund_status', '<>', OrderItem::REFUND_STATUS_NOREFUND)->count();
if (!$refundNum) {
// 无条件全额退款
$refund = new OrderRefund($order);
$refund->fullRefund($user, [
'remark' => '拼团失败退款'
]);
}
} else if ($order && $order->isOffline($order)) {
$orderOper = new OrderOper();
$orderOper->cancel($order, null, 'system', '拼团失败,系统自动取消订单');
}
}
// 触发拼团失败行为
$data = ['groupon' => $activityGroupon];
\think\Hook::listen('activity_groupon_fail', $data);
return true;
}
/**
* 检查团状态
*/
protected function checkGrouponStatus($activityGroupon)
{
if (!$activityGroupon) {
return true;
}
// 重新获取团信息
$activityGroupon = ActivityGroupon::where('id', $activityGroupon['id'])->find();
if ($activityGroupon['current_num'] >= $activityGroupon['num'] && !in_array($activityGroupon['status'], ['finish', 'finish_fictitious'])) {
// 查询是否有虚拟团成员
$fictitiousCount = GrouponLog::where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 1)->count();
// 将团设置为已完成
$activityGroupon->status = $fictitiousCount ? 'finish_fictitious' : 'finish';
$activityGroupon->finish_time = time();
$activityGroupon->save();
// 触发成团行为
$data = ['groupon' => $activityGroupon];
\think\Hook::listen('activity_groupon_finish', $data);
}
return true;
}
}