Explorar o código

Merge branch 'dev-rewardPoints' into dev

bianjunhui hai 1 semana
pai
achega
2058bc1e5f

+ 246 - 0
packages/Longyi/RewardPoints/src/Http/Controllers/RewardPointsController.php

@@ -12,6 +12,8 @@ use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Routing\Controller;
 use Illuminate\Routing\Controller;
 use Longyi\RewardPoints\Models\RewardPointHistory;
 use Longyi\RewardPoints\Models\RewardPointHistory;
+use Illuminate\Support\Facades\Redis;
+
 class RewardPointsController extends Controller
 class RewardPointsController extends Controller
 {
 {
     protected $rewardPointRepository;
     protected $rewardPointRepository;
@@ -310,6 +312,248 @@ class RewardPointsController extends Controller
         return ApiResponse::success([], 'Reward points removed successfully');
         return ApiResponse::success([], 'Reward points removed successfully');
     }
     }
 
 
+    /**
+     * 浏览产品赠送积分(每个产品每天仅赠送一次)
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function browseProduct(Request $request)
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $productId = $request->input('categorys');
+
+        if (!$productId) {
+            return ApiResponse::validationError('categorys is required');
+        }
+
+        // 查询有效的浏览产品规则
+        $rule = RewardActiveRule::active()
+            ->ofType(RewardActiveRule::TYPE_BROWSE_PRODUCT)
+            ->first();
+
+        if (!$rule) {
+            return ApiResponse::forbidden('Browse product reward is not available');
+        }
+
+        // 检查规则是否适用于当前客户
+        if (!$rule->isApplicableToCustomer($customer)) {
+            return ApiResponse::forbidden('This reward is not applicable to your account');
+        }
+
+        // Redis Key: browse_product:{customer_id}:{date}:{product_id}
+        $today = Carbon::now()->format('Y-m-d');
+        $redisKey = "browse_product:{$customer->id}:{$today}:{$productId}";
+
+        // Redis 判断该产品今天是否已经浏览过
+        if (Redis::exists($redisKey)) {
+            return ApiResponse::error('You have already earned points for browsing this product today');
+        }
+
+        // 获取该客户应得的积分
+        $points = $rule->getPointsForCustomer($customer);
+        if ($points <= 0) {
+            $points = (int) $rule->reward_point;
+        }
+        if ($points <= 0) {
+            return ApiResponse::error('No points configured for browse product');
+        }
+
+        // 添加积分
+        $this->rewardPointRepository->addPoints(
+            $customer->id,
+            RewardActiveRule::TYPE_BROWSE_PRODUCT,
+            $points,
+            null,
+            "Browsed categorys #{$productId}",
+            $rule
+        );
+
+        // 写入 Redis,TTL = 到当天结束的剩余秒数
+        $endOfDay = Carbon::now()->endOfDay();
+        $ttl = Carbon::now()->diffInSeconds($endOfDay);
+        Redis::setex($redisKey, $ttl, 1);
+
+        // 同时记录到当天已浏览产品集合(方便 getBrowsedProducts 读取)
+        $setKey = "browse_product:{$customer->id}:{$today}:set";
+        Redis::sadd($setKey, $productId);
+        Redis::expire($setKey, $ttl);
+
+        $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
+
+        return ApiResponse::success([
+            'categorys' => $productId,
+            'points' => $points,
+            'total_points' => $totalPoints
+        ], "You earned {$points} points for browsing this product!");
+    }
+
+    /**
+     * 获取今天已浏览并获得积分的产品列表
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function getBrowsedProducts()
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $today = Carbon::now()->format('Y-m-d');
+        $setKey = "browse_product:{$customer->id}:{$today}:set";
+
+        // 从 Redis Set 中获取今天已浏览的产品ID列表
+        $browsedProductIds = Redis::smembers($setKey);
+        $browsedProductIds = array_map('intval', $browsedProductIds);
+
+        return ApiResponse::success([
+            'browsed_product_ids' => $browsedProductIds,
+            'browsed_count' => count($browsedProductIds)
+        ]);
+    }
+
+    /**
+     * 支持的关注平台
+     */
+    const FOLLOW_PLATFORMS = ['ig' => 'Instagram', 'fb' => 'Facebook', 'ytb' => 'YouTube', 'tt' => 'TikTok'];
+
+    /**
+     * 关注社交平台赠送积分(每位用户每个平台仅一次,永久有效)
+     *
+     * @param Request $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function followPlatform(Request $request)
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $platform = $request->input('platform');
+
+        if (!$platform || !isset(self::FOLLOW_PLATFORMS[$platform])) {
+            return ApiResponse::validationError('Invalid platform. Supported: ' . implode(', ', array_keys(self::FOLLOW_PLATFORMS)));
+        }
+
+        // Redis Key: follow_platform:{customer_id}:{platform}(永久有效,不设 TTL)
+        $redisKey = "follow_platform:{$customer->id}:{$platform}";
+
+        // 1. Redis 快速判断(主缓存)
+        if (Redis::exists($redisKey)) {
+            return ApiResponse::error('You have already earned points for following ' . self::FOLLOW_PLATFORMS[$platform]);
+        }
+
+        // 2. DB 兜底检查:防止 Redis 被清理后重复发积分
+        $dbExists = RewardPointHistory::where('customer_id', $customer->id)
+            ->where('type_of_transaction', RewardActiveRule::TYPE_FOLLOW)
+            ->where('transaction_detail', 'Followed ' . self::FOLLOW_PLATFORMS[$platform])
+            ->exists();
+
+        if ($dbExists) {
+            // DB 有记录但 Redis 丢失,回补 Redis
+            Redis::set($redisKey, 1);
+            return ApiResponse::error('You have already earned points for following ' . self::FOLLOW_PLATFORMS[$platform]);
+        }
+
+        // 查询有效的关注规则
+        $rule = RewardActiveRule::active()
+            ->ofType(RewardActiveRule::TYPE_FOLLOW)
+            ->first();
+
+        if (!$rule) {
+            return ApiResponse::forbidden('Follow reward is not available');
+        }
+
+        // 检查规则是否适用于当前客户
+        if (!$rule->isApplicableToCustomer($customer)) {
+            return ApiResponse::forbidden('This reward is not applicable to your account');
+        }
+
+        // 获取该客户应得的积分
+        $points = $rule->getPointsForCustomer($customer);
+        if ($points <= 0) {
+            $points = (int) $rule->reward_point;
+        }
+        if ($points <= 0) {
+            return ApiResponse::error('No points configured for follow reward');
+        }
+
+        // 添加积分
+        $this->rewardPointRepository->addPoints(
+            $customer->id,
+            RewardActiveRule::TYPE_FOLLOW,
+            $points,
+            null,
+            'Followed ' . self::FOLLOW_PLATFORMS[$platform],
+            $rule
+        );
+
+        // 写入 Redis(永久有效,无需 TTL)
+        Redis::set($redisKey, 1);
+
+        $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
+
+        return ApiResponse::success([
+            'platform' => $platform,
+            'platform_name' => self::FOLLOW_PLATFORMS[$platform],
+            'points' => $points,
+            'total_points' => $totalPoints
+        ], 'You earned ' . $points . ' points for following ' . self::FOLLOW_PLATFORMS[$platform] . '!');
+    }
+
+    /**
+     * 获取用户已关注的平台列表及积分状态
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function getFollowStatus()
+    {
+        $customer = auth()->guard('customer')->user();
+
+        if (!$customer) {
+            return ApiResponse::unauthorized();
+        }
+
+        $platforms = [];
+        foreach (self::FOLLOW_PLATFORMS as $code => $name) {
+            $redisKey = "follow_platform:{$customer->id}:{$code}";
+            $followed = (bool) Redis::exists($redisKey);
+
+            // Redis 未命中时,从 DB 回补并修复 Redis
+            if (!$followed) {
+                $dbExists = RewardPointHistory::where('customer_id', $customer->id)
+                    ->where('type_of_transaction', RewardActiveRule::TYPE_FOLLOW)
+                    ->where('transaction_detail', 'Followed ' . $name)
+                    ->exists();
+
+                if ($dbExists) {
+                    Redis::set($redisKey, 1);
+                    $followed = true;
+                }
+            }
+
+            $platforms[] = [
+                'code' => $code,
+                'name' => $name,
+                'followed' => $followed
+            ];
+        }
+
+        return ApiResponse::success([
+            'platforms' => $platforms,
+            'followed_count' => count(array_filter($platforms, fn($p) => $p['followed']))
+        ]);
+    }
+
     /**
     /**
      * Get points information for cart
      * Get points information for cart
      */
      */
