From a7261d0c21093ac2243a709d02ff1b8c4fd6f0a7 Mon Sep 17 00:00:00 2001 From: augushong Date: Mon, 12 Oct 2026 11:13:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(notice):=20NoticeService=20=E5=8F=91?= =?UTF-8?q?=E9=80=81=E6=A0=B8=E5=BF=83/=E5=9C=BA=E6=99=AF=E6=B3=A8?= =?UTF-8?q?=E5=86=8C/=E8=AE=A2=E9=98=85=E8=BF=87=E6=BB=A4=E4=B8=8E?= =?UTF-8?q?=E7=AB=99=E5=86=85=E4=BF=A1=E9=A9=B1=E5=8A=A8=E5=8F=8A=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/NoticeService.php | 15 + .../common/notice/NoticeChannelInterface.php | 34 ++ .../common/notice/channel/InMailChannel.php | 27 ++ .../base/common/service/NoticeServiceBase.php | 287 ++++++++++++ tests/Feature/NoticeServiceTest.php | 410 ++++++++++++++++++ 5 files changed, 773 insertions(+) create mode 100644 app/common/service/NoticeService.php create mode 100644 extend/base/common/notice/NoticeChannelInterface.php create mode 100644 extend/base/common/notice/channel/InMailChannel.php create mode 100644 extend/base/common/service/NoticeServiceBase.php create mode 100644 tests/Feature/NoticeServiceTest.php diff --git a/app/common/service/NoticeService.php b/app/common/service/NoticeService.php new file mode 100644 index 0000000..0ff8ab2 --- /dev/null +++ b/app/common/service/NoticeService.php @@ -0,0 +1,15 @@ + ['title' => '...', 'channels' => [...], 'locked' => false]]); + */ +class NoticeService extends NoticeServiceBase +{ +} diff --git a/extend/base/common/notice/NoticeChannelInterface.php b/extend/base/common/notice/NoticeChannelInterface.php new file mode 100644 index 0000000..117ce45 --- /dev/null +++ b/extend/base/common/notice/NoticeChannelInterface.php @@ -0,0 +1,34 @@ + [...]]); + */ +class NoticeServiceBase +{ + /** 渠道标识:站内信 */ + public const CHANNEL_INMAIL = 'inmail'; + + /** 渠道标识:邮件 */ + public const CHANNEL_EMAIL = 'email'; + + /** 渠道标识:短信 */ + public const CHANNEL_SMS = 'sms'; + + /** 渠道标识:公众号 */ + public const CHANNEL_WECHAT = 'wechat'; + + /** + * 场景注册表(代码级,app 层可覆盖合并). + * + * 每项含 title(展示名)/ channels(该场景允许投递的渠道)/ locked(强制必选, + * 订阅不可关闭,发送侧豁免订阅过滤)。locked 不豁免渠道 ∩ 规则: + * 强投递同样仅限场景注册渠道。 + */ + public static function scenes(): array + { + return [ + 'system' => ['title' => '系统公告', 'channels' => [self::CHANNEL_INMAIL], 'locked' => true], + 'common' => ['title' => '通用通知', 'channels' => [self::CHANNEL_INMAIL, self::CHANNEL_EMAIL], 'locked' => false], + 'admin_login' => ['title' => '登录提醒', 'channels' => [self::CHANNEL_INMAIL, self::CHANNEL_EMAIL], 'locked' => false], + 'mall_stock' => ['title' => '库存预警', 'channels' => [self::CHANNEL_INMAIL, self::CHANNEL_EMAIL], 'locked' => false], + ]; + } + + /** + * 渠道驱动映射(class-string,延迟实例化). + * + * email/sms/wechat 驱动类由后续任务提供类文件;class-string 字面量 + * 为编译期解析,不触发 autoload,本任务内无实例化路径,映射先行声明安全。 + */ + public static function channels(): array + { + return [ + self::CHANNEL_INMAIL => InMailChannel::class, + self::CHANNEL_EMAIL => EmailChannel::class, + self::CHANNEL_SMS => SmsChannel::class, + self::CHANNEL_WECHAT => WechatChannel::class, + ]; + } + + /** + * 统一发送通知. + * + * 渠道解析规则(统一口径): + * - 实际投递渠道 = options.channels ?? 注册场景的 scene.channels ?? [inmail] + * - 注册场景再 ∩ scene.channels(保序);locked 强投递同样仅限场景注册渠道 + * - 未注册场景跳过 ∩,缺省 [inmail],并记 warning 放行 + * + * @param array|int|string $adminIds 管理员ID数组、单个ID或 'all'(status=1 全体) + * @param string $title 标题 + * @param string $content 内容 + * @param array $options scene(缺省 common)/channels/biz_id/send_admin_id + * @return int 通知ID(system_notice.id) + */ + public static function send(array|int|string $adminIds, string $title, string $content, array $options = []): int + { + $scene = (string) ($options['scene'] ?? 'common'); + $scenes = static::scenes(); + $registered = isset($scenes[$scene]); + + if (!$registered) { + Log::warning('notice send: unregistered scene, fallback to default channels', ['scene' => $scene]); + } + + // 渠道解析:第一段 = options.channels ?? 注册场景的 scene.channels ?? [inmail] + if (isset($options['channels'])) { + $channels = array_values(array_unique(array_map('strval', (array) $options['channels']))); + } elseif ($registered) { + $channels = array_map('strval', $scenes[$scene]['channels']); + } else { + $channels = [self::CHANNEL_INMAIL]; + } + + // 渠道解析:第二段 = 注册场景再 ∩ scene.channels(保序),locked 不豁免 + if ($registered) { + $channels = array_values(array_intersect($channels, $scenes[$scene]['channels'])); + } + + if ($adminIds === 'all') { + $adminIds = Db::name('system_admin')->where('status', 1)->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; + + $targets = []; + if ($adminIds !== []) { + $targets = static::buildTargets($noticeId, $adminIds, $scene, $channels, $registered, $now); + if ($targets !== []) { + Db::name('system_notice_target')->insertAll($targets); + } + } + + Event::trigger('NoticeSended', [$notice, $targets]); + + return $noticeId; + } + + /** + * 展开投递行:对每个 admin×每个实际渠道做订阅过滤与地址解析. + * + * @param int $noticeId 通知ID + * @param array $adminIds 管理员ID列表 + * @param string $scene 场景 + * @param array $channels 解析后的实际投递渠道(可能为空数组) + * @param bool $registered 场景是否已注册(决定 locked 语义是否生效) + * @param int $now 时间戳 + * @return array 投递行列表(insertAll 格式) + */ + protected static function buildTargets(int $noticeId, array $adminIds, string $scene, array $channels, bool $registered, int $now): array + { + if ($channels === []) { + return []; + } + + $locked = $registered && !empty(static::scenes()[$scene]['locked']); + + // inmail 全局总闸:sysconfig('notify','notify_inmail_enable') 严格等于 0 时关闭; + // 键不存在(null/缺省 1)视为开。DB value 为字符串,须强转 int 后比较。 + $inmailEnabled = true; + if (in_array(self::CHANNEL_INMAIL, $channels, true)) { + $inmailEnabled = (int) sysconfig('notify', 'notify_inmail_enable', 1) !== 0; + } + + // 一次取齐 email 地址映射(email 渠道地址 = admin.email) + $adminEmailMap = Db::name('system_admin') + ->whereIn('id', $adminIds) + ->column('email', 'id'); + + $targets = []; + foreach ($adminIds as $adminId) { + $adminId = (int) $adminId; + foreach ($channels as $channel) { + // locked 场景强制投递;否则按订阅矩阵过滤(status=0 退订即跳过该渠道) + if (!$locked && !static::isSubscribed($adminId, $scene, $channel)) { + continue; + } + + $targetAddr = ''; + if ($channel === self::CHANNEL_EMAIL) { + $targetAddr = (string) ($adminEmailMap[$adminId] ?? ''); + if ($targetAddr === '') { + Log::info('notice send: skip email channel, admin has no email', ['admin_id' => $adminId, 'notice_id' => $noticeId]); + continue; + } + } + + if ($channel === self::CHANNEL_INMAIL && !$inmailEnabled) { + continue; + } + + // inmail 直接置成功终态(投递即落库),其余渠道待消费者投递 + $isInmail = $channel === self::CHANNEL_INMAIL; + $status = $isInmail ? SystemNoticeTargetBase::STATUS_SUCCESS : SystemNoticeTargetBase::STATUS_PENDING; + + $targets[] = [ + 'notice_id' => $noticeId, + 'admin_id' => $adminId, + 'channel' => $channel, + 'target_addr' => $targetAddr, + 'status' => $status, + 'send_time' => $isInmail ? $now : 0, + 'read_time' => 0, + 'create_time' => $now, + 'update_time' => $now, + ]; + } + } + + return $targets; + } + + /** + * 是否订阅指定场景渠道. + * + * locked 场景恒 true;subscribe 表无记录视为默认订阅; + * 仅 status=0(显式退订)返回 false。 + */ + public static function isSubscribed(int $adminId, string $scene, string $channel): bool + { + $scenes = static::scenes(); + if (isset($scenes[$scene]) && !empty($scenes[$scene]['locked'])) { + return true; + } + + $status = Db::name('system_notice_subscribe') + ->where('admin_id', $adminId) + ->where('scene', $scene) + ->where('channel', $channel) + ->value('status'); + + if ($status === null) { + return true; + } + + return (int) $status === 1; + } + + /** + * 未读站内信数(channel=inmail AND read_time=0 AND status=2,走复合索引). + */ + 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) + ->count(); + } + + /** + * 标记站内信已读(带 admin_id 条件防越权). + * + * @param array $targetIds 投递行ID列表 + * @param int $adminId 当前管理员 + * @return int 实际标记的行数 + */ + public static function markRead(array $targetIds, int $adminId): int + { + if ($targetIds === []) { + return 0; + } + + return Db::name('system_notice_target') + ->where('admin_id', $adminId) + ->whereIn('id', $targetIds) + ->where('read_time', 0) + ->update(['read_time' => time()]); + } + + /** + * 全部站内信标记已读(带 admin_id 条件防越权). + * + * @return int 实际标记的行数 + */ + public static function markAllRead(int $adminId): int + { + return Db::name('system_notice_target') + ->where('admin_id', $adminId) + ->where('channel', self::CHANNEL_INMAIL) + ->where('read_time', 0) + ->update(['read_time' => time()]); + } +} diff --git a/tests/Feature/NoticeServiceTest.php b/tests/Feature/NoticeServiceTest.php new file mode 100644 index 0000000..4a17092 --- /dev/null +++ b/tests/Feature/NoticeServiceTest.php @@ -0,0 +1,410 @@ +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(); + } +}