AdminListAccountTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Services\BackflowService;
  4. use App\Services\RebateService;
  5. use App\Services\RechargeService;
  6. use Illuminate\Config\Repository;
  7. use Illuminate\Database\Capsule\Manager;
  8. use Illuminate\Database\Schema\Blueprint;
  9. use Illuminate\Foundation\Application;
  10. use Illuminate\Support\Facades\DB;
  11. use Illuminate\Support\Facades\Facade;
  12. use PHPUnit\Framework\TestCase;
  13. /** 后台列表返回会员账号 account,并支持按 account 模糊筛选(关联查询与 join 查询两种写法)。 */
  14. class AdminListAccountTest extends TestCase
  15. {
  16. protected function setUp(): void
  17. {
  18. parent::setUp();
  19. Facade::clearResolvedInstances();
  20. $app = new Application(dirname(__DIR__, 2));
  21. $app->instance('config', new Repository(['app' => ['timezone' => 'Asia/Shanghai', 'url' => 'http://x']]));
  22. Facade::setFacadeApplication($app);
  23. $db = new Manager($app);
  24. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  25. $db->setAsGlobal();
  26. $db->bootEloquent();
  27. $app->instance('db', $db->getDatabaseManager());
  28. $app->bind('db.schema', fn () => $db->schema());
  29. $schema = $db->schema();
  30. $schema->create('config', function (Blueprint $t) {
  31. $t->id(); $t->string('field'); $t->text('val'); $t->integer('group_id');
  32. });
  33. $schema->create('users', function (Blueprint $t) {
  34. $t->id(); $t->string('member_id'); $t->string('account')->nullable(); $t->string('username')->nullable();
  35. $t->string('first_name')->nullable(); $t->string('admin_note')->nullable(); $t->integer('status')->default(0);
  36. $t->string('phone')->nullable(); $t->string('visitor_id')->nullable(); $t->string('register_ip')->nullable(); $t->timestamps();
  37. });
  38. $schema->create('rebates', function (Blueprint $t) {
  39. $t->id(); $t->string('member_id'); $t->date('date'); $t->integer('status')->default(0);
  40. $t->decimal('betting_amount', 20, 2)->default(0); $t->timestamps();
  41. });
  42. $schema->create('backflow', function (Blueprint $t) {
  43. $t->id(); $t->string('member_id'); $t->string('date'); $t->integer('status')->default(0);
  44. $t->decimal('amount', 20, 2)->default(0); $t->decimal('granted_amount', 20, 2)->default(0); $t->timestamps();
  45. });
  46. $schema->create('backflow_rules', function (Blueprint $t) {
  47. $t->id(); $t->string('category'); $t->decimal('min_loss_amount', 20, 2); $t->decimal('reward_amount', 20, 2);
  48. $t->decimal('turnover_multiple', 20, 2)->default(0);
  49. });
  50. $schema->create('share_bet_facts', function (Blueprint $t) {
  51. $t->id(); $t->unsignedBigInteger('user_id'); $t->string('category'); $t->date('bet_date'); $t->dateTime('bet_at');
  52. $t->string('source'); $t->string('source_id'); $t->string('member_id'); $t->boolean('settled')->default(true);
  53. $t->decimal('bet_amount', 24, 10); $t->decimal('effective_amount', 24, 10); $t->decimal('net_profit', 24, 10);
  54. $t->boolean('voided')->default(false); $t->string('currency')->default('CNY');
  55. });
  56. $schema->create('lhc_order', function (Blueprint $t) {
  57. $t->id(); $t->string('member_id'); $t->string('ordernum'); $t->integer('type');
  58. });
  59. $schema->create('lhc_order_item', function (Blueprint $t) {
  60. $t->id(); $t->string('member_id'); $t->string('ordernum');
  61. });
  62. $schema->create('recharges', function (Blueprint $t) {
  63. $t->id(); $t->string('member_id'); $t->string('coin')->default('CNY'); $t->decimal('amount', 20, 2)->default(0);
  64. $t->integer('status')->default(0); $t->timestamps();
  65. });
  66. DB::table('users')->insert([
  67. ['member_id' => 'm1', 'account' => 'alice'],
  68. ['member_id' => 'm2', 'account' => 'bob'],
  69. ['member_id' => 'm3', 'account' => null],
  70. ]);
  71. }
  72. protected function tearDown(): void
  73. {
  74. DB::disconnect();
  75. Facade::clearResolvedInstances();
  76. parent::tearDown();
  77. }
  78. public function test_rebate_list_shows_and_filters_account(): void
  79. {
  80. foreach (['m1', 'm2', 'm3'] as $m) {
  81. DB::table('rebates')->insert(['member_id' => $m, 'date' => '2026-01-01']);
  82. }
  83. $rows = $this->accounts(RebateService::paginate(['limit' => 10])['data']);
  84. $this->assertSame(['m1' => 'alice', 'm2' => 'bob', 'm3' => ''], $rows);
  85. $this->assertSame(['m1' => 'alice'], $this->accounts(RebateService::paginate(['account' => 'lic'])['data']));
  86. $this->assertSame(0, RebateService::paginate(['account' => 'nobody'])['total']);
  87. }
  88. public function test_rebate_list_shows_the_current_day_but_only_past_unpaid_days_can_be_granted(): void
  89. {
  90. \Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2026-11-05 15:00:00', 'Asia/Shanghai')); // 纽约 2026-11-05 02:00
  91. try {
  92. DB::table('rebates')->insert([
  93. ['member_id' => 'm1', 'date' => '2026-11-05', 'status' => 0],
  94. ['member_id' => 'm2', 'date' => '2026-11-04', 'status' => 0],
  95. ['member_id' => 'm3', 'date' => '2026-11-04', 'status' => 1],
  96. ]);
  97. $rows = [];
  98. foreach (json_decode(json_encode(RebateService::paginate(['limit' => 10])['data']), true) as $row) {
  99. $rows[$row['member_id']] = [$row['date'], $row['can_grant']];
  100. }
  101. $this->assertSame(['m1' => ['2026-11-05', false], 'm2' => ['2026-11-04', true], 'm3' => ['2026-11-04', false]], $rows);
  102. } finally {
  103. \Carbon\Carbon::setTestNow();
  104. }
  105. }
  106. public function test_backflow_list_shows_and_filters_account(): void
  107. {
  108. foreach (['m1', 'm2'] as $m) {
  109. DB::table('backflow')->insert(['member_id' => $m, 'date' => '2026-01']);
  110. }
  111. $this->assertSame(['m1' => 'alice', 'm2' => 'bob'], $this->accounts(BackflowService::paginate(['limit' => 10])['data']));
  112. $this->assertSame(['m2' => 'bob'], $this->accounts(BackflowService::paginate(['limit' => 10, 'account' => 'bo'])['data']));
  113. }
  114. public function test_backflow_list_recalculates_unpaid_rows_with_current_rules_and_shows_granted_and_pending(): void
  115. {
  116. \Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2026-11-05 15:00:00', 'Asia/Shanghai'));
  117. try {
  118. DB::table('backflow_rules')->insert([
  119. ['category' => 'self:lottery:pc28', 'min_loss_amount' => 10, 'reward_amount' => 8],
  120. ['category' => 'self:lottery:pc28', 'min_loss_amount' => 300, 'reward_amount' => 50],
  121. ]);
  122. foreach ([1 => '-50', 2 => '-350', 3 => '-50'] as $userId => $profit) {
  123. $order = ['member_id' => "m{$userId}", 'ordernum' => "backflow-{$userId}"];
  124. DB::table('lhc_order')->insert($order + ['type' => 5]);
  125. $itemId = DB::table('lhc_order_item')->insertGetId($order);
  126. DB::table('share_bet_facts')->insert(['user_id' => $userId, 'category' => 'self:lottery', 'bet_date' => '2026-11-03',
  127. 'bet_at' => '2026-11-03 12:00:00', 'bet_amount' => 100, 'effective_amount' => 100, 'net_profit' => $profit,
  128. 'member_id' => $order['member_id'], 'source' => 'lhc_order_item', 'source_id' => (string) $itemId]);
  129. }
  130. // 存储的 amount=20 是旧规则算出的过期值;列表应按当前规则显示
  131. DB::table('backflow')->insert([
  132. ['member_id' => 'm1', 'date' => '2026-11', 'status' => 0, 'amount' => 20, 'granted_amount' => 0], // 当月未发:应发 8
  133. ['member_id' => 'm2', 'date' => '2026-11', 'status' => 2, 'amount' => 20, 'granted_amount' => 8], // 部分发放:应发 50,待发 42
  134. ['member_id' => 'm3', 'date' => '2026-11', 'status' => 2, 'amount' => 20, 'granted_amount' => 8], // 已发完:旧值会误显示待发 12
  135. ['member_id' => 'm1', 'date' => '2026-10', 'status' => 0, 'amount' => 5, 'granted_amount' => 0], // 往月无注单:应发 0,可点发放结清
  136. ['member_id' => 'm2', 'date' => '2026-10', 'status' => 1, 'amount' => 35, 'granted_amount' => 35], // 已发放:保留存储值
  137. ['member_id' => 'm1', 'date' => '2026-12', 'status' => 0, 'amount' => 0, 'granted_amount' => 0], // 未来月份不展示
  138. ]);
  139. $rows = [];
  140. foreach (json_decode(json_encode(BackflowService::paginate(['limit' => 10])['data']), true) as $row) {
  141. $rows[$row['member_id'] . '|' . $row['date']] = [$row['amount'], $row['granted_amount'], $row['pending_amount'], $row['can_grant'], $row['status_text']];
  142. }
  143. ksort($rows);
  144. $this->assertSame([
  145. 'm1|2026-10' => ['0.00', '0.00', '0.00', true, '未发放'],
  146. 'm1|2026-11' => ['8.00', '0.00', '8.00', true, '未发放'],
  147. 'm2|2026-10' => ['35.00', '35.00', '0.00', false, '已发放'],
  148. 'm2|2026-11' => ['50.00', '8.00', '42.00', true, '部分发放'],
  149. 'm3|2026-11' => ['8.00', '8.00', '0.00', false, '部分发放'],
  150. ], $rows);
  151. $this->assertSame(2, BackflowService::paginate(['limit' => 10, 'status' => 2])['total']);
  152. } finally {
  153. \Carbon\Carbon::setTestNow();
  154. }
  155. }
  156. public function test_recharge_list_shows_and_filters_account(): void
  157. {
  158. foreach (['m1', 'm2'] as $m) {
  159. DB::table('recharges')->insert(['member_id' => $m, 'amount' => 1, 'created_at' => '2026-01-01 00:00:00']);
  160. }
  161. $this->assertSame(['m1' => 'alice', 'm2' => 'bob'], $this->accounts(RechargeService::paginate([])['data']));
  162. $this->assertSame(['m1' => 'alice'], $this->accounts(RechargeService::paginate(['account' => 'ALI'])['data']));
  163. }
  164. private function accounts(array $rows): array
  165. {
  166. $out = [];
  167. foreach (json_decode(json_encode($rows), true) as $row) {
  168. $out[$row['member_id']] = $row['account'];
  169. }
  170. ksort($out);
  171. return $out;
  172. }
  173. }