AgentTestEnvironment.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. /** 充值/提现通道(tests/fixtures 的线上快照)、支付公司开关,以及代理人民币充值、提现通道迁移。可重复调用。 */
  123. private function paymentChannels(): void
  124. {
  125. if (Schema::hasTable('recharge_channel')) return;
  126. Schema::create('recharge_channel', function (Blueprint $t) {
  127. $t->id(); $t->integer('from'); $t->integer('data_type'); $t->string('key');
  128. $t->string('name'); $t->string('type'); $t->decimal('rate', 8, 4);
  129. $t->decimal('min')->nullable(); $t->decimal('max')->nullable(); $t->string('fixed')->nullable();
  130. $t->integer('status'); $t->integer('sort'); $t->timestamps();
  131. });
  132. Schema::create('recharge_channel_group', function (Blueprint $t) {
  133. $t->id(); $t->string('name'); $t->text('recharge_type')->nullable();
  134. $t->text('withdraw_type')->nullable(); $t->text('activity_type')->nullable(); $t->timestamps();
  135. });
  136. $fixture = require dirname(__DIR__) . '/fixtures/payment_provider_channels.php';
  137. DB::table('recharge_channel')->insert($fixture['channels']);
  138. DB::table('recharge_channel_group')->insert($fixture['groups']);
  139. foreach (['2026_09_08_160000_add_payment_provider_status_and_channel_soft_deletes.php',
  140. '2026_09_08_170000_add_soft_deletes_to_recharge_channel_groups.php',
  141. '2026_10_28_150000_add_recharge_channel_logo.php', '2026_10_28_160000_create_recharge_method_logos.php',
  142. '2026_11_08_120000_create_agent_payment_orders.php', '2026_11_08_170000_add_agent_withdrawal_channels.php'] as $file) {
  143. (require dirname(__DIR__, 2) . '/database/migrations/' . $file)->up();
  144. }
  145. }
  146. /** 用 payment_orders 的完整字段替换简化表(三方下单/回调会按 order_no、pay_no 查会员订单)。 */
  147. private function fullPaymentOrders(): void
  148. {
  149. Schema::drop('payment_orders');
  150. Schema::create('payment_orders', function (Blueprint $t) {
  151. $t->id(); $t->tinyInteger('type'); $t->string('order_no')->unique(); $t->string('member_id');
  152. $t->decimal('amount', 18, 2); $t->decimal('fee', 18, 4)->default(0); $t->string('channel')->nullable();
  153. $t->string('bank_name')->nullable(); $t->integer('payment_type')->nullable(); $t->tinyInteger('status')->default(0);
  154. $t->string('state')->default('0'); $t->string('callback_url')->nullable(); $t->text('callback_data')->nullable();
  155. $t->string('pay_no')->nullable(); $t->text('pay_url')->nullable(); $t->text('pay_data')->nullable();
  156. $t->string('image')->nullable(); $t->tinyInteger('is_send')->default(0); $t->string('remark')->nullable();
  157. $t->text('recharge_bonus_snapshot')->nullable(); $t->timestamps();
  158. });
  159. }
  160. private function agent(int $type = Agent::TYPE_JOINT): Agent
  161. {
  162. static $number = 0;
  163. return $this->agents->create([
  164. 'username' => 'agent' . ++$number, 'password' => 'old-password',
  165. 'withdraw_password' => 'withdraw-old', 'type' => $type,
  166. ]);
  167. }
  168. private function token(Agent $agent): string
  169. {
  170. $plain = 'test-token-' . $agent->id;
  171. AgentToken::query()->create(['agent_id' => $agent->id, 'token_hash' => hash('sha256', $plain),
  172. 'expires_at' => now()->addDay(), 'last_used_at' => now(), 'ip' => '', 'device' => 'pc']);
  173. return $plain;
  174. }
  175. }