app = new Application(dirname(__DIR__, 2)); $this->app->instance('config', new Repository(['app' => ['locale' => 'zh'], 'agent' => []])); Facade::setFacadeApplication($this->app); $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setEventDispatcher(new Dispatcher($this->app)); $db->setAsGlobal(); $db->bootEloquent(); // 生产环境由 DatabaseServiceProvider 绑定;DB::afterCommit 依赖它。 $this->app->instance('db.transactions', new \Illuminate\Database\DatabaseTransactionsManager()); $this->app->instance('db', $db->getDatabaseManager()); $this->app->bind('db.schema', fn () => $db->schema()); $this->app->instance('hash', new BcryptHasher(['rounds' => 4])); $translator = new Translator(new ArrayLoader(), 'zh'); $this->app->instance('translator', $translator); $validator = new Factory($translator, $this->app); $validator->setPresenceVerifier(new \Illuminate\Validation\DatabasePresenceVerifier($db->getDatabaseManager())); $this->app->instance('validator', $validator); $this->app->instance('request', Request::create('/')); Request::macro('validate', function (array $rules) { return app('validator')->make($this->all(), $rules)->validate(); }); $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new JsonResponseFactory()); Schema::create('users', function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->string('username'); $t->string('first_name')->default(''); $t->integer('status')->default(1); $t->string('register_ip')->default(''); $t->string('register_domain')->default(''); $t->timestamps(); }); // The profit migration inspects MySQL index metadata before creating indexes. DB::connection()->getPdo()->sqliteCreateFunction('DATABASE', static fn () => 'isolated'); DB::statement("ATTACH DATABASE ':memory:' AS information_schema"); 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)'); foreach (['2026_08_24_140000_create_agent_system_tables.php', '2026_08_28_120000_create_agent_profit_profiles_and_rebate_records.php'] as $file) { (require dirname(__DIR__, 2) . '/database/migrations/' . $file)->up(); } // Pre-upgrade flow commission and owner columns (their MySQL-only migrations use information_schema). Schema::create('agent_flow_commission_profiles', function (Blueprint $t) { $t->id(); $t->string('name'); $t->integer('is_default')->default(0); $t->timestamps(); }); Schema::create('agent_flow_commission_assignments', function (Blueprint $t) { $t->id(); $t->unsignedBigInteger('agent_id'); $t->date('effective_from'); $t->timestamps(); }); Schema::table('agents', fn (Blueprint $t) => $t->unsignedBigInteger('flow_commission_profile_id')->nullable()->index()); Schema::table('agents', fn (Blueprint $t) => $t->string('language', 8)->default('zh')); Schema::table('agents', fn (Blueprint $t) => $t->unsignedBigInteger('owner_user_id')->nullable()); Schema::table('agent_profit_commission_profiles', fn (Blueprint $t) => $t->unsignedBigInteger('owner_agent_id')->nullable()); Schema::create('share_system_state', function (Blueprint $t) { $t->string('key')->primary(); $t->text('value')->nullable(); $t->timestamps(); }); Schema::create('share_policies', function (Blueprint $t) { $t->id(); $t->date('effective_date'); $t->json('rules'); $t->timestamps(); }); DB::table('share_policies')->insert(['effective_date' => '1970-01-01', 'rules' => json_encode(['third:1' => [], 'third:3' => [], 'self:lottery' => []])]); (require dirname(__DIR__, 2) . '/database/migrations/2026_11_07_120000_agent_joint_venture_and_equity.php')->up(); foreach (['recharges', 'withdraws', 'payment_orders'] as $table) { Schema::create($table, function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->integer('status'); $t->string('type')->nullable(); $t->decimal('amount', 18, 4); $t->timestamps(); }); } Schema::create('wallets', function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->decimal('available_balance', 18, 4)->default(0); $t->decimal('frozen_balance', 18, 4)->default(0); }); Schema::create('balance_logs', function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->string('change_type'); $t->integer('related_id')->nullable(); $t->decimal('amount', 18, 4); $t->timestamps(); }); Schema::create('config', function (Blueprint $t) { $t->id(); $t->string('field'); $t->string('val')->nullable(); }); Schema::create('share_bet_facts', function (Blueprint $t) { $t->id(); $t->string('source', 32); $t->string('source_id', 64); $t->unsignedBigInteger('user_id'); $t->string('member_id', 64)->default(''); $t->string('category', 32); $t->dateTime('bet_at'); $t->date('bet_date'); $t->dateTime('settled_at')->nullable(); foreach (['bet_amount', 'net_profit', 'effective_amount'] as $c) $t->decimal($c, 24, 10)->default(0); $t->boolean('settled')->default(false); $t->boolean('voided')->default(false); $t->timestamps(); }); $this->agents = new AgentService(); } protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); Carbon::setTestNow(); parent::tearDown(); } private function agent(int $type = Agent::TYPE_JOINT): Agent { static $number = 0; return $this->agents->create([ 'username' => 'agent' . ++$number, 'password' => 'old-password', 'withdraw_password' => 'withdraw-old', 'type' => $type, ]); } private function token(Agent $agent): string { $plain = 'test-token-' . $agent->id; AgentToken::query()->create(['agent_id' => $agent->id, 'token_hash' => hash('sha256', $plain), 'expires_at' => now()->addDay(), 'last_used_at' => now(), 'ip' => '', 'device' => 'pc']); return $plain; } }