diff --git a/app/common/event/AdminLoginSuccess/NoticeEvent.php b/app/common/event/AdminLoginSuccess/NoticeEvent.php new file mode 100644 index 0000000..8800d17 --- /dev/null +++ b/app/common/event/AdminLoginSuccess/NoticeEvent.php @@ -0,0 +1,9 @@ +id], '登录提醒', "账号 {$admin->username} 于 ".date('Y-m-d H:i').' 登录成功', ['scene' => 'admin_login']); + } catch (\Throwable $e) { + // 通知失败不得影响登录主流程 + Log::error('admin login notice failed: ' . $e->getMessage()); + } + } +} diff --git a/extend/base/common/event/NoticeSended/LogEventBase.php b/extend/base/common/event/NoticeSended/LogEventBase.php new file mode 100644 index 0000000..da6d3fd --- /dev/null +++ b/extend/base/common/event/NoticeSended/LogEventBase.php @@ -0,0 +1,20 @@ + (int) $notice->id, + 'target_count' => count($targets), + 'channels' => array_values(array_unique(array_column($targets, 'channel'))), + ]); + } +} diff --git a/extend/think/UlthonAdminService.php b/extend/think/UlthonAdminService.php index e9a7734..8fb82b3 100644 --- a/extend/think/UlthonAdminService.php +++ b/extend/think/UlthonAdminService.php @@ -45,8 +45,10 @@ use app\common\command\tools\log\ToolsLogShow; use app\common\command\tools\log\ToolsLogStats; use app\common\command\Test; use app\common\event\AdminLoginSuccess\LogEvent; +use app\common\event\AdminLoginSuccess\NoticeEvent; use app\common\event\AdminLoginType\DemoEvent; use app\common\event\AdminMenuTab\DemoEvent as AdminMenuTabDemoEvent; +use app\common\event\NoticeSended\LogEvent as NoticeSendedLogEvent; use app\common\provider\ExceptionHandle; use app\common\provider\Request; use app\common\provider\View; @@ -68,6 +70,7 @@ class UlthonAdminService extends Service 'LogWrite' => [], 'AdminLoginSuccess' => [ LogEvent::class, + NoticeEvent::class, ], 'AdminLoginType' => [ DemoEvent::class, @@ -75,6 +78,9 @@ class UlthonAdminService extends Service 'AdminMenuTab' => [ AdminMenuTabDemoEvent::class, ], + 'NoticeSended' => [ + NoticeSendedLogEvent::class, + ], ]; $this->app->event->listenEvents($event_listen); diff --git a/tests/Feature/NoticeEventTest.php b/tests/Feature/NoticeEventTest.php new file mode 100644 index 0000000..1130746 --- /dev/null +++ b/tests/Feature/NoticeEventTest.php @@ -0,0 +1,163 @@ +createAdmin(); + + // 临时注入收集器监听器(容器内与框架注册的 NoticeSended LogEvent 并存,互不影响) + $captured = []; + Event::listen('NoticeSended', function ($payload) use (&$captured) { + $captured[] = $payload; + }); + + $noticeId = NoticeService::send([$adminId], '事件测试', '内容'); + + $this->assertCount(1, $captured, 'send 应触发一次 NoticeSended 事件'); + [$notice, $targets] = $captured[0]; + $this->assertSame($noticeId, (int) $notice->id, 'payload[0] 应为本次创建的 notice 模型'); + $this->assertIsArray($targets, 'payload[1] 应为投递行数组'); + $this->assertCount(1, $targets); + $this->assertSame($adminId, (int) $targets[0]['admin_id']); + $this->assertSame('inmail', $targets[0]['channel']); + } + + // ========================================================================= + // 2. 登录提醒开关开启:NoticeEvent 落 admin_login 通知 + // ========================================================================= + + public function test_login_notice_sent_when_scene_switch_on(): void + { + $adminId = $this->createAdmin(['username' => 'lgn_' . uniqid()]); + $admin = SystemAdmin::find($adminId); + $this->assertNotNull($admin, 'SystemAdmin 模型应能取到监听器入参形态'); + + $this->setLoginSceneSwitch('1'); + + (new NoticeEvent())->handle($admin); + + $row = Db::name('system_notice') + ->where('scene', 'admin_login') + ->where('send_admin_id', 0) + ->find(); + $this->assertNotNull($row, '开关开启时应产生 admin_login 通知主体'); + $this->assertSame('登录提醒', $row['title']); + // 文案含登录时间(Y-m-d H:i),跨分钟竞态下按正则断言而非逐字相等 + $pattern = '/^账号 ' . preg_quote((string) $admin->username, '/') . ' 于 \d{4}-\d{2}-\d{2} \d{2}:\d{2} 登录成功$/u'; + $this->assertMatchesRegularExpression($pattern, (string) $row['content']); + + // inmail 行直接成功终态(无 email 地址的管理员 email 渠道被跳过) + $this->assertDatabaseHas('system_notice_target', [ + 'notice_id' => $row['id'], + 'admin_id' => $adminId, + 'channel' => 'inmail', + 'status' => 2, + ]); + $this->assertSame( + 1, + Db::name('system_notice_target')->where('notice_id', $row['id'])->count(), + '无 email 管理员应仅展开 inmail 一行' + ); + } + + // ========================================================================= + // 3. 登录提醒开关默认关闭 + // ========================================================================= + + public function test_login_notice_skipped_when_scene_switch_absent(): void + { + $adminId = $this->createAdmin(['username' => 'lgn_' . uniqid()]); + $admin = SystemAdmin::find($adminId); + + // 无配置行 = 默认关;清理可能被前序用例缓存的非 '0' 值 + $this->clearLoginSceneSwitch(); + + (new NoticeEvent())->handle($admin); + + $this->assertSame( + 0, + Db::name('system_notice')->where('scene', 'admin_login')->where('send_admin_id', 0)->count(), + '开关缺省(无配置行)时不应产生 admin_login 通知' + ); + } + + // ========================================================================= + // fixture 工厂(事务内运行,tearDown 自动回滚) + // ========================================================================= + + /** + * 造启用管理员. + */ + private function createAdmin(array $overrides = []): int + { + $data = array_merge([ + 'username' => 'evt_' . uniqid(), + 'password' => 'not-a-real-password', + 'nickname' => '事件测试用户', + 'email' => '', + 'status' => 1, + 'create_time' => time(), + 'update_time' => time(), + 'delete_time' => 0, + ], $overrides); + + return (int) Db::name('system_admin')->insertGetId($data); + } + + /** + * 写 notify 组登录提醒开关配置行并清缓存(缓存不受事务回滚影响). + */ + private function setLoginSceneSwitch(string $value): void + { + Db::name('system_config')->insert([ + 'name' => 'notify_login_scene_enable', + 'group' => 'notify', + 'value' => $value, + 'remark' => 'NoticeEventTest fixture', + 'sort' => 100, + 'create_time' => time(), + 'update_time' => time(), + ]); + Cache::tag('sysconfig')->clear(); + } + + /** + * 删除 notify 组登录提醒开关配置行并清缓存. + */ + private function clearLoginSceneSwitch(): void + { + Db::name('system_config') + ->where('group', 'notify') + ->where('name', 'notify_login_scene_enable') + ->delete(); + Cache::tag('sysconfig')->clear(); + } +}