doge пре 2 дана
родитељ
комит
b5ca78d85e

+ 15 - 5
app/Http/Controllers/admin/User.php

@@ -514,9 +514,19 @@ class User extends Controller
             $page = (int)request()->input('page', 1);
             $limit = (int)request()->input('limit', 15);
 
-            $query = UserLogin::query()
-                ->leftJoin('users', function ($join) {
-                    $join->on('user_login.user_id', '=', 'users.user_id')
+            $query = UserLogin::query();
+            $grammar = $query->getQuery()->getGrammar();
+            // 历史表字段可能混用 general_ci / unicode_ci;显式统一比较规则,保留账号大小写不敏感语义。
+            $collation = in_array($query->getConnection()->getDriverName(), ['mysql', 'mariadb'], true)
+                ? ' COLLATE utf8mb4_unicode_ci' : '';
+            $userIdMatch = $grammar->wrap('user_login.user_id') . $collation
+                . ' = ' . $grammar->wrap('users.user_id') . $collation;
+            $accountMatch = $grammar->wrap('customer_service_users.account') . $collation
+                . ' = ' . $grammar->wrap('user_login.login_account') . $collation;
+
+            $query
+                ->leftJoin('users', function ($join) use ($userIdMatch) {
+                    $join->whereRaw($userIdMatch)
                         // 无用户的失败日志不能关联到同样缺少 ID 的客服账号。
                         ->where('user_login.user_id', '<>', '');
                 })
@@ -524,11 +534,11 @@ class User extends Controller
                     $q->whereNull('users.from')->orWhere('users.from', '<>', '0');
                 })
                 // 失败日志可能没有用户 ID,仍需按输入账号排除客服来源(from=0)。
-                ->whereNotExists(function ($q) {
+                ->whereNotExists(function ($q) use ($accountMatch) {
                     $q->selectRaw('1')->from('users as customer_service_users')
                         ->where('customer_service_users.from', '0')
                         ->where('user_login.login_account', '<>', '')
-                        ->whereColumn('customer_service_users.account', 'user_login.login_account');
+                        ->whereRaw($accountMatch);
                 })
                 ->select([
                     'user_login.id',

+ 62 - 0
tests/Integration/AdminUserLoginLogMysqlTest.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Tests\Integration;
+
+use Illuminate\Support\Facades\DB;
+use PDO;
+
+/** Runs the login log scenarios on a disposable MySQL server without loading .env. */
+class AdminUserLoginLogMysqlTest extends AdminUserLoginLogTest
+{
+    private ?PDO $control = null;
+    private string $database;
+
+    protected function setUp(): void
+    {
+        $socket = getenv('LOGIN_LOG_TEST_MYSQL_SOCKET');
+        if (!$socket) {
+            $this->markTestSkipped('Set LOGIN_LOG_TEST_MYSQL_SOCKET to a disposable MySQL server.');
+        }
+        $this->control = new PDO('mysql:unix_socket=' . $socket, 'root', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
+        $this->database = 'bot28_login_log_test_' . bin2hex(random_bytes(8));
+        $this->control->exec('CREATE DATABASE `' . $this->database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci');
+        parent::setUp();
+
+        // Reproduce the deployed schema: accounts and user IDs can have different collations.
+        DB::statement('ALTER TABLE bot_users
+            MODIFY user_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
+            MODIFY account VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL');
+    }
+
+    protected function tearDown(): void
+    {
+        if ($this->control !== null) {
+            parent::tearDown();
+            $this->control->exec('DROP DATABASE `' . $this->database . '`');
+            $this->control = null;
+        }
+    }
+
+    protected function connectionConfig(): array
+    {
+        return [
+            'driver' => 'mysql', 'unix_socket' => getenv('LOGIN_LOG_TEST_MYSQL_SOCKET'),
+            'database' => $this->database, 'username' => 'root', 'password' => '',
+            'prefix' => 'bot_', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_general_ci',
+        ];
+    }
+
+    public function test_account_matching_keeps_case_insensitive_and_unicode_semantics(): void
+    {
+        DB::table('users')->insert([
+            'user_id' => '9004', 'member_id' => 'support-unicode', 'account' => '客服É', 'from' => '0',
+        ]);
+        $this->log('', 'KEFU001', 0);
+        $this->log('', '客服é', 0);
+        $expected = [$this->log('', '其他账号', 0), $this->log('1001', 'ALICE', 0)];
+
+        $data = $this->logs(['status' => 0]);
+        $this->assertSame(2, $data['total']);
+        $this->assertSame(array_reverse($expected), array_column($data['data'], 'id'));
+    }
+}

+ 8 - 3
tests/Integration/AdminUserLoginLogTest.php

@@ -36,7 +36,7 @@ class AdminUserLoginLogTest extends TestCase
         });
         Facade::setFacadeApplication($this->app);
         $db = new Manager($this->app);
-        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->addConnection($this->connectionConfig());
         $db->setAsGlobal();
         $db->bootEloquent();
         $this->app->instance('db', $db->getDatabaseManager());
@@ -161,7 +161,12 @@ class AdminUserLoginLogTest extends TestCase
         $this->assertSame([$older], array_column($data['data'], 'id'));
     }
 
-    private function log(?string $userId, string $account, int $status, string $at = '2026-11-07 12:00:00'): int
+    protected function connectionConfig(): array
+    {
+        return ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_'];
+    }
+
+    protected function log(?string $userId, string $account, int $status, string $at = '2026-11-07 12:00:00'): int
     {
         return DB::table('user_login')->insertGetId([
             'user_id' => $userId, 'login_account' => $account, 'status' => $status,
@@ -169,7 +174,7 @@ class AdminUserLoginLogTest extends TestCase
         ]);
     }
 
-    private function logs(array $params = []): array
+    protected function logs(array $params = []): array
     {
         $this->app->instance('request', Request::create('/admin/user/loginLog', 'GET', $params));
         $body = (new UserController())->loginLog()->getData(true);