feat(notice): NoticeSended 事件与登录提醒内置监听

This commit is contained in:
augushong
2026-10-12 12:09:01 +00:00
parent 02f77f4eca
commit 7aba2d02fe
6 changed files with 233 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\event\AdminLoginSuccess;
use base\common\event\AdminLoginSuccess\NoticeEventBase;
class NoticeEvent extends NoticeEventBase
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace app\common\event\NoticeSended;
use base\common\event\NoticeSended\LogEventBase;
class LogEvent extends LogEventBase
{
}

View File

@@ -0,0 +1,26 @@
<?php
namespace base\common\event\AdminLoginSuccess;
use app\admin\model\SystemAdmin;
use app\common\service\NoticeService;
use think\facade\Log;
class NoticeEventBase
{
public function handle(SystemAdmin $admin)
{
try {
// 登录提醒场景默认关闭:无配置行(null 强转 0)不发,DB value 为字符串须强转 int
if ((int) sysconfig('notify', 'notify_login_scene_enable') === 0) {
return;
}
// 不传 channels=按场景注册渠道展开(admin_login=[inmail,email]),再经各管理员订阅矩阵过滤
NoticeService::send([$admin->id], '登录提醒', "账号 {$admin->username} 于 ".date('Y-m-d H:i').' 登录成功', ['scene' => 'admin_login']);
} catch (\Throwable $e) {
// 通知失败不得影响登录主流程
Log::error('admin login notice failed: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace base\common\event\NoticeSended;
use think\facade\Log;
class LogEventBase
{
public function handle(array $payload)
{
// 事件监听处理:payload = [$notice(模型), $targets(投递行数组)](NoticeServiceBase::send trigger 形态)
[$notice, $targets] = $payload;
Log::info('notice sended', [
'notice_id' => (int) $notice->id,
'target_count' => count($targets),
'channels' => array_values(array_unique(array_column($targets, 'channel'))),
]);
}
}

View File

@@ -45,8 +45,10 @@ use app\common\command\tools\log\ToolsLogShow;
use app\common\command\tools\log\ToolsLogStats; use app\common\command\tools\log\ToolsLogStats;
use app\common\command\Test; use app\common\command\Test;
use app\common\event\AdminLoginSuccess\LogEvent; use app\common\event\AdminLoginSuccess\LogEvent;
use app\common\event\AdminLoginSuccess\NoticeEvent;
use app\common\event\AdminLoginType\DemoEvent; use app\common\event\AdminLoginType\DemoEvent;
use app\common\event\AdminMenuTab\DemoEvent as AdminMenuTabDemoEvent; 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\ExceptionHandle;
use app\common\provider\Request; use app\common\provider\Request;
use app\common\provider\View; use app\common\provider\View;
@@ -68,6 +70,7 @@ class UlthonAdminService extends Service
'LogWrite' => [], 'LogWrite' => [],
'AdminLoginSuccess' => [ 'AdminLoginSuccess' => [
LogEvent::class, LogEvent::class,
NoticeEvent::class,
], ],
'AdminLoginType' => [ 'AdminLoginType' => [
DemoEvent::class, DemoEvent::class,
@@ -75,6 +78,9 @@ class UlthonAdminService extends Service
'AdminMenuTab' => [ 'AdminMenuTab' => [
AdminMenuTabDemoEvent::class, AdminMenuTabDemoEvent::class,
], ],
'NoticeSended' => [
NoticeSendedLogEvent::class,
],
]; ];
$this->app->event->listenEvents($event_listen); $this->app->event->listenEvents($event_listen);

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace tests\Feature;
use app\admin\model\SystemAdmin;
use app\common\event\AdminLoginSuccess\NoticeEvent;
use app\common\service\NoticeService;
use app\common\test\TestCase;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Event;
/**
* 事件层集成测试.
*
* 覆盖:
* 1. send 触发 NoticeSended 事件,payload = [$notice(模型), $targets(投递行数组)]
* 2. 登录提醒开关开启时 NoticeEvent 落 admin_login 通知(inmail 行直接成功终态)
* 3. 开关默认关闭(无配置行)时不发
*
* fixture 均为事务内自造数据,tearDown 自动回滚;sysconfig 缓存显式清理
* (Cache 不受事务回滚影响,非 '0' 值会缓存 3600 秒)。
*/
class NoticeEventTest extends TestCase
{
// =========================================================================
// 1. NoticeSended 事件触发
// =========================================================================
public function test_send_triggers_notice_sended_event_with_notice_and_targets(): void
{
$adminId = $this->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();
}
}