doge 13 часов назад
Родитель
Сommit
15275738ff

+ 10 - 0
app/Models/User.php

@@ -28,9 +28,19 @@ class User extends BaseModel
     'game_id', 'username', 'secret_key', 'secret_pass', 'language','user_code', 'agent_user_code', 'agent_id', 'level','recharge_channel_group_id'];
     'game_id', 'username', 'secret_key', 'secret_pass', 'language','user_code', 'agent_user_code', 'agent_id', 'level','recharge_channel_group_id'];
     protected $attributes = [
     protected $attributes = [
         'language' => 'zh',
         'language' => 'zh',
+        'recharge_channel_group_id' => 1,
     ];
     ];
     protected $hidden = ['updated_at'];
     protected $hidden = ['updated_at'];
 
 
+    protected static function booted()
+    {
+        static::creating(static function (self $user) {
+            if (empty($user->recharge_channel_group_id)) {
+                $user->recharge_channel_group_id = 1;
+            }
+        });
+    }
+
     
     
     // public function newQuery($excludeDeleted = true): Builder
     // public function newQuery($excludeDeleted = true): Builder
     // {
     // {

+ 21 - 0
database/migrations/2026_10_25_130000_default_user_recharge_channel_group_to_one.php

@@ -0,0 +1,21 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        $grammar = DB::connection()->getQueryGrammar();
+        DB::statement('ALTER TABLE ' . $grammar->wrapTable('users')
+            . ' ALTER COLUMN ' . $grammar->wrap('recharge_channel_group_id') . ' SET DEFAULT 1');
+    }
+
+    public function down(): void
+    {
+        $grammar = DB::connection()->getQueryGrammar();
+        DB::statement('ALTER TABLE ' . $grammar->wrapTable('users')
+            . ' ALTER COLUMN ' . $grammar->wrap('recharge_channel_group_id') . ' SET DEFAULT 0');
+    }
+};

+ 2 - 0
tests/Integration/SharingSettlementTest.php

