diff --git a/extend/base/admin/controller/NoticeBase.php b/extend/base/admin/controller/NoticeBase.php index ab6929f..2d63dfb 100644 --- a/extend/base/admin/controller/NoticeBase.php +++ b/extend/base/admin/controller/NoticeBase.php @@ -61,12 +61,15 @@ class NoticeBase extends AdminController ->withJoin('notice', 'LEFT') ->where('system_notice_target.admin_id', $adminId) ->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL) + // 软删通知不进个人域(管理页删除后此处消失) + ->where('system_notice.delete_time', 0) ->where($where) ->count(); $list = $this->model ->withJoin('notice', 'LEFT') ->where('system_notice_target.admin_id', $adminId) ->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL) + ->where('system_notice.delete_time', 0) ->where($where) ->page($page, $limit) ->order($this->sort) @@ -95,6 +98,8 @@ class NoticeBase extends AdminController ->withJoin('notice', 'LEFT') ->where('system_notice_target.id', (int) $id) ->where('system_notice_target.admin_id', $adminId) + // 软删通知不进个人域(管理页删除后详情不可打开) + ->where('system_notice.delete_time', 0) ->find(); if (empty($row)) { $this->error('消息不存在或无权查看'); diff --git a/extend/base/common/service/NoticeServiceBase.php b/extend/base/common/service/NoticeServiceBase.php index 4a05e00..b4172ad 100644 --- a/extend/base/common/service/NoticeServiceBase.php +++ b/extend/base/common/service/NoticeServiceBase.php @@ -109,33 +109,39 @@ class NoticeServiceBase } if ($adminIds === 'all') { - $adminIds = Db::name('system_admin')->where('status', 1)->column('id'); + // delete_time=0 过滤软删管理员,与 sceneOverview 的 SystemAdmin 模型口径对齐 + $adminIds = Db::name('system_admin')->where('status', 1)->where('delete_time', 0)->column('id'); } else { $adminIds = array_values(array_unique(array_map('intval', (array) $adminIds))); } $now = time(); - $notice = SystemNotice::create([ - 'scene' => $scene, - 'title' => $title, - 'content' => $content, - 'biz_id' => (string) ($options['biz_id'] ?? ''), - 'send_admin_id' => (int) ($options['send_admin_id'] ?? 0), - ]); - $noticeId = (int) $notice->id; + // 主体创建与投递展开同事务:insertAll 中途异常时回滚,不残留零投递主体 + [$notice, $targets] = Db::transaction(function () use ($adminIds, $channels, $content, $now, $options, $registered, $scene, $title) { + $notice = SystemNotice::create([ + 'scene' => $scene, + 'title' => $title, + 'content' => $content, + 'biz_id' => (string) ($options['biz_id'] ?? ''), + 'send_admin_id' => (int) ($options['send_admin_id'] ?? 0), + ]); + $noticeId = (int) $notice->id; - $targets = []; - if ($adminIds !== []) { - $targets = static::buildTargets($noticeId, $adminIds, $scene, $channels, $registered, $now); - if ($targets !== []) { - Db::name('system_notice_target')->insertAll($targets); + $targets = []; + if ($adminIds !== []) { + $targets = static::buildTargets($noticeId, $adminIds, $scene, $channels, $registered, $now); + if ($targets !== []) { + Db::name('system_notice_target')->insertAll($targets); + } } - } + + return [$notice, $targets]; + }); Event::trigger('NoticeSended', [$notice, $targets]); - return $noticeId; + return (int) $notice->id; } /** @@ -240,14 +246,18 @@ class NoticeServiceBase /** * 未读站内信数(channel=inmail AND read_time=0 AND status=2,走复合索引). + * JOIN 过滤软删通知:与个人域 mine/read 口径一致,管理页删除后未读数同步下降. */ public static function unreadCount(int $adminId): int { return Db::name('system_notice_target') - ->where('admin_id', $adminId) - ->where('channel', self::CHANNEL_INMAIL) - ->where('read_time', 0) - ->where('status', SystemNoticeTargetBase::STATUS_SUCCESS) + ->alias('target') + ->join('system_notice notice', 'notice.id = target.notice_id') + ->where('target.admin_id', $adminId) + ->where('target.channel', self::CHANNEL_INMAIL) + ->where('target.read_time', 0) + ->where('target.status', SystemNoticeTargetBase::STATUS_SUCCESS) + ->where('notice.delete_time', 0) ->count(); } diff --git a/tests/Feature/NoticeServiceTest.php b/tests/Feature/NoticeServiceTest.php index de1aa4f..b52a8fe 100644 --- a/tests/Feature/NoticeServiceTest.php +++ b/tests/Feature/NoticeServiceTest.php @@ -12,7 +12,7 @@ use think\facade\Db; /** * NoticeService 核心行为集成测试. * - * 覆盖 11 个契约点(与计划任务 3 逐项对齐,每项一个 test 方法): + * 覆盖 13 个契约点(与计划任务 3 逐项对齐,每项一个 test 方法): * 1. send 主体 + target 展开 * 2. 'all' 广播(仅 status=1,含无 email 管理员) * 3. channels 缺省按 scene.channels 展开 @@ -24,6 +24,8 @@ use think\facade\Db; * 9. inmail 全局总闸关闭跳过(键不存在视为开) * 10. unreadCount/markRead/markAllRead 数字正确 * 11. markRead 越权防护 + * 12. 'all' 广播过滤软删管理员(F2-C10) + * 13. unreadCount 不受软删通知影响(F2-C6 个人域口径) * * fixture 均为事务内自造数据,tearDown 自动回滚;缓存清理显式执行 * (Cache 不受事务回滚影响,见 setInmailSwitch/clearInmailSwitch)。 @@ -335,6 +337,44 @@ class NoticeServiceTest extends TestCase $this->assertSame(1, NoticeService::unreadCount($adminB), 'markAllRead 不得触碰他人行'); } + // ========================================================================= + // 12. 'all' 广播过滤软删管理员(F2-C10) + // ========================================================================= + + public function test_send_all_excludes_soft_deleted_admins(): void + { + $aliveAdmin = $this->createAdmin(); + $softDeletedAdmin = $this->createAdmin(['delete_time' => time()]); + + $noticeId = NoticeService::send('all', '软删过滤广播', '内容'); + + $this->assertGreaterThan(0, Db::name('system_notice_target') + ->where('notice_id', $noticeId)->where('admin_id', $aliveAdmin)->count(), '在册管理员应收到广播'); + $this->assertSame( + 0, + Db::name('system_notice_target')->where('notice_id', $noticeId)->where('admin_id', $softDeletedAdmin)->count(), + '软删管理员不应收到广播' + ); + } + + // ========================================================================= + // 13. unreadCount 不受软删通知影响(F2-C6 个人域口径) + // ========================================================================= + + public function test_unread_count_ignores_soft_deleted_notices(): void + { + $adminId = $this->createAdmin(); + + NoticeService::send([$adminId], '正常通知', '内容'); + $softDeletedNoticeId = NoticeService::send([$adminId], '将被删除的通知', '内容'); + $this->assertSame(2, NoticeService::unreadCount($adminId)); + + // 管理页删除 = 软删 notice 主体(delete_time 置时间戳) + Db::name('system_notice')->where('id', $softDeletedNoticeId)->update(['delete_time' => time()]); + + $this->assertSame(1, NoticeService::unreadCount($adminId), '软删通知不应计入未读数'); + } + // ========================================================================= // fixture 工厂(事务内运行,tearDown 自动回滚) // =========================================================================