Просмотр исходного кода

Telegram 用户注册自动生成 tg 开头唯一 account,并新增补数据命令

- User::generateTgAccount():tg + 8 位随机数字,查重保证唯一
- PublicService::index:account 为空时自动生成
- users:fill-tg-account:给 from=-1 且 account 为空的用户补 account(支持 --dry-run)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
doge 8 часов назад
Родитель
Сommit
e0381d92a4

+ 39 - 0
app/Console/Commands/FillTelegramAccounts.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\User;
+use Illuminate\Console\Command;
+
+class FillTelegramAccounts extends Command
+{
+    protected $signature = 'users:fill-tg-account {--dry-run : 只打印,不写库}';
+    protected $description = '给 from=-1(Telegram 进来)且没有 account 的用户补 account:tg + 随机数字,唯一';
+
+    public function handle(): int
+    {
+        $dry = (bool)$this->option('dry-run');
+        $count = 0;
+        $query = User::query()->where('from', -1)
+            ->where(fn ($q) => $q->whereNull('account')->orWhere('account', ''));
+
+        // chunkById:更新后行会脱离筛选条件,普通 chunk 会跳页
+        $query->select('id', 'member_id', 'account')->chunkById(500, function ($users) use ($dry, &$count) {
+            foreach ($users as $user) {
+                $account = User::generateTgAccount();
+                if (!$dry) {
+                    // 只在 account 仍为空时写入,避免覆盖并发产生的值
+                    $updated = User::where('id', $user->id)
+                        ->where(fn ($q) => $q->whereNull('account')->orWhere('account', ''))
+                        ->update(['account' => $account]);
+                    if (!$updated) continue;
+                }
+                $count++;
+                $this->line("{$user->id} ({$user->member_id}): {$account}");
+            }
+        });
+
+        $this->info(($dry ? '[dry-run] 将更新 ' : '已更新 ') . "{$count} 个用户");
+        return self::SUCCESS;
+    }
+}

+ 14 - 0
app/Models/User.php

@@ -140,4 +140,18 @@ class User extends BaseModel
         }
         return $user_code;
     }
+
+    /**
+     * Telegram 进来的用户没有 account,生成 tg + 随机数字,保证不与 users.account 重复
+     */
+    public static function generateTgAccount(int $num_len = 8): string
+    {
+        do {
+            $account = 'tg';
+            for ($i = 0; $i < $num_len; $i++) {
+                $account .= random_int(0, 9);
+            }
+        } while (User::where('account', $account)->exists());
+        return $account;
+    }
 }

+ 3 - 0
app/Services/PublicService.php

@@ -56,6 +56,9 @@ class PublicService extends BaseService
             $user->from = -1;
             $user->user_code = User::generateUserCode();
         }
+        if (empty($user->account)) {
+            $user->account = User::generateTgAccount();
+        }
         $user->member_id = $chatId;
         $user->first_name = $firstName;
         if ($username) $user->username = $username;

+ 81 - 0
tests/Integration/TelegramAccountTest.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Tests\Integration;
+
+use App\Console\Commands\FillTelegramAccounts;
+use App\Models\User;
+use Illuminate\Config\Repository;
+use Illuminate\Database\Capsule\Manager;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Facade;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Output\BufferedOutput;
+
+/** Telegram users (from=-1) get a unique tg+digits account; backfill only touches empty ones. */
+class TelegramAccountTest extends TestCase
+{
+    private Application $app;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        Facade::clearResolvedInstances();
+        $this->app = $app = new Application(dirname(__DIR__, 2));
+        $app->instance('config', new Repository(['app' => ['timezone' => 'Asia/Shanghai']]));
+        Facade::setFacadeApplication($app);
+        $db = new Manager($app);
+        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->setAsGlobal();
+        $db->bootEloquent();
+        $app->instance('db', $db->getDatabaseManager());
+        $db->schema()->create('users', function (Blueprint $t) {
+            $t->id(); $t->string('member_id'); $t->string('account')->nullable(); $t->integer('from')->default(0); $t->timestamps();
+        });
+        DB::table('users')->insert([
+            ['member_id' => 't1', 'account' => null, 'from' => -1],
+            ['member_id' => 't2', 'account' => '', 'from' => -1],
+            ['member_id' => 't3', 'account' => 'keepme', 'from' => -1],
+            ['member_id' => 'w1', 'account' => null, 'from' => 1],
+        ]);
+    }
+
+    protected function tearDown(): void
+    {
+        DB::disconnect();
+        Facade::clearResolvedInstances();
+        parent::tearDown();
+    }
+
+    public function test_generated_account_format_and_skips_existing(): void
+    {
+        $account = User::generateTgAccount();
+        $this->assertMatchesRegularExpression('/^tg\d{8}$/', $account);
+        // 只有 1 位数字时空间只有 10 个,占掉 9 个后必须拿到剩下那个
+        foreach (range(0, 8) as $d) DB::table('users')->insert(['member_id' => "x$d", 'account' => "tg$d"]);
+        $this->assertSame('tg9', User::generateTgAccount(1));
+    }
+
+    public function test_backfill_only_fills_empty_telegram_accounts(): void
+    {
+        $this->runCommand(['--dry-run' => true]);
+        $this->assertSame(2, DB::table('users')->where('from', -1)->where(fn ($q) => $q->whereNull('account')->orWhere('account', ''))->count());
+
+        $this->runCommand([]);
+        $rows = DB::table('users')->pluck('account', 'member_id');
+        $this->assertMatchesRegularExpression('/^tg\d{8}$/', $rows['t1']);
+        $this->assertMatchesRegularExpression('/^tg\d{8}$/', $rows['t2']);
+        $this->assertNotSame($rows['t1'], $rows['t2']);
+        $this->assertSame('keepme', $rows['t3']);
+        $this->assertNull($rows['w1']);
+    }
+
+    private function runCommand(array $input): void
+    {
+        $command = new FillTelegramAccounts();
+        $command->setLaravel($this->app);
+        $command->run(new ArrayInput($input), new BufferedOutput());
+    }
+}