AgentTestEnvironment.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. <?php
  2. namespace Tests\Support;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentToken;
  5. use App\Services\Agent\AgentService;
  6. use Carbon\Carbon;
  7. use Illuminate\Config\Repository;
  8. use Illuminate\Database\Capsule\Manager;
  9. use Illuminate\Database\Schema\Blueprint;
  10. use Illuminate\Events\Dispatcher;
  11. use Illuminate\Foundation\Application;
  12. use Illuminate\Hashing\BcryptHasher;
  13. use Illuminate\Http\Request;
  14. use Illuminate\Support\Facades\DB;
  15. use Illuminate\Support\Facades\Facade;
  16. use Illuminate\Support\Facades\Schema;
  17. use Illuminate\Translation\ArrayLoader;
  18. use Illuminate\Translation\Translator;
  19. use Illuminate\Validation\Factory;
  20. /**
  21. * 代理模块集成测试环境:隔离 SQLite(表前缀 bot_),按生产顺序跑代理迁移,
  22. * 包括把旧的流水佣金/二级代理结构迁移到合营/入股结构的 2026_11_07 迁移。
  23. */
  24. trait AgentTestEnvironment
  25. {
  26. private Application $app;
  27. private AgentService $agents;
  28. protected function setUp(): void
  29. {
  30. parent::setUp();
  31. Carbon::setTestNow(Carbon::parse('2026-09-07 12:00:00'));
  32. Facade::clearResolvedInstances();
  33. $this->app = new Application(dirname(__DIR__, 2));
  34. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh'], 'agent' => []]));
  35. Facade::setFacadeApplication($this->app);
  36. $db = new Manager($this->app);
  37. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  38. $db->setEventDispatcher(new Dispatcher($this->app));
  39. $db->setAsGlobal();
  40. $db->bootEloquent();
  41. // 生产环境由 DatabaseServiceProvider 绑定;DB::afterCommit 依赖它。
  42. $this->app->instance('db.transactions', new \Illuminate\Database\DatabaseTransactionsManager());
  43. $this->app->instance('db', $db->getDatabaseManager());
  44. $this->app->bind('db.schema', fn () => $db->schema());
  45. $this->app->instance('hash', new BcryptHasher(['rounds' => 4]));
  46. $translator = new Translator(new ArrayLoader(), 'zh');
  47. $this->app->instance('translator', $translator);
  48. $validator = new Factory($translator, $this->app);
  49. $validator->setPresenceVerifier(new \Illuminate\Validation\DatabasePresenceVerifier($db->getDatabaseManager()));
  50. $this->app->instance('validator', $validator);
  51. $this->app->instance('request', Request::create('/'));
  52. Request::macro('validate', function (array $rules) {
  53. return app('validator')->make($this->all(), $rules)->validate();
  54. });
  55. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new JsonResponseFactory());
  56. Schema::create('users', function (Blueprint $t) {
  57. $t->id(); $t->string('member_id'); $t->string('username'); $t->string('first_name')->default('');
  58. $t->integer('status')->default(1); $t->string('register_ip')->default('');
  59. $t->string('register_domain')->default(''); $t->timestamps();
  60. });
  61. // The profit migration inspects MySQL index metadata before creating indexes.
  62. DB::connection()->getPdo()->sqliteCreateFunction('DATABASE', static fn () => 'isolated');
  63. DB::statement("ATTACH DATABASE ':memory:' AS information_schema");
  64. DB::statement('CREATE TABLE information_schema.statistics (table_schema TEXT,table_name TEXT,index_name TEXT,column_name TEXT,non_unique INTEGER,seq_in_index INTEGER)');
  65. foreach (['2026_08_24_140000_create_agent_system_tables.php',
  66. '2026_08_28_120000_create_agent_profit_profiles_and_rebate_records.php'] as $file) {
  67. (require dirname(__DIR__, 2) . '/database/migrations/' . $file)->up();
  68. }
  69. // Pre-upgrade flow commission and owner columns (their MySQL-only migrations use information_schema).
  70. Schema::create('agent_flow_commission_profiles', function (Blueprint $t) {
  71. $t->id(); $t->string('name'); $t->integer('is_default')->default(0); $t->timestamps();
  72. });
  73. Schema::create('agent_flow_commission_assignments', function (Blueprint $t) {
  74. $t->id(); $t->unsignedBigInteger('agent_id'); $t->date('effective_from'); $t->timestamps();
  75. });
  76. Schema::table('agents', fn (Blueprint $t) => $t->unsignedBigInteger('flow_commission_profile_id')->nullable()->index());
  77. Schema::table('agents', fn (Blueprint $t) => $t->string('language', 8)->default('zh'));
  78. Schema::table('agents', fn (Blueprint $t) => $t->unsignedBigInteger('owner_user_id')->nullable());
  79. Schema::table('agent_profit_commission_profiles', fn (Blueprint $t) => $t->unsignedBigInteger('owner_agent_id')->nullable());
  80. Schema::create('share_system_state', function (Blueprint $t) {
  81. $t->string('key')->primary(); $t->text('value')->nullable(); $t->timestamps();
  82. });
  83. Schema::create('share_policies', function (Blueprint $t) {
  84. $t->id(); $t->date('effective_date'); $t->json('rules'); $t->timestamps();
  85. });
  86. DB::table('share_policies')->insert(['effective_date' => '1970-01-01',
  87. 'rules' => json_encode(['third:1' => [], 'third:3' => [], 'self:lottery' => []])]);
  88. (require dirname(__DIR__, 2) . '/database/migrations/2026_11_07_120000_agent_joint_venture_and_equity.php')->up();
  89. foreach (['recharges', 'withdraws', 'payment_orders'] as $table) {
  90. Schema::create($table, function (Blueprint $t) {
  91. $t->id(); $t->string('member_id'); $t->integer('status'); $t->string('type')->nullable();
  92. $t->decimal('amount', 18, 4); $t->timestamps();
  93. });
  94. }
  95. Schema::create('wallets', function (Blueprint $t) {
  96. $t->id(); $t->string('member_id'); $t->decimal('available_balance', 18, 4)->default(0);
  97. $t->decimal('frozen_balance', 18, 4)->default(0);
  98. });
  99. Schema::create('balance_logs', function (Blueprint $t) {
  100. $t->id(); $t->string('member_id'); $t->string('change_type');
  101. $t->integer('related_id')->nullable(); $t->decimal('amount', 18, 4); $t->timestamps();
  102. });
  103. Schema::create('config', function (Blueprint $t) {
  104. $t->id(); $t->string('field'); $t->string('val')->nullable();
  105. });
  106. Schema::create('share_bet_facts', function (Blueprint $t) {
  107. $t->id(); $t->string('source', 32); $t->string('source_id', 64); $t->unsignedBigInteger('user_id');
  108. $t->string('member_id', 64)->default(''); $t->string('category', 32); $t->dateTime('bet_at');
  109. $t->date('bet_date'); $t->dateTime('settled_at')->nullable();
  110. foreach (['bet_amount', 'net_profit', 'effective_amount'] as $c) $t->decimal($c, 24, 10)->default(0);
  111. $t->boolean('settled')->default(false); $t->boolean('voided')->default(false); $t->timestamps();
  112. });
  113. $this->agents = new AgentService();
  114. }
  115. protected function tearDown(): void
  116. {
  117. DB::disconnect();
  118. Facade::clearResolvedInstances();
  119. Carbon::setTestNow();
  120. parent::tearDown();
  121. }
  122. private function agent(int $type = Agent::TYPE_JOINT): Agent
  123. {
  124. static $number = 0;
  125. return $this->agents->create([
  126. 'username' => 'agent' . ++$number, 'password' => 'old-password',
  127. 'withdraw_password' => 'withdraw-old', 'type' => $type,
  128. ]);
  129. }
  130. private function token(Agent $agent): string
  131. {
  132. $plain = 'test-token-' . $agent->id;
  133. AgentToken::query()->create(['agent_id' => $agent->id, 'token_hash' => hash('sha256', $plain),
  134. 'expires_at' => now()->addDay(), 'last_used_at' => now(), 'ip' => '', 'device' => 'pc']);
  135. return $plain;
  136. }
  137. }