@@ -31,6 +31,7 @@ class SharingSettlementTest extends TestCase
         $db->schema()->create('users',function(Blueprint $t){
         $db->schema()->create('users',function(Blueprint $t){
             $t->id(); $t->string('member_id'); $t->string('user_code'); $t->string('agent_user_code')->nullable(); $t->integer('from')->default(1); $t->timestamps();
             $t->id(); $t->string('member_id'); $t->string('user_code'); $t->string('agent_user_code')->nullable(); $t->integer('from')->default(1); $t->timestamps();
             $t->string('user_id')->nullable(); $t->string('first_name')->nullable(); $t->string('username')->nullable(); $t->string('language')->nullable();
             $t->string('user_id')->nullable(); $t->string('first_name')->nullable(); $t->string('username')->nullable(); $t->string('language')->nullable();
+            $t->integer('recharge_channel_group_id')->default(0);
         });
         });
         $db->schema()->create('share_settings',function(Blueprint $t){
         $db->schema()->create('share_settings',function(Blueprint $t){
             $t->id(); $t->boolean('enabled')->default(1); $t->boolean('benefit_enabled')->default(0); $t->string('share_domain')->default('https://pc.example');
             $t->id(); $t->boolean('enabled')->default(1); $t->boolean('benefit_enabled')->default(0); $t->string('share_domain')->default('https://pc.example');
@@ -339,6 +340,7 @@ class SharingSettlementTest extends TestCase
         $user=\App\Services\PublicService::index('M99','telegram-new','新会员');
         $user=\App\Services\PublicService::index('M99','telegram-new','新会员');
         $id=(int)$user->id;
         $id=(int)$user->id;
         $this->assertSame(-1,(int)$user->from);
         $this->assertSame(-1,(int)$user->from);
+        $this->assertSame(1,(int)$user->recharge_channel_group_id);
         $this->assertSame('root',DB::table('share_members')->where('user_id',$id)->value('binding_status'));
         $this->assertSame('root',DB::table('share_members')->where('user_id',$id)->value('binding_status'));
         $this->assertSame((string)$user->user_code,DB::table('share_members')->where('user_id',$id)->value('invitation_code'));
         $this->assertSame((string)$user->user_code,DB::table('share_members')->where('user_id',$id)->value('invitation_code'));
         $this->assertSame(0,DB::table('share_tree')->where('ancestor_user_id',$id)->where('descendant_user_id',$id)->value('depth'));
         $this->assertSame(0,DB::table('share_tree')->where('ancestor_user_id',$id)->where('descendant_user_id',$id)->value('depth'));

+ 90 - 0
tests/Integration/UserRechargeChannelGroupDefaultTest.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace Tests\Integration;
+
+use App\Models\User;
+use Illuminate\Config\Repository;
+use Illuminate\Database\Capsule\Manager;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Events\Dispatcher;
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Facade;
+use PHPUnit\Framework\TestCase;
+
+/** The database deliberately retains legacy DEFAULT 0 to verify application-level inserts. */
+class UserRechargeChannelGroupDefaultTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        Facade::clearResolvedInstances();
+        $app = new Application(dirname(__DIR__, 2));
+        $app->instance('config', new Repository());
+        Facade::setFacadeApplication($app);
+        $db = new Manager($app);
+        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->setEventDispatcher(new Dispatcher($app));
+        $db->setAsGlobal(); $db->bootEloquent();
+        $app->instance('db', $db->getDatabaseManager());
+        User::clearBootedModels();
+        $db->schema()->create('users', function (Blueprint $table) {
+            $table->id(); $table->string('member_id')->unique(); $table->string('first_name')->nullable();
+            $table->string('language')->default('zh');
+            $table->integer('recharge_channel_group_id')->nullable()->default(0); $table->timestamps();
+        });
+    }
+
+    protected function tearDown(): void
+    {
+        DB::disconnect(); User::clearBootedModels(); Facade::clearResolvedInstances(); parent::tearDown();
+    }
+
+    public function test_new_users_default_to_one_even_if_database_default_is_zero(): void
+    {
+        foreach ([[], ['recharge_channel_group_id' => 0], ['recharge_channel_group_id' => '0'],
+            ['recharge_channel_group_id' => null], ['recharge_channel_group_id' => '']] as $index => $extra) {
+            $user = User::create(['member_id' => 'new-' . $index] + $extra);
+            $this->assertSame(1, (int)$user->recharge_channel_group_id);
+            $this->assertSame(1, (int)DB::table('users')->where('id', $user->id)->value('recharge_channel_group_id'));
+        }
+    }
+
+    public function test_explicit_positive_groups_and_existing_group_assignments_are_preserved(): void
+    {
+        $assigned = User::create(['member_id' => 'assigned', 'recharge_channel_group_id' => 7]);
+        $this->assertSame(7, (int)$assigned->fresh()->recharge_channel_group_id);
+        $legacyId = DB::table('users')->insertGetId(['member_id' => 'legacy', 'recharge_channel_group_id' => 0]);
+        foreach ([$legacyId => 0, $assigned->id => 7] as $id => $expected) {
+            $user = User::findOrFail($id);
+            $this->assertSame($expected, (int)$user->recharge_channel_group_id);
+            $user->first_name = 'updated profile';
+            $user->save();
+            $this->assertSame($expected, (int)$user->fresh()->recharge_channel_group_id);
+        }
+        $assigned->recharge_channel_group_id = 2;
+        $assigned->save();
+        $this->assertSame(2, (int)$assigned->fresh()->recharge_channel_group_id);
+    }
+
+    public function test_migration_only_changes_the_prefixed_column_default_without_rewriting_existing_users(): void
+    {
+        app('config')->set('database.connections.mysql_preview', [
+            'driver' => 'mysql', 'host' => '127.0.0.1', 'database' => 'not_connected',
+            'username' => 'not_connected', 'password' => '', 'prefix' => 'bot_',
+        ]);
+        $original = DB::getDefaultConnection();
+        DB::setDefaultConnection('mysql_preview');
+        try {
+            $connection = DB::connection();
+            $connection->setPdo(static function () { throw new \RuntimeException('SQL preview must not connect to a real database.'); });
+            $migration = require dirname(__DIR__, 2) . '/database/migrations/2026_10_25_130000_default_user_recharge_channel_group_to_one.php';
+            $queries = $connection->pretend(function () use ($migration) { $migration->up(); $migration->down(); });
+            $this->assertSame([
+                'ALTER TABLE `bot_users` ALTER COLUMN `recharge_channel_group_id` SET DEFAULT 1',
+                'ALTER TABLE `bot_users` ALTER COLUMN `recharge_channel_group_id` SET DEFAULT 0',
+            ], array_column($queries, 'query'));
+        } finally {
+            DB::disconnect('mysql_preview'); DB::setDefaultConnection($original);
+        }
+    }
+}

+ 87 - 0
tests/Regression/registration_default_recharge_group.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace app\api\service {
+    final class UserTokenService {
+        public static function setToken($userId): array {
+            $row = \think\facade\Db::table('bot_users')->where('user_id', $userId)->find();
+            $GLOBALS['registration_token_groups'][] = $row['recharge_channel_group_id'];
+            return ['user_id' => $userId, 'account' => $row['account'], 'token' => 'test-only-token', 'expire_time' => time() + 3600];
+        }
+    }
+}
+namespace app\common\service {
+    final class FileService { public static function getFileUrl($value): string { return (string)$value; } }
+}
+namespace app\common\model {
+    final class UserLogin { public const STATUS_SUCCESS = 1; public static function record(...$args): void {} }
+}
+namespace {
+    error_reporting(E_ALL & ~E_DEPRECATED);
+    function request() { return $GLOBALS['registration_request']; }
+    function generateUserCode(): string { static $sequence = 0; return 'OWN' . str_pad((string)++$sequence, 5, '0', STR_PAD_LEFT); }
+    function create_password($password, $salt): string { return hash('sha256', $salt . $password); }
+    function avatarUrl($value): string { return (string)$value; }
+    function getDeviceUniqueId(): string { return 'test-device'; }
+    class RegistrationRequest {
+        public function ip(): string { return '127.0.0.1'; }
+        public function header($name, $default = '') { return $name === 'referer' ? 'https://test.example.test/register' : $default; }
+        public function param($name, $default = null) { return $default; }
+    }
+    $GLOBALS['registration_request'] = new RegistrationRequest();
+    $GLOBALS['registration_token_groups'] = [];
+    $repo = getenv('REGISTRATION_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api';
+    $stage = getenv('REGISTRATION_SITE_STAGE') ?: $repo;
+    $loader = require $repo . '/vendor/autoload.php';
+    $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']);
+    $app = new \think\App($repo . '/');
+    \think\Container::setInstance($app);
+    $db = new \think\DbManager();
+    $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [
+        'type' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'fields_strict' => true, 'debug' => false,
+    ]], 'auto_timestamp' => false]);
+    $app->instance(\think\DbManager::class, $db);
+    $app->instance('log', new \Psr\Log\NullLogger());
+    \think\Model::setInvoker([$app, 'invoke']);
+    \think\facade\Config::set(['unique_identification' => 'test-only-salt'], 'project');
+    $db->execute('CREATE TABLE bot_users (id INTEGER PRIMARY KEY AUTOINCREMENT,user_id TEXT,member_id TEXT,account TEXT,first_name TEXT,realname TEXT,avatar TEXT,password TEXT,phone TEXT,email TEXT,register_ip TEXT DEFAULT "",register_domain TEXT,device_id TEXT DEFAULT "",last_active_time INTEGER,agent_user_code TEXT,user_code TEXT,"from" INTEGER DEFAULT 1,recharge_channel_group_id INTEGER DEFAULT 0,delete_time TEXT,created_at TEXT,updated_at TEXT)');
+    $db->execute('CREATE TABLE bot_wallets (id INTEGER PRIMARY KEY AUTOINCREMENT,user_id INTEGER,member_id TEXT,net TEXT,coin TEXT,available_balance TEXT DEFAULT "0",created_at TEXT,updated_at TEXT)');
+    $db->execute('CREATE TABLE bot_share_system_state ("key" TEXT PRIMARY KEY,value TEXT)');
+    $db->execute('CREATE TABLE bot_share_members (user_id INTEGER PRIMARY KEY,parent_user_id INTEGER,invitation_code TEXT,binding_status TEXT,level INTEGER,bound_at TEXT,created_at TEXT,updated_at TEXT)');
+    $db->execute('CREATE TABLE bot_share_tree (ancestor_user_id INTEGER,descendant_user_id INTEGER,depth INTEGER,bound_at TEXT,PRIMARY KEY(ancestor_user_id,descendant_user_id))');
+    \think\facade\Db::table('bot_users')->insert(['id' => 1, 'user_id' => '10000001', 'member_id' => '10000001', 'account' => 'inviter', 'user_code' => 'INVITE01', 'recharge_channel_group_id' => 7]);
+    \think\facade\Db::table('bot_share_members')->insert(['user_id' => 1, 'parent_user_id' => null, 'invitation_code' => 'INVITE01', 'binding_status' => 'root', 'level' => 1, 'bound_at' => '2000-01-01 00:00:00']);
+    \think\facade\Db::table('bot_share_tree')->insert(['ancestor_user_id' => 1, 'descendant_user_id' => 1, 'depth' => 0, 'bound_at' => '2000-01-01 00:00:00']);
+    \think\facade\Db::table('bot_share_system_state')->insert(['key' => 'relations_lock', 'value' => '1']);
+    $checks = 0;
+    function check($actual, $expected, string $message): void {
+        global $checks; ++$checks;
+        if ($actual !== $expected) throw new \RuntimeException($message . ': actual=' . json_encode($actual) . ' expected=' . json_encode($expected));
+    }
+    foreach (['account' => 'new-account', 'phone' => '13800000001', 'email' => 'new@example.test'] as $type => $account) {
+        $result = \app\api\logic\LoginLogic::register(['account' => $account, 'password' => 'secret12', 'type' => $type, 'invitationCode' => 'INVITE01', 'recharge_channel_group_id' => 9]);
+        check(is_array($result), true, 'Real registration succeeds: ' . ($result === false ? \app\api\logic\LoginLogic::getError() : $type));
+        $row = \think\facade\Db::table('bot_users')->where('account', $account)->find();
+        check($row['recharge_channel_group_id'], 1, 'All registration types assign default group instead of database zero or client input');
+        check($row['from'], 1, 'Real registration remains real');
+        check(\think\facade\Db::table('bot_share_members')->where('user_id', $row['id'])->value('parent_user_id'), 1, 'Referral binding is unchanged');
+        check(\think\facade\Db::table('bot_wallets')->where('user_id', $row['id'])->value('available_balance'), '0', 'Real wallet does not receive demo funds');
+    }
+    $guest = \app\api\logic\LoginLogic::guestRegister();
+    check(is_array($guest), true, 'Guest registration succeeds: ' . ($guest === false ? \app\api\logic\LoginLogic::getError() : 'ok'));
+    $guestRow = \think\facade\Db::table('bot_users')->where('user_id', $guest['user_id'])->find();
+    check($guestRow['recharge_channel_group_id'], 1, 'Guest registration also assigns group one');
+    check($guestRow['from'], 2, 'Guest remains a demo user');
+    check((string)\think\facade\Db::table('bot_wallets')->where('user_id', $guestRow['id'])->value('available_balance'), '100000', 'Demo wallet behavior is unchanged');
+    check($GLOBALS['registration_token_groups'], [1, 1, 1, 1], 'Token creation sees the persisted default group');
+    foreach ([[], ['recharge_channel_group_id' => 0], ['recharge_channel_group_id' => null], ['recharge_channel_group_id' => ''], ['recharge_channel_group_id' => 7]] as $index => $extra) {
+        $model = \app\common\model\User::create(['user_id' => 'direct-' . $index, 'member_id' => 'direct-' . $index, 'account' => 'direct-' . $index] + $extra);
+        check((int)$model->recharge_channel_group_id, $extra['recharge_channel_group_id'] ?? null ? 7 : 1, 'Model insertion fallback preserves a valid assigned group');
+    }
+    \think\facade\Db::table('bot_users')->insert(['user_id' => 'legacy-zero', 'account' => 'legacy-zero', 'recharge_channel_group_id' => 0]);
+    foreach (['legacy-zero' => 0, '10000001' => 7] as $userId => $group) {
+        $user = \app\common\model\User::where('user_id', $userId)->find();
+        $user->first_name = 'profile edit'; $user->save();
+        check((int)\think\facade\Db::table('bot_users')->where('user_id', $userId)->value('recharge_channel_group_id'), $group, 'Updating an existing user never resets their group');
+    }
+    echo 'PASS ' . $checks . " registration/default-group assertions (isolated SQLite, token/file/login-log adapters)\n";
+}