@@ -341,6 +585,8 @@ class RewardPointsController extends Controller
             7 => 'Share',
             7 => 'Share',
             8 => 'Subscription',
             8 => 'Subscription',
             9 => 'Login',
             9 => 'Login',
+            13 => 'Browse Product',
+            14 => 'Follow',
             99 => 'admin',
             99 => 'admin',
         ];
         ];
 
 

+ 9 - 5
packages/Longyi/RewardPoints/src/Listeners/ReviewEvents.php

@@ -49,8 +49,10 @@ class ReviewEvents
             return;
             return;
         }
         }
 
 
-        // 【简化】使用统一方法获取积分值
-        $points = $rule->getPointsForCustomer($customer);
+        // 根据评价是否包含图片/视频决定积分数
+        // 纯文字评价 50 积分,带照片/视频评价 100 积分
+        $hasMedia = $review->images()->count() > 0;
+        $points = $hasMedia ? 100 : 50;
 
 
         if ($points <= 0) {
         if ($points <= 0) {
             // Log::info('Review points is 0, skipping', [
             // Log::info('Review points is 0, skipping', [
@@ -84,7 +86,7 @@ class ReviewEvents
             RewardActiveRule::TYPE_REVIEW,
             RewardActiveRule::TYPE_REVIEW,
             $points,
             $points,
             null,
             null,
-            $this->getReviewPointsDescription($review),
+            $this->getReviewPointsDescription($review, $hasMedia),
             $rule
             $rule
         );
         );
 
 
