RewardPointsController.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. namespace Longyi\RewardPoints\Http\Controllers;
  3. use Illuminate\Support\Facades\Log;
  4. use Longyi\RewardPoints\Models\RewardActiveRule;
  5. use Longyi\RewardPoints\Repositories\RewardPointRepository;
  6. use Longyi\RewardPoints\Repositories\RewardPointSettingRepository;
  7. use Longyi\RewardPoints\Models\RewardPointCustomerSign;
  8. use Longyi\RewardPoints\Helpers\ApiResponse;
  9. use Carbon\Carbon;
  10. use Illuminate\Http\Request;
  11. use Illuminate\Routing\Controller;
  12. use Longyi\RewardPoints\Models\RewardPointHistory;
  13. use Illuminate\Support\Facades\Redis;
  14. class RewardPointsController extends Controller
  15. {
  16. protected $rewardPointRepository;
  17. protected $settingRepository;
  18. protected $_config;
  19. public function __construct(
  20. RewardPointRepository $rewardPointRepository,
  21. RewardPointSettingRepository $settingRepository
  22. ) {
  23. $this->rewardPointRepository = $rewardPointRepository;
  24. $this->settingRepository = $settingRepository;
  25. $this->_config = request('_config');
  26. }
  27. public function index()
  28. {
  29. $customer = auth()->guard('customer')->user();
  30. if (!$customer) {
  31. return redirect()->route('customer.session.index');
  32. }
  33. $points = $this->rewardPointRepository->getCustomerPoints($customer->id);
  34. $history = $this->rewardPointRepository->getHistory($customer->id);
  35. return view($this->_config['view'], compact('points', 'history'));
  36. }
  37. public function signIn()
  38. {
  39. $customer = auth()->guard('customer')->user();
  40. if (!$customer) {
  41. return ApiResponse::unauthorized();
  42. }
  43. // Check if sign in feature is enabled
  44. $signinEnabled = $this->settingRepository->getConfigValue('signin_enabled', true);
  45. if (!$signinEnabled) {
  46. return ApiResponse::forbidden('Sign in feature is disabled');
  47. }
  48. $today = Carbon::now()->format('Y-m-d');
  49. $existingSign = RewardPointCustomerSign::where('customer_id', $customer->id)
  50. ->where('sign_date', $today)
  51. ->first();
  52. if ($existingSign) {
  53. return ApiResponse::error('Already signed in today');
  54. }
  55. $lastSign = RewardPointCustomerSign::where('customer_id', $customer->id)
  56. ->orderBy('sign_date', 'desc')
  57. ->first();
  58. $countDate = 1;
  59. if ($lastSign) {
  60. $lastSignDate = Carbon::parse($lastSign->sign_date);
  61. $yesterday = Carbon::yesterday();
  62. if ($lastSignDate->toDateString() === $yesterday->toDateString()) {
  63. $countDate = $lastSign->count_date + 1;
  64. } elseif ($lastSignDate->toDateString() !== $today) {
  65. $countDate = 1;
  66. }
  67. }
  68. // Get sign in points from ly_mw_reward_points_settings table
  69. $points = $this->getSignInPointsByDay($countDate);
  70. // Create sign in record (Laravel auto-maintains created_at and updated_at)
  71. $sign = RewardPointCustomerSign::create([
  72. 'customer_id' => $customer->id,
  73. 'sign_date' => $today,
  74. 'count_date' => $countDate,
  75. 'point' => $points,
  76. 'code' => uniqid('SIGN_')
  77. ]);
  78. // Add points to customer account
  79. $this->rewardPointRepository->addPoints(
  80. $customer->id,
  81. RewardActiveRule::TYPE_SIGN_IN,
  82. $points,
  83. null,
  84. "Daily sign-in reward (Day {$countDate})"
  85. );
  86. $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
  87. return ApiResponse::success([
  88. 'points' => $points,
  89. 'streak' => $countDate,
  90. 'total_points' => $totalPoints
  91. ], "Sign in successful! Earned {$points} points");
  92. }
  93. /**
  94. * Get sign in points by day from ly_mw_reward_points_settings table
  95. */
  96. protected function getSignInPointsByDay($day)
  97. {
  98. // Map sign in days to configuration codes
  99. $dayPointsMap = [
  100. 1 => 'signin_day1_points',
  101. 2 => 'signin_day2_points',
  102. 3 => 'signin_day3_points',
  103. 4 => 'signin_day4_points',
  104. 5 => 'signin_day5_points',
  105. 6 => 'signin_day6_points',
  106. 7 => 'signin_day7_points',
  107. ];
  108. // If <= 7 days, use corresponding configuration
  109. if ($day <= 7 && isset($dayPointsMap[$day])) {
  110. return (int) $this->settingRepository->getConfigValue($dayPointsMap[$day], 10);
  111. }
  112. // 8 days and beyond, use signin_day8_plus_points configuration
  113. return (int) $this->settingRepository->getConfigValue('signin_day8_plus_points', 70);
  114. }
  115. public function getSignStatus()
  116. {
  117. $customer = auth()->guard('customer')->user();
  118. if (!$customer) {
  119. return ApiResponse::unauthorized();
  120. }
  121. $today = Carbon::now()->format('Y-m-d');
  122. $signedToday = RewardPointCustomerSign::where('customer_id', $customer->id)
  123. ->where('sign_date', $today)
  124. ->exists();
  125. $lastSign = RewardPointCustomerSign::where('customer_id', $customer->id)
  126. ->orderBy('sign_date', 'desc')
  127. ->first();
  128. $currentGroup = 'sign_in';
  129. $settings = $this->settingRepository->getSettingsByGroup($currentGroup);
  130. $dayNamesMap = [
  131. 'signin_day1_points' => 'Day 1 Sign-in Points',
  132. 'signin_day2_points' => 'Day 2 Sign-in Points',
  133. 'signin_day3_points' => 'Day 3 Sign-in Points',
  134. 'signin_day4_points' => 'Day 4 Sign-in Points',
  135. 'signin_day5_points' => 'Day 5 Sign-in Points',
  136. 'signin_day6_points' => 'Day 6 Sign-in Points',
  137. 'signin_day7_points' => 'Day 7 Sign-in Points',
  138. 'signin_day8_plus_points' => 'Day 7+ Sign-in Points',
  139. ];
  140. $new = [];
  141. foreach ($settings as $setting) {
  142. $code = $setting['code'];
  143. if (array_key_exists($code, $dayNamesMap)) {
  144. $new[] = [
  145. 'day' => $dayNamesMap[$code],
  146. 'points' => $setting['value']
  147. ];
  148. }
  149. }
  150. return ApiResponse::success([
  151. 'signed_today' => $signedToday,
  152. 'current_streak' => $lastSign ? $lastSign->count_date : 0,
  153. 'signed_detail' => $new,
  154. 'total_points' => $this->rewardPointRepository->getCustomerPoints($customer->id)
  155. ]);
  156. }
  157. /**
  158. * Get customer points history/records
  159. *
  160. * @param Request $request
  161. * @return \Illuminate\Http\JsonResponse
  162. */
  163. public function getPointsHistory(Request $request)
  164. {
  165. $customer = auth()->guard('customer')->user();
  166. if (!$customer) {
  167. return ApiResponse::unauthorized();
  168. }
  169. // Get pagination parameters
  170. $page = $request->input('page', 1);
  171. $limit = $request->input('per_page', 20);
  172. $amountType= $request->input('amountType');
  173. // Validate limit
  174. $limit = min(max($limit, 1), 100); // Limit between 1 and 100
  175. // Get paginated history
  176. $history = $this->rewardPointRepository->getHistory($customer->id, $limit,$page,$amountType);
  177. // Transform history data
  178. $transformedHistory = collect($history->items())->map(function ($item) {
  179. return [
  180. 'id' => $item->history_id,
  181. 'type' => $item->type_of_transaction,
  182. 'type_text' => $this->getTransactionTypeText($item->type_of_transaction),
  183. 'amount' => $item->amount,
  184. 'balance' => $item->balance,
  185. 'detail' => $item->transaction_detail,
  186. 'order_id' => $item->history_order_id > 0 ? $item->history_order_id : null,
  187. 'expired_day' => $item->expired_day,
  188. 'expired_time' => $item->expired_time ? Carbon::parse($item->expired_time)->format('Y-m-d H:i:s') : null,
  189. 'remaining_points' => $item->point_remaining,
  190. 'status' => $item->status,
  191. 'status_text' => $this->getStatusText($item->status),
  192. 'created_at' => Carbon::parse($item->transaction_time)->format('Y-m-d H:i:s'),
  193. ];
  194. });
  195. return ApiResponse::paginated(
  196. [
  197. 'current_points' => $this->rewardPointRepository->getCustomerPoints($customer->id),
  198. 'history' => $transformedHistory
  199. ],
  200. $history
  201. );
  202. }
  203. /**
  204. * Get customer points summary
  205. *
  206. * @return \Illuminate\Http\JsonResponse
  207. */
  208. public function getPointsSummary()
  209. {
  210. $customer = auth()->guard('customer')->user();
  211. if (!$customer) {
  212. return ApiResponse::unauthorized();
  213. }
  214. $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
  215. // Get points expired in the last month
  216. $oneMonthAgo = Carbon::now()->subMonth();
  217. $recentlyExpired = RewardPointHistory::where('customer_id', $customer->id)
  218. ->where('status', RewardPointHistory::STATUS_EXPIRED)
  219. ->whereNotNull('expired_time')
  220. ->where('expired_time', '>=', $oneMonthAgo)
  221. ->sum('amount') * -1;
  222. // Get expiring points in the next month (still valid but will expire soon)
  223. $nextMonth = Carbon::now()->addMonth();
  224. $expiringSoon = RewardPointHistory::where('customer_id', $customer->id)
  225. ->where('status', RewardPointHistory::STATUS_COMPLETED)
  226. ->whereNotNull('expired_time')
  227. ->where('expired_time', '>', Carbon::now())
  228. ->where('expired_time', '<=', $nextMonth)
  229. ->sum('point_remaining');
  230. return ApiResponse::success([
  231. 'recently_expired' => $recentlyExpired,
  232. 'expiring_soon' => $expiringSoon,
  233. 'current_points' => $totalPoints
  234. ]);
  235. }
  236. /**
  237. * Apply reward points to cart
  238. */
  239. public function applyPoints(Request $request)
  240. {
  241. $points = $request->input('points', 0);
  242. $cartRewardPoints = app('cartrewardpoints');
  243. $validation = $cartRewardPoints->validatePoints($points);
  244. if ($validation !== true) {
  245. return ApiResponse::error($validation);
  246. }
  247. $result = $cartRewardPoints->applyPoints($points);
  248. if ($result) {
  249. $discountDetails = $cartRewardPoints->getDiscountDetails();
  250. return ApiResponse::success([
  251. 'discount' => $discountDetails
  252. ], 'Reward points applied successfully');
  253. }
  254. return ApiResponse::error('Failed to apply reward points');
  255. }
  256. /**
  257. * Remove reward points from cart
  258. */
  259. public function removePoints()
  260. {
  261. $cartRewardPoints = app('cartrewardpoints');
  262. $cartRewardPoints->removePoints();
  263. return ApiResponse::success([], 'Reward points removed successfully');
  264. }
  265. /**
  266. * 浏览产品赠送积分(每个产品每天仅赠送一次)
  267. *
  268. * @param Request $request
  269. * @return \Illuminate\Http\JsonResponse
  270. */
  271. public function browseProduct(Request $request)
  272. {
  273. $customer = auth()->guard('customer')->user();
  274. if (!$customer) {
  275. return ApiResponse::unauthorized();
  276. }
  277. $productId = $request->input('categorys');
  278. if (!$productId) {
  279. return ApiResponse::validationError('categorys is required');
  280. }
  281. // 查询有效的浏览产品规则
  282. $rule = RewardActiveRule::active()
  283. ->ofType(RewardActiveRule::TYPE_BROWSE_PRODUCT)
  284. ->first();
  285. if (!$rule) {
  286. return ApiResponse::forbidden('Browse product reward is not available');
  287. }
  288. // 检查规则是否适用于当前客户
  289. if (!$rule->isApplicableToCustomer($customer)) {
  290. return ApiResponse::forbidden('This reward is not applicable to your account');
  291. }
  292. // Redis Key: browse_product:{customer_id}:{date}:{product_id}
  293. $today = Carbon::now()->format('Y-m-d');
  294. $redisKey = "browse_product:{$customer->id}:{$today}:{$productId}";
  295. // Redis 判断该产品今天是否已经浏览过
  296. if (Redis::exists($redisKey)) {
  297. return ApiResponse::error('You have already earned points for browsing this product today');
  298. }
  299. // 获取该客户应得的积分
  300. $points = $rule->getPointsForCustomer($customer);
  301. if ($points <= 0) {
  302. $points = (int) $rule->reward_point;
  303. }
  304. if ($points <= 0) {
  305. return ApiResponse::error('No points configured for browse product');
  306. }
  307. // 添加积分
  308. $this->rewardPointRepository->addPoints(
  309. $customer->id,
  310. RewardActiveRule::TYPE_BROWSE_PRODUCT,
  311. $points,
  312. null,
  313. "Browsed categorys #{$productId}",
  314. $rule
  315. );
  316. // 写入 Redis,TTL = 到当天结束的剩余秒数
  317. $endOfDay = Carbon::now()->endOfDay();
  318. $ttl = Carbon::now()->diffInSeconds($endOfDay);
  319. Redis::setex($redisKey, $ttl, 1);
  320. // 同时记录到当天已浏览产品集合(方便 getBrowsedProducts 读取)
  321. $setKey = "browse_product:{$customer->id}:{$today}:set";
  322. Redis::sadd($setKey, $productId);
  323. Redis::expire($setKey, $ttl);
  324. $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
  325. return ApiResponse::success([
  326. 'categorys' => $productId,
  327. 'points' => $points,
  328. 'total_points' => $totalPoints
  329. ], "You earned {$points} points for browsing this product!");
  330. }
  331. /**
  332. * 获取今天已浏览并获得积分的产品列表
  333. *
  334. * @return \Illuminate\Http\JsonResponse
  335. */
  336. public function getBrowsedProducts()
  337. {
  338. $customer = auth()->guard('customer')->user();
  339. if (!$customer) {
  340. return ApiResponse::unauthorized();
  341. }
  342. $today = Carbon::now()->format('Y-m-d');
  343. $setKey = "browse_product:{$customer->id}:{$today}:set";
  344. // 从 Redis Set 中获取今天已浏览的产品ID列表
  345. $browsedProductIds = Redis::smembers($setKey);
  346. $browsedProductIds = array_map('intval', $browsedProductIds);
  347. return ApiResponse::success([
  348. 'browsed_product_ids' => $browsedProductIds,
  349. 'browsed_count' => count($browsedProductIds)
  350. ]);
  351. }
  352. /**
  353. * 支持的关注平台
  354. */
  355. const FOLLOW_PLATFORMS = ['ig' => 'Instagram', 'fb' => 'Facebook', 'ytb' => 'YouTube', 'tt' => 'TikTok'];
  356. /**
  357. * 关注社交平台赠送积分(每位用户每个平台仅一次,永久有效)
  358. *
  359. * @param Request $request
  360. * @return \Illuminate\Http\JsonResponse
  361. */
  362. public function followPlatform(Request $request)
  363. {
  364. $customer = auth()->guard('customer')->user();
  365. if (!$customer) {
  366. return ApiResponse::unauthorized();
  367. }
  368. $platform = $request->input('platform');
  369. if (!$platform || !isset(self::FOLLOW_PLATFORMS[$platform])) {
  370. return ApiResponse::validationError('Invalid platform. Supported: ' . implode(', ', array_keys(self::FOLLOW_PLATFORMS)));
  371. }
  372. // Redis Key: follow_platform:{customer_id}:{platform}(永久有效,不设 TTL)
  373. $redisKey = "follow_platform:{$customer->id}:{$platform}";
  374. // 1. Redis 快速判断(主缓存)
  375. if (Redis::exists($redisKey)) {
  376. return ApiResponse::error('You have already earned points for following ' . self::FOLLOW_PLATFORMS[$platform]);
  377. }
  378. // 2. DB 兜底检查:防止 Redis 被清理后重复发积分
  379. $dbExists = RewardPointHistory::where('customer_id', $customer->id)
  380. ->where('type_of_transaction', RewardActiveRule::TYPE_FOLLOW)
  381. ->where('transaction_detail', 'Followed ' . self::FOLLOW_PLATFORMS[$platform])
  382. ->exists();
  383. if ($dbExists) {
  384. // DB 有记录但 Redis 丢失,回补 Redis
  385. Redis::set($redisKey, 1);
  386. return ApiResponse::error('You have already earned points for following ' . self::FOLLOW_PLATFORMS[$platform]);
  387. }
  388. // 查询有效的关注规则
  389. $rule = RewardActiveRule::active()
  390. ->ofType(RewardActiveRule::TYPE_FOLLOW)
  391. ->first();
  392. if (!$rule) {
  393. return ApiResponse::forbidden('Follow reward is not available');
  394. }
  395. // 检查规则是否适用于当前客户
  396. if (!$rule->isApplicableToCustomer($customer)) {
  397. return ApiResponse::forbidden('This reward is not applicable to your account');
  398. }
  399. // 获取该客户应得的积分
  400. $points = $rule->getPointsForCustomer($customer);
  401. if ($points <= 0) {
  402. $points = (int) $rule->reward_point;
  403. }
  404. if ($points <= 0) {
  405. return ApiResponse::error('No points configured for follow reward');
  406. }
  407. // 添加积分
  408. $this->rewardPointRepository->addPoints(
  409. $customer->id,
  410. RewardActiveRule::TYPE_FOLLOW,
  411. $points,
  412. null,
  413. 'Followed ' . self::FOLLOW_PLATFORMS[$platform],
  414. $rule
  415. );
  416. // 写入 Redis(永久有效,无需 TTL)
  417. Redis::set($redisKey, 1);
  418. $totalPoints = $this->rewardPointRepository->getCustomerPoints($customer->id);
  419. return ApiResponse::success([
  420. 'platform' => $platform,
  421. 'platform_name' => self::FOLLOW_PLATFORMS[$platform],
  422. 'points' => $points,
  423. 'total_points' => $totalPoints
  424. ], 'You earned ' . $points . ' points for following ' . self::FOLLOW_PLATFORMS[$platform] . '!');
  425. }
  426. /**
  427. * 获取用户已关注的平台列表及积分状态
  428. *
  429. * @return \Illuminate\Http\JsonResponse
  430. */
  431. public function getFollowStatus()
  432. {
  433. $customer = auth()->guard('customer')->user();
  434. if (!$customer) {
  435. return ApiResponse::unauthorized();
  436. }
  437. $platforms = [];
  438. foreach (self::FOLLOW_PLATFORMS as $code => $name) {
  439. $redisKey = "follow_platform:{$customer->id}:{$code}";
  440. $followed = (bool) Redis::exists($redisKey);
  441. // Redis 未命中时,从 DB 回补并修复 Redis
  442. if (!$followed) {
  443. $dbExists = RewardPointHistory::where('customer_id', $customer->id)
  444. ->where('type_of_transaction', RewardActiveRule::TYPE_FOLLOW)
  445. ->where('transaction_detail', 'Followed ' . $name)
  446. ->exists();
  447. if ($dbExists) {
  448. Redis::set($redisKey, 1);
  449. $followed = true;
  450. }
  451. }
  452. $platforms[] = [
  453. 'code' => $code,
  454. 'name' => $name,
  455. 'followed' => $followed
  456. ];
  457. }
  458. return ApiResponse::success([
  459. 'platforms' => $platforms,
  460. 'followed_count' => count(array_filter($platforms, fn($p) => $p['followed']))
  461. ]);
  462. }
  463. /**
  464. * Get points information for cart
  465. */
  466. public function getPointsInfo()
  467. {
  468. $cartRewardPoints = app('cartrewardpoints');
  469. return ApiResponse::success([
  470. 'available_points' => $cartRewardPoints->getAvailablePoints(),
  471. 'points_used' => $cartRewardPoints->getPointsUsed(),
  472. 'discount_amount' => $cartRewardPoints->getDiscountAmount(),
  473. 'points_value' => $cartRewardPoints->getPointsValue(1),
  474. 'max_points_allowed' => $cartRewardPoints->getMaxPointsByCartTotal()
  475. ]);
  476. }
  477. /**
  478. * Get transaction type text
  479. */
  480. protected function getTransactionTypeText($type)
  481. {
  482. $types = [
  483. 1 => 'Daily Sign In',
  484. 2 => 'Registration',
  485. 3 => 'Order Purchase',
  486. 4 => 'Product Review',
  487. 5 => 'Referral',
  488. 6 => 'Birthday',
  489. 7 => 'Share',
  490. 8 => 'Subscription',
  491. 9 => 'Login',
  492. 13 => 'Browse Product',
  493. 14 => 'Follow',
  494. 99 => 'admin',
  495. ];
  496. return $types[$type] ?? 'Unknown';
  497. }
  498. /**
  499. * Get status text
  500. */
  501. protected function getStatusText($status)
  502. {
  503. $statuses = [
  504. 1 => 'Completed',
  505. 0 => 'Pending',
  506. 3 => 'Expired',
  507. 2 => 'Cancelled'
  508. ];
  509. return $statuses[$status] ?? 'Unknown';
  510. }
  511. }