doge 2 hari lalu
induk
melakukan
5daa6b9d39
2 mengubah file dengan 195 tambahan dan 1 penghapusan
  1. 15 1
      app/Http/Controllers/admin/User.php
  2. 180 0
      tests/Integration/AdminUserLoginLogTest.php

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

@@ -515,7 +515,21 @@ class User extends Controller
             $limit = (int)request()->input('limit', 15);
             $limit = (int)request()->input('limit', 15);
 
 
             $query = UserLogin::query()
             $query = UserLogin::query()
-                ->leftJoin('users', 'user_login.user_id', '=', 'users.user_id')
+                ->leftJoin('users', function ($join) {
+                    $join->on('user_login.user_id', '=', 'users.user_id')
+                        // 无用户的失败日志不能关联到同样缺少 ID 的客服账号。
+                        ->where('user_login.user_id', '<>', '');
+                })
+                ->where(function ($q) {
+                    $q->whereNull('users.from')->orWhere('users.from', '<>', '0');
+                })
+                // 失败日志可能没有用户 ID,仍需按输入账号排除客服来源(from=0)。
+                ->whereNotExists(function ($q) {
+                    $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');
+                })
                 ->select([
                 ->select([
                     'user_login.id',
                     'user_login.id',
                     'user_login.user_id',
                     'user_login.user_id',

+ 180 - 0
tests/Integration/AdminUserLoginLogTest.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace Tests\Integration;
+
+use App\Http\Controllers\admin\User as UserController;
+use Illuminate\Config\Repository;
+use Illuminate\Database\Capsule\Manager;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Foundation\Application;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Facade;
+use Illuminate\Translation\ArrayLoader;
+use Illuminate\Translation\Translator;
+use Illuminate\Validation\Factory;
+use PHPUnit\Framework\TestCase;
+
+class AdminUserLoginLogTest extends TestCase
+{
+    private Application $app;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        Facade::clearResolvedInstances();
+        Model::clearBootedModels();
+        $this->app = new Application(dirname(__DIR__, 2));
+        $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
+        $translator = new Translator(new ArrayLoader(), 'zh');
+        $this->app->instance('translator', $translator);
+        $this->app->instance('validator', new Factory($translator, $this->app));
+        $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
+        Request::macro('validate', function (array $rules) {
+            return app('validator')->make($this->all(), $rules)->validate();
+        });
+        Facade::setFacadeApplication($this->app);
+        $db = new Manager($this->app);
+        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->setAsGlobal();
+        $db->bootEloquent();
+        $this->app->instance('db', $db->getDatabaseManager());
+        $db->schema()->create('users', function (Blueprint $t) {
+            $t->id();
+            $t->string('user_id')->nullable();
+            $t->string('member_id');
+            $t->string('account')->nullable();
+            $t->string('from')->nullable();
+            $t->string('first_name')->default('');
+            $t->string('username')->default('');
+        });
+        $db->schema()->create('user_login', function (Blueprint $t) {
+            $t->id();
+            $t->string('user_id')->nullable();
+            foreach (['login_account', 'login_ip', 'login_domain', 'country', 'browser', 'os', 'platform', 'user_agent'] as $field) {
+                $t->string($field)->default('');
+            }
+            $t->integer('status');
+            $t->dateTime('logout_time')->nullable();
+            $t->integer('online_duration')->nullable();
+            $t->timestamps();
+        });
+        foreach ([
+            ['1001', 'alice', '1'],
+            ['1002', 'tg1002', '-1'],
+            ['1003', 'guest1003', '2'],
+            ['1004', 'kefu_member', '1'],
+            ['1005', 'legacy', null],
+            ['9001', 'kefu001', '0'],
+            ['9002', 'kefu002', '0'],
+            ['9003', 'support-alpha', '0'],
+            ['', 'desk-blank-id', '0'],
+            [null, 'desk-null-id', '0'],
+        ] as [$userId, $account, $from]) {
+            DB::table('users')->insert([
+                'user_id' => $userId, 'member_id' => $account, 'account' => $account,
+                'from' => $from, 'first_name' => $account,
+            ]);
+        }
+    }
+
+    protected function tearDown(): void
+    {
+        DB::disconnect();
+        Facade::clearResolvedInstances();
+        Model::clearBootedModels();
+        parent::tearDown();
+    }
+
+    public function test_excludes_customer_service_successes_and_failures_from_list_and_total(): void
+    {
+        $expected = [];
+        foreach (['1001', '1002', '1003', '1004', '1005'] as $userId) {
+            $expected[] = $this->log($userId, '', 1);
+        }
+        foreach (['9001', '9002', '9003'] as $userId) {
+            $this->log($userId, '', 1);
+            $this->log($userId, '', 0);
+        }
+
+        $data = $this->logs();
+        $this->assertSame(5, $data['total']);
+        $this->assertSame(array_reverse($expected), array_column($data['data'], 'id'));
+        $this->assertSame(11, DB::table('user_login')->count());
+    }
+
+    public function test_identifies_customer_service_failures_by_account_when_user_id_is_missing(): void
+    {
+        foreach (['', null, 'deleted-id'] as $userId) {
+            foreach (['kefu001', 'kefu002', 'support-alpha', 'desk-blank-id', 'desk-null-id'] as $account) {
+                $this->log($userId, $account, 0);
+            }
+        }
+        $expected = [
+            $this->log('', 'unknown-account', 0),
+            $this->log(null, 'another-unknown', 0),
+            $this->log('deleted-member', 'former-member', 0),
+            $this->log('', 'kefu_not_staff', 0),
+            $this->log('', '', 0),
+        ];
+
+        $data = $this->logs(['status' => 0]);
+        $this->assertSame(5, $data['total']);
+        $this->assertSame(array_reverse($expected), array_column($data['data'], 'id'));
+        foreach ($data['data'] as $row) {
+            $this->assertNull($row['account']);
+        }
+    }
+
+    public function test_account_and_user_filters_cannot_restore_customer_service_records(): void
+    {
+        $this->log('9001', 'kefu001', 1);
+        $this->log('', 'kefu001', 0);
+        $memberId = $this->log('1004', 'kefu_member', 1);
+
+        $data = $this->logs(['account' => 'kefu']);
+        $this->assertSame(1, $data['total']);
+        $this->assertSame([$memberId], array_column($data['data'], 'id'));
+        foreach ([['account' => 'kefu001'], ['user_id' => '9001'], ['member_id' => '9001'], ['first_name' => 'kefu001']] as $params) {
+            $this->assertSame(['total' => 0, 'data' => []], $this->logs($params));
+        }
+    }
+
+    public function test_member_filters_and_pagination_keep_normal_failed_logins(): void
+    {
+        $this->log('1001', 'alice', 1);
+        $older = $this->log('1001', 'alice', 0);
+        $newer = $this->log('1001', 'alice', 0, '2026-11-07 12:00:01');
+        $this->log('9001', 'kefu001', 0, '2026-11-07 12:00:02');
+        $this->log('1001', 'alice', 0, '2026-11-06 23:59:59');
+        $params = ['account' => 'ali', 'user_id' => '1001', 'status' => 0,
+            'start_time' => '2026-11-07', 'end_time' => '2026-11-07', 'limit' => 1];
+
+        $data = $this->logs($params);
+        $this->assertSame(2, $data['total']);
+        $this->assertSame([$newer], array_column($data['data'], 'id'));
+        $this->assertSame('alice', $data['data'][0]['account']);
+        $this->assertFalse($data['data'][0]['is_online']);
+        $data = $this->logs($params + ['page' => 2]);
+        $this->assertSame(2, $data['total']);
+        $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
+    {
+        return DB::table('user_login')->insertGetId([
+            'user_id' => $userId, 'login_account' => $account, 'status' => $status,
+            'created_at' => $at, 'updated_at' => $at,
+        ]);
+    }
+
+    private function logs(array $params = []): array
+    {
+        $this->app->instance('request', Request::create('/admin/user/loginLog', 'GET', $params));
+        $body = (new UserController())->loginLog()->getData(true);
+        $this->assertSame(0, $body['code'], $body['msg']);
+
+        return $body['data'];
+    }
+}