@@ -161,12 +163,14 @@ class ReviewEvents
     /**
     /**
      * 获取评价积分描述
      * 获取评价积分描述
      */
      */
-    protected function getReviewPointsDescription($review): string
+    protected function getReviewPointsDescription($review, bool $hasMedia = false): string
     {
     {
         $productInfo = !empty($review->product_id)
         $productInfo = !empty($review->product_id)
             ? " for product #{$review->product_id}"
             ? " for product #{$review->product_id}"
             : '';
             : '';
 
 
-        return "Points earned for product review{$productInfo} (Review ID: {$review->id})";
+        $mediaLabel = $hasMedia ? ' (with photo/video)' : ' (text only)';
+
+        return "Points earned for product review{$productInfo}{$mediaLabel} (Review ID: {$review->id})";
     }
     }
 }
 }

+ 2 - 0
packages/Longyi/RewardPoints/src/Models/RewardActiveRule.php

@@ -22,6 +22,8 @@ class RewardActiveRule extends Model
     const TYPE_GIFT_CARD_REDEEM = TransactionType::GIFT_CARD_REDEEM;
     const TYPE_GIFT_CARD_REDEEM = TransactionType::GIFT_CARD_REDEEM;
     const TYPE_ADMIN_ACTION = TransactionType::ADMIN_ACTION;
     const TYPE_ADMIN_ACTION = TransactionType::ADMIN_ACTION;
     const TYPE_ORDER_CANCEL_REFUND = TransactionType::ORDER_CANCEL_REFUND;
     const TYPE_ORDER_CANCEL_REFUND = TransactionType::ORDER_CANCEL_REFUND;
