createAdmin(); $noticeId = NoticeService::send([$adminId], '测试标题', '测试内容'); $this->assertGreaterThan(0, $noticeId); $this->assertDatabaseHas('system_notice', [ 'id' => $noticeId, 'scene' => 'common', 'title' => '测试标题', 'content' => '测试内容', 'biz_id' => '', 'send_admin_id' => 0, ]); $target = Db::name('system_notice_target') ->where('notice_id', $noticeId) ->where('admin_id', $adminId) ->find(); $this->assertNotNull($target, 'target 行应存在'); $this->assertSame('inmail', $target['channel']); $this->assertSame(2, (int) $target['status'], 'inmail 投递即落库,直接成功'); $this->assertSame('', $target['target_addr'], 'inmail 无需外部地址'); $this->assertGreaterThan(0, (int) $target['send_time']); $this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId)->count()); } // ========================================================================= // 2. 'all' 广播 // ========================================================================= public function test_send_all_broadcasts_to_active_admins_only(): void { // 记录广播前的启用管理员基数(测试库含初始数据,不能假设从 0 开始) $activeBefore = Db::name('system_admin')->where('status', 1)->count(); $withMailBefore = Db::name('system_admin')->where('status', 1)->where('email', '<>', '')->count(); $adminWithMail = $this->createAdmin(['email' => 'all_a@test.com']); $adminNoMail = $this->createAdmin(); $adminDisabled = $this->createAdmin(['status' => 0]); $noticeId = NoticeService::send('all', '广播标题', '广播内容'); $this->assertGreaterThan(0, $noticeId); // 总展开数 = 全体启用管理员各 1 行 inmail + 有 email 的管理员各 1 行 email $expectedTotal = ($activeBefore + 2) + ($withMailBefore + 1); $actualTotal = Db::name('system_notice_target')->where('notice_id', $noticeId)->count(); $this->assertSame($expectedTotal, $actualTotal, "target 总数应为 {$expectedTotal},实际 {$actualTotal}"); // 禁用管理员不在广播内 $this->assertSame( 0, Db::name('system_notice_target')->where('notice_id', $noticeId)->where('admin_id', $adminDisabled)->count(), 'status=0 的管理员不应收到广播' ); // 无 email 管理员仅 inmail 一行 $this->assertSame(1, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->count()); $this->assertSame(1, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->where('channel', 'inmail')->count()); // 有 email 管理员 inmail + email 两行,email 行待发 $this->assertSame(2, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->count()); $emailRow = Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->where('channel', 'email')->find(); $this->assertNotNull($emailRow); $this->assertSame(0, (int) $emailRow['status'], 'email 渠道应置待发由消费者投递'); $this->assertSame('all_a@test.com', $emailRow['target_addr']); } // ========================================================================= // 3. channels 缺省按 scene.channels 展开 // ========================================================================= public function test_send_without_channels_expands_scene_channels(): void { $adminNoMail = $this->createAdmin(); $adminWithMail = $this->createAdmin(['email' => 'scene@test.com']); // admin_login 注册渠道 [inmail,email],不传 channels 按场景展开 $noticeId = NoticeService::send([$adminNoMail, $adminWithMail], '登录提醒', '内容', ['scene' => 'admin_login']); // 无 email 管理员:email 被跳过,仅 inmail 一行 $this->assertSame(1, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->count()); $this->assertSame(1, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->where('channel', 'inmail')->count()); // 有 email 管理员:两行,email 行地址正确 $this->assertSame(2, Db::name('system_notice_target') ->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->count()); $this->assertDatabaseHas('system_notice_target', [ 'notice_id' => $noticeId, 'admin_id' => $adminWithMail, 'channel' => 'email', 'target_addr' => 'scene@test.com', 'status' => 0, ]); } // ========================================================================= // 4. 订阅过滤 // ========================================================================= public function test_unsubscribe_row_filters_channel_delivery(): void { $adminId = $this->createAdmin(); // 默认订阅:第一次 send 产生 inmail 行 $noticeId1 = NoticeService::send([$adminId], '第一次', '内容'); $this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId1)->count()); // 显式退订 common 场景的 inmail $this->insertSubscribe($adminId, 'common', 'inmail', 0); $noticeId2 = NoticeService::send([$adminId], '第二次', '内容'); // notice 主体仍存在,但零 target(email 渠道因无地址跳过,inmail 被退订过滤) $this->assertDatabaseHas('system_notice', ['id' => $noticeId2]); $this->assertSame( 0, Db::name('system_notice_target')->where('notice_id', $noticeId2)->count(), '全部退订 common 后不应产生任何 target 行' ); } // ========================================================================= // 5. locked 强投递 // ========================================================================= public function test_locked_scene_forces_delivery_despite_unsubscribe(): void { $adminId = $this->createAdmin(); $this->insertSubscribe($adminId, 'system', 'inmail', 0); $noticeId = NoticeService::send([$adminId], '系统公告', '内容', ['scene' => 'system']); $target = Db::name('system_notice_target') ->where('notice_id', $noticeId) ->where('admin_id', $adminId) ->find(); $this->assertNotNull($target, 'locked 场景即使退订也强制投递'); $this->assertSame('inmail', $target['channel']); $this->assertSame(2, (int) $target['status']); } // ========================================================================= // 6. locked 场景请求未注册渠道被 ∩ 掉 // ========================================================================= public function test_locked_scene_intersects_requested_channels(): void { $adminId = $this->createAdmin(['email' => 'locked@test.com']); // system 场景注册渠道仅 [inmail];locked 不豁免 ∩,请求 email 应被裁掉 → 零 target $noticeId = NoticeService::send([$adminId], '系统公告', '内容', ['scene' => 'system', 'channels' => ['email']]); $this->assertDatabaseHas('system_notice', ['id' => $noticeId]); $this->assertSame( 0, Db::name('system_notice_target')->where('notice_id', $noticeId)->count(), 'locked 场景请求未注册渠道应被 ∩ 掉,零 target' ); } // ========================================================================= // 7. 未注册 scene 放行 // ========================================================================= public function test_unregistered_scene_passes_with_default_inmail(): void { $adminId = $this->createAdmin(); // 未注册场景:Log::warning 后放行,缺省渠道 [inmail]。 // warning 断言取舍:框架 Log::listen 实为 LogWrite 事件监听, // 仅在日志通道 save 时触发(请求收尾),CLI 测试进程不触发, // 监听式断言不可行 —— 此处断言行为(放行 + 缺省 inmail 展开)。 $noticeId = NoticeService::send([$adminId], '业务自定义', '内容', ['scene' => 'totally_new_scene']); $this->assertDatabaseHas('system_notice', ['id' => $noticeId, 'scene' => 'totally_new_scene']); $this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId)->count()); $this->assertDatabaseHas('system_notice_target', [ 'notice_id' => $noticeId, 'channel' => 'inmail', 'status' => 2, ]); } // ========================================================================= // 8. 无 email 跳过 email 渠道 // ========================================================================= public function test_missing_email_skips_email_channel(): void { $adminId = $this->createAdmin(); // 显式请求 email 渠道但管理员无 email 值:跳过该渠道,不写行 $noticeId = NoticeService::send([$adminId], '标题', '内容', ['scene' => 'common', 'channels' => ['email']]); $this->assertDatabaseHas('system_notice', ['id' => $noticeId]); $this->assertSame( 0, Db::name('system_notice_target')->where('notice_id', $noticeId)->count(), '无 email 值的管理员不应产生 email target 行' ); } // ========================================================================= // 9. inmail 全局总闸 // ========================================================================= public function test_inmail_global_switch_off_skips_inmail(): void { $adminId = $this->createAdmin(); // 插入关闭配置行(value='0')并清 sysconfig 缓存(缓存不受事务回滚影响) $this->setInmailSwitch('0'); $noticeId1 = NoticeService::send([$adminId], '总闸关闭', '内容'); $this->assertDatabaseHas('system_notice', ['id' => $noticeId1]); $this->assertSame( 0, Db::name('system_notice_target')->where('notice_id', $noticeId1)->count(), 'notify_inmail_enable=0 时 inmail 渠道应全局跳过' ); // 移除配置行:键不存在视为开(全局总闸默认放行) $this->clearInmailSwitch(); $noticeId2 = NoticeService::send([$adminId], '总闸恢复', '内容'); $this->assertDatabaseHas('system_notice_target', [ 'notice_id' => $noticeId2, 'channel' => 'inmail', 'status' => 2, ]); } // ========================================================================= // 10. unreadCount / markRead / markAllRead // ========================================================================= public function test_unread_count_and_read_actions(): void { $adminId = $this->createAdmin(); NoticeService::send([$adminId], '第一条', '内容'); NoticeService::send([$adminId], '第二条', '内容'); NoticeService::send([$adminId], '第三条', '内容'); $this->assertSame(3, NoticeService::unreadCount($adminId)); $targetIds = Db::name('system_notice_target') ->where('admin_id', $adminId) ->where('read_time', 0) ->column('id'); // 标记两条已读 $marked = NoticeService::markRead([$targetIds[0], $targetIds[1]], $adminId); $this->assertSame(2, $marked); $this->assertSame(1, NoticeService::unreadCount($adminId)); $this->assertGreaterThan(0, (int) Db::name('system_notice_target')->where('id', $targetIds[0])->value('read_time')); // 重复标记不再增加 $this->assertSame(0, NoticeService::markRead([$targetIds[0]], $adminId), '已读行不应被重复标记'); // 全部已读 $this->assertSame(1, NoticeService::markAllRead($adminId)); $this->assertSame(0, NoticeService::unreadCount($adminId)); } // ========================================================================= // 11. markRead 越权防护 // ========================================================================= public function test_mark_read_cannot_touch_other_admins_rows(): void { $adminA = $this->createAdmin(); $adminB = $this->createAdmin(); NoticeService::send([$adminA], 'A的通知', '内容'); NoticeService::send([$adminB], 'B的通知', '内容'); $this->assertSame(1, NoticeService::unreadCount($adminA)); $this->assertSame(1, NoticeService::unreadCount($adminB)); $bTargetId = (int) Db::name('system_notice_target') ->where('admin_id', $adminB) ->value('id'); // adminA 尝试读 adminB 的行:影响 0 行,adminB 数据不受影响 $this->assertSame(0, NoticeService::markRead([$bTargetId], $adminA), 'markRead 不得触碰他人行'); $this->assertSame(0, (int) Db::name('system_notice_target')->where('id', $bTargetId)->value('read_time')); $this->assertSame(1, NoticeService::unreadCount($adminB)); // markAllRead 同样带 admin_id 条件:只影响自己 NoticeService::markAllRead($adminA); $this->assertSame(0, NoticeService::unreadCount($adminA)); $this->assertSame(1, NoticeService::unreadCount($adminB), 'markAllRead 不得触碰他人行'); } // ========================================================================= // fixture 工厂(事务内运行,tearDown 自动回滚) // ========================================================================= /** * 造启用管理员. * * @param array $overrides 字段覆盖(email/status 等) * @return int 管理员ID */ private function createAdmin(array $overrides = []): int { $data = array_merge([ 'username' => 'ntc_' . 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); } /** * 插入订阅矩阵行. */ private function insertSubscribe(int $adminId, string $scene, string $channel, int $status): void { Db::name('system_notice_subscribe')->insert([ 'admin_id' => $adminId, 'scene' => $scene, 'channel' => $channel, 'status' => $status, 'create_time' => time(), 'update_time' => time(), ]); } /** * 写 notify 组 inmail 总闸配置行并清缓存. * * sysconfig 缓存不受事务回滚影响:非 '0' 值会缓存 3600 秒, * 必须显式清 tag 才能让后续读取看到事务内的新值。 */ private function setInmailSwitch(string $value): void { Db::name('system_config')->insert([ 'name' => 'notify_inmail_enable', 'group' => 'notify', 'value' => $value, 'remark' => 'NoticeServiceTest fixture', 'sort' => 100, 'create_time' => time(), 'update_time' => time(), ]); Cache::tag('sysconfig')->clear(); } /** * 删除 notify 组 inmail 总闸配置行并清缓存. */ private function clearInmailSwitch(): void { Db::name('system_config') ->where('group', 'notify') ->where('name', 'notify_inmail_enable') ->delete(); Cache::tag('sysconfig')->clear(); } }