RecallGiftLogicTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. namespace Tests\Unit;
  3. use Tests\TestCase;
  4. /**
  5. * 测试召回礼包(gift_id=305)核心逻辑:tier 计算、分布比例、天数计算
  6. *
  7. * @see PayRechargeController::getRecallGiftTiers()
  8. * @see PayRechargeController::vipInactiveGiftSevenDays()
  9. * @see PayRechargeController::claimVipInactiveGiftDayReward()
  10. */
  11. class RecallGiftLogicTest extends TestCase
  12. {
  13. /**
  14. * 可用档位:9, 19, 29, 39, 49, 59, 79, 99
  15. * 初档 70%,中档 85%,高档 100%
  16. */
  17. /** @test */
  18. public function it_selects_first_three_tiers_when_avg_below_minimum()
  19. {
  20. // avgRecharge < 9 → 初档=9, 中档=19, 高档=29
  21. $tiers = $this->getRecallGiftTiers(5);
  22. $this->assertCount(3, $tiers);
  23. $this->assertEquals(9, $tiers[0]['amount']);
  24. $this->assertEquals(19, $tiers[1]['amount']);
  25. $this->assertEquals(29, $tiers[2]['amount']);
  26. $this->assertEquals(70, $tiers[0]['bonus_percent']);
  27. $this->assertEquals(85, $tiers[1]['bonus_percent']);
  28. $this->assertEquals(100, $tiers[2]['bonus_percent']);
  29. }
  30. /** @test */
  31. public function it_selects_tiers_based_on_average_recharge()
  32. {
  33. // avgRecharge = 15,初档应为第一个 >= 15 的档位 = 19
  34. $tiers = $this->getRecallGiftTiers(15);
  35. $this->assertCount(3, $tiers);
  36. $this->assertEquals(19, $tiers[0]['amount']);
  37. $this->assertEquals(29, $tiers[1]['amount']);
  38. $this->assertEquals(39, $tiers[2]['amount']);
  39. }
  40. /** @test */
  41. public function it_selects_last_three_tiers_when_avg_above_maximum()
  42. {
  43. // avgRecharge = 100 > 99,取最后三位:59, 79, 99
  44. $tiers = $this->getRecallGiftTiers(100);
  45. $this->assertCount(3, $tiers);
  46. $this->assertEquals(59, $tiers[0]['amount']);
  47. $this->assertEquals(79, $tiers[1]['amount']);
  48. $this->assertEquals(99, $tiers[2]['amount']);
  49. $this->assertEquals(70, $tiers[0]['bonus_percent']); // 初档
  50. $this->assertEquals(85, $tiers[1]['bonus_percent']); // 中档
  51. $this->assertEquals(100, $tiers[2]['bonus_percent']); // 高档
  52. }
  53. /** @test */
  54. public function it_handles_exact_boundary_values()
  55. {
  56. // avgRecharge = 9,恰好等于初档
  57. $tiers = $this->getRecallGiftTiers(9);
  58. $this->assertEquals(9, $tiers[0]['amount']);
  59. // avgRecharge = 29,初档应为 29
  60. $tiers = $this->getRecallGiftTiers(29);
  61. $this->assertEquals(29, $tiers[0]['amount']);
  62. $this->assertEquals(39, $tiers[1]['amount']);
  63. $this->assertEquals(49, $tiers[2]['amount']);
  64. }
  65. /** @test */
  66. public function it_handles_avg_between_tiers()
  67. {
  68. // avgRecharge = 25,第一个 >= 25 的是 29
  69. $tiers = $this->getRecallGiftTiers(25);
  70. $this->assertEquals(29, $tiers[0]['amount']);
  71. $this->assertEquals(39, $tiers[1]['amount']);
  72. $this->assertEquals(49, $tiers[2]['amount']);
  73. }
  74. /** @test */
  75. public function distribution_sums_to_100_percent()
  76. {
  77. $distribution = [7, 9, 11, 13, 14, 21, 25];
  78. $this->assertEquals(100, array_sum($distribution));
  79. }
  80. /** @test */
  81. public function daily_amounts_match_distribution()
  82. {
  83. // 如果总奖励 = $10.00,按比例分布
  84. $totalBonus = 10.00;
  85. $distribution = [7, 9, 11, 13, 14, 21, 25];
  86. $dayAmounts = [];
  87. for ($i = 0; $i < 7; $i++) {
  88. $dayAmounts[] = round($totalBonus * $distribution[$i] / 100, 2);
  89. }
  90. $this->assertEquals(0.70, $dayAmounts[0]); // Day 1: 7%
  91. $this->assertEquals(0.90, $dayAmounts[1]); // Day 2: 9%
  92. $this->assertEquals(1.10, $dayAmounts[2]); // Day 3: 11%
  93. $this->assertEquals(1.30, $dayAmounts[3]); // Day 4: 13%
  94. $this->assertEquals(1.40, $dayAmounts[4]); // Day 5: 14%
  95. $this->assertEquals(2.10, $dayAmounts[5]); // Day 6: 21%
  96. $this->assertEquals(2.50, $dayAmounts[6]); // Day 7: 25%
  97. // 总和应等于 $10.00(允许浮点误差)
  98. $this->assertEquals(10.00, round(array_sum($dayAmounts), 2));
  99. }
  100. /** @test */
  101. public function daily_amounts_match_distribution_realistic_bonus()
  102. {
  103. // payAmt=19, bonusPercent=70% → totalBonus = 19 * 0.70 = 13.30
  104. $payAmt = 19.00;
  105. $bonusPercent = 70;
  106. $totalBonus = round($payAmt * $bonusPercent / 100, 2);
  107. $this->assertEquals(13.30, $totalBonus);
  108. $distribution = [7, 9, 11, 13, 14, 21, 25];
  109. $dayAmounts = [];
  110. for ($i = 0; $i < 7; $i++) {
  111. $dayAmounts[] = round($totalBonus * $distribution[$i] / 100, 2);
  112. }
  113. $this->assertEquals(0.93, $dayAmounts[0]); // 13.30 * 7% = 0.931
  114. $this->assertEquals(1.20, $dayAmounts[1]); // 13.30 * 9% = 1.197
  115. $this->assertEquals(1.46, $dayAmounts[2]); // 13.30 * 11% = 1.463
  116. $this->assertEquals(1.73, $dayAmounts[3]); // 13.30 * 13% = 1.729
  117. $this->assertEquals(1.86, $dayAmounts[4]); // 13.30 * 14% = 1.862
  118. $this->assertEquals(2.79, $dayAmounts[5]); // 13.30 * 21% = 2.793
  119. $this->assertEquals(3.33, $dayAmounts[6]); // 13.30 * 25% = 3.325
  120. }
  121. /** @test */
  122. public function inactive_days_calculation_is_correct()
  123. {
  124. // 模拟:最后一次充值在 7 天前
  125. $lastDate = date('Y-m-d', strtotime('-7 days'));
  126. $today = date('Y-m-d');
  127. $diffDays = (strtotime($today) - strtotime($lastDate)) / 86400;
  128. $inactiveDays = max(0, (int)$diffDays - 1);
  129. // 7天前充值 → diffDays=7 → inactiveDays=6
  130. // 但需要连续7天未充值,所以 lastPay 应是8天前
  131. $this->assertEquals(6, $inactiveDays);
  132. // 8天前充值 → diffDays=8 → inactiveDays=7(满足条件)
  133. $lastDate = date('Y-m-d', strtotime('-8 days'));
  134. $diffDays = (strtotime($today) - strtotime($lastDate)) / 86400;
  135. $inactiveDays = max(0, (int)$diffDays - 1);
  136. $this->assertEquals(7, $inactiveDays);
  137. }
  138. /** @test */
  139. public function day_offset_from_recharge_to_signin()
  140. {
  141. // 如果充值在 2026-05-14
  142. $createdAt = '2026-05-14';
  143. // 签到第1天 = 充值次日 = 2026-05-15
  144. $day1Target = date('Ymd', strtotime($createdAt . ' +1 days'));
  145. $this->assertEquals('20260515', $day1Target);
  146. // 签到第7天 = 充值 + 7天 = 2026-05-21
  147. $day7Target = date('Ymd', strtotime($createdAt . ' +7 days'));
  148. $this->assertEquals('20260521', $day7Target);
  149. }
  150. /** @test */
  151. public function days_passed_calculation()
  152. {
  153. // 充值当天
  154. $createdAt = '2026-05-14';
  155. $today = '2026-05-14';
  156. $daysPassed = floor((strtotime($today) - strtotime($createdAt)) / 86400);
  157. $this->assertEquals(0, $daysPassed); // 还不能签到
  158. // 充值次日
  159. $today = '2026-05-15';
  160. $daysPassed = floor((strtotime($today) - strtotime($createdAt)) / 86400);
  161. $this->assertEquals(1, $daysPassed); // 可以签到第1天
  162. // 充值7天后
  163. $today = '2026-05-21';
  164. $daysPassed = floor((strtotime($today) - strtotime($createdAt)) / 86400);
  165. $this->assertEquals(7, $daysPassed); // 可以签到第7天
  166. // 充值8天后(过期)
  167. $today = '2026-05-22';
  168. $daysPassed = floor((strtotime($today) - strtotime($createdAt)) / 86400);
  169. $this->assertEquals(8, $daysPassed); // 超过7天,过期
  170. }
  171. /** @test */
  172. public function claim_day_mapping()
  173. {
  174. // 按实际签到次数确定天数,不再依赖自然日
  175. // 已签到0次 → day=1 (第1次签到) → dayIndex=0 → 7%
  176. $claimedDaysCount = 0;
  177. $day = $claimedDaysCount + 1;
  178. $dayIndex = $day - 1; // 0
  179. $distribution = [7, 9, 11, 13, 14, 21, 25];
  180. $this->assertEquals(0, $dayIndex);
  181. $this->assertEquals(7, $distribution[$dayIndex]);
  182. // 已签到1次 → day=2 (第2次签到) → dayIndex=1 → 9%
  183. $claimedDaysCount = 1;
  184. $day = $claimedDaysCount + 1;
  185. $dayIndex = $day - 1; // 1
  186. $this->assertEquals(1, $dayIndex);
  187. $this->assertEquals(9, $distribution[$dayIndex]);
  188. // 已签到3次 → day=4 (第4次签到) → dayIndex=3 → 13%
  189. $claimedDaysCount = 3;
  190. $day = $claimedDaysCount + 1;
  191. $dayIndex = $day - 1; // 3
  192. $this->assertEquals(3, $dayIndex);
  193. $this->assertEquals(13, $distribution[$dayIndex]);
  194. // 已签到6次 → day=7 (第7次签到) → dayIndex=6 → 25%
  195. $claimedDaysCount = 6;
  196. $day = $claimedDaysCount + 1;
  197. $dayIndex = $day - 1; // 6
  198. $this->assertEquals(6, $dayIndex);
  199. $this->assertEquals(25, $distribution[$dayIndex]);
  200. }
  201. /** @test */
  202. public function claimed_count_determines_next_day()
  203. {
  204. // 签到次数直接影响下一次能签第几天
  205. // 已签到0次,下次签第1天
  206. $claimedMask = 0; // 0b0000000
  207. $claimedCount = 0;
  208. for ($i = 0; $i < 7; $i++) {
  209. if ($claimedMask & (1 << $i)) {
  210. $claimedCount++;
  211. }
  212. }
  213. $nextDay = $claimedCount + 1;
  214. $this->assertEquals(1, $nextDay);
  215. // 已签到2次(第1、2天),下次签第3天
  216. $claimedMask = (1 << 0) | (1 << 1); // 0b0000011 = 3
  217. $claimedCount = 0;
  218. for ($i = 0; $i < 7; $i++) {
  219. if ($claimedMask & (1 << $i)) {
  220. $claimedCount++;
  221. }
  222. }
  223. $nextDay = $claimedCount + 1;
  224. $this->assertEquals(3, $nextDay);
  225. $this->assertEquals(2, $claimedCount);
  226. // 已签到7次,所有签完
  227. $claimedMask = (1 << 7) - 1; // 0b1111111 = 127
  228. $claimedCount = 0;
  229. for ($i = 0; $i < 7; $i++) {
  230. if ($claimedMask & (1 << $i)) {
  231. $claimedCount++;
  232. }
  233. }
  234. $nextDay = $claimedCount + 1;
  235. $this->assertEquals(7, $claimedCount);
  236. $this->assertEquals(8, $nextDay); // > 7,不能再签
  237. }
  238. /** @test */
  239. public function only_one_claim_per_day()
  240. {
  241. // 如果今天已经签过,不能再次签到
  242. $lastClaimDate = '2026-05-15';
  243. $todayDate = '2026-05-15';
  244. $this->assertTrue($lastClaimDate === $todayDate); // 今天已签
  245. $lastClaimDate = '2026-05-14';
  246. $todayDate = '2026-05-15';
  247. $this->assertFalse($lastClaimDate === $todayDate); // 今天未签
  248. }
  249. /** @test */
  250. public function claimed_bits_match_seven_days_view()
  251. {
  252. // sevenDays 用 bit0 表示 day1,bit1 表示 day2 ...
  253. // claim 中用 dayIndex = day-1,也映射到 bit0=day1, bit1=day2
  254. // 模拟已领取 day1, day2, day3
  255. $claimedMask = (1 << 0) | (1 << 1) | (1 << 2); // 0b0000111 = 7
  256. $this->assertTrue((bool)($claimedMask & (1 << 0))); // day1 已领
  257. $this->assertTrue((bool)($claimedMask & (1 << 1))); // day2 已领
  258. $this->assertTrue((bool)($claimedMask & (1 << 2))); // day3 已领
  259. $this->assertFalse((bool)($claimedMask & (1 << 3))); // day4 未领
  260. $this->assertFalse((bool)($claimedMask & (1 << 6))); // day7 未领
  261. // 所有 7 天都已领完
  262. $allClaimedMask = (1 << 7) - 1; // 0b1111111 = 127
  263. $this->assertEquals(127, $allClaimedMask);
  264. $isAllClaimed = ($claimedMask & $allClaimedMask) == $allClaimedMask;
  265. $this->assertFalse($isAllClaimed); // 只领了3天
  266. $fullMask = 0b1111111;
  267. $this->assertTrue(($fullMask & $allClaimedMask) == $allClaimedMask);
  268. }
  269. /**
  270. * 复制控制器中的 getRecallGiftTiers 逻辑用于纯逻辑测试
  271. */
  272. private function getRecallGiftTiers($avgRecharge)
  273. {
  274. $amounts = [9, 19, 29, 39, 49, 59, 79, 99];
  275. $bonusPercents = [70, 85, 100];
  276. $index = count($amounts);
  277. foreach ($amounts as $i => $amt) {
  278. if ($avgRecharge <= $amt) {
  279. $index = $i;
  280. break;
  281. }
  282. }
  283. if ($index > count($amounts) - 3) {
  284. $index = count($amounts) - 3;
  285. }
  286. $tiers = [];
  287. for ($i = 0; $i < 3; $i++) {
  288. $tiers[] = [
  289. 'amount' => $amounts[$index + $i],
  290. 'bonus_percent' => $bonusPercents[$i],
  291. ];
  292. }
  293. return $tiers;
  294. }
  295. }