+    const TYPE_BROWSE_PRODUCT = TransactionType::BROWSE_PRODUCT;
+    const TYPE_FOLLOW = TransactionType::FOLLOW;
 
 
     protected $table = 'mw_reward_active_rules';
     protected $table = 'mw_reward_active_rules';
     protected $primaryKey = 'rule_id';
     protected $primaryKey = 'rule_id';

+ 5 - 7
packages/Longyi/RewardPoints/src/Repositories/RewardPointRepository.php

@@ -271,16 +271,14 @@ class RewardPointRepository extends Repository
     {
     {
         $query = $this->where('customer_id', $customerId)
         $query = $this->where('customer_id', $customerId)
             ->orderBy('transaction_time', 'desc');
             ->orderBy('transaction_time', 'desc');
-
-        if ($page) {
-            return $query->paginate($limit, ['*'], 'page', $page);
-        }
-// 根据视图类型筛选(仅获取或全部)
-       if ($amountType === 'earned') {
+        if ($amountType == 'earned') {
             $query->where('amount', '>', 0)->where('status',1);
             $query->where('amount', '>', 0)->where('status',1);
-        } elseif ($amountType === 'redeemed') {
+        } elseif ($amountType == 'redeemed') {
             $query->where('amount', '<', 0);
             $query->where('amount', '<', 0);
         }
         }
+        if ($page) {
+            return $query->paginate($limit, ['*'], 'page', $page);
+        }
         return $query->paginate($limit);
         return $query->paginate($limit);
     }
     }
 
 

+ 3 - 0
packages/Longyi/RewardPoints/src/Resources/views/admin/rules/index.blade.php

@@ -117,6 +117,9 @@
                                 'indigo' => 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300',
                                 'indigo' => 'bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-300',
                                 'pink' => 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
                                 'pink' => 'bg-pink-100 dark:bg-pink-900 text-pink-700 dark:text-pink-300',
                                 'orange' => 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
                                 'orange' => 'bg-orange-100 dark:bg-orange-900 text-orange-700 dark:text-orange-300',
+                                'red' => 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300',
+                                'cyan' => 'bg-cyan-100 dark:bg-cyan-900 text-cyan-700 dark:text-cyan-300',
+                                'teal' => 'bg-teal-100 dark:bg-teal-900 text-teal-700 dark:text-teal-300',
                                 'gray' => 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400',
                                 'gray' => 'bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400',
                             ];
                             ];
                         @endphp
                         @endphp

+ 20 - 0
packages/Longyi/RewardPoints/src/Routes/routes.php

@@ -99,6 +99,26 @@ Route::group(['middleware' => ['api', ApiCustomerAuthenticate::class], 'prefix'
         'as' => 'api.checkout.reward-points-info',
         'as' => 'api.checkout.reward-points-info',
         'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getPointsInfo'
         'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getPointsInfo'
     ]);
     ]);
+
+    Route::post('reward-points/browse-product', [
+        'as' => 'api.customer.reward-points.browse-product',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@browseProduct'
+    ]);
+
+    Route::get('reward-points/browsed-products', [
+        'as' => 'api.customer.reward-points.browsed-products',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getBrowsedProducts'
+    ]);
+
+    Route::post('reward-points/follow', [
+        'as' => 'api.customer.reward-points.follow',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@followPlatform'
+    ]);
+
+    Route::get('reward-points/follow-status', [
+        'as' => 'api.customer.reward-points.follow-status',
+        'uses' => 'Longyi\RewardPoints\Http\Controllers\RewardPointsController@getFollowStatus'
+    ]);
 });
 });
 
 
 // 后台路由
 // 后台路由