fix(notice): 发送事务包裹与软删过滤加固

This commit is contained in:
augushong
2026-10-12 14:39:17 +00:00
parent 710ce97473
commit 77e75fcc0b
3 changed files with 76 additions and 21 deletions

View File

@@ -61,12 +61,15 @@ class NoticeBase extends AdminController
->withJoin('notice', 'LEFT') ->withJoin('notice', 'LEFT')
->where('system_notice_target.admin_id', $adminId) ->where('system_notice_target.admin_id', $adminId)
->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL) ->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL)
// 软删通知不进个人域(管理页删除后此处消失)
->where('system_notice.delete_time', 0)
->where($where) ->where($where)
->count(); ->count();
$list = $this->model $list = $this->model
->withJoin('notice', 'LEFT') ->withJoin('notice', 'LEFT')
->where('system_notice_target.admin_id', $adminId) ->where('system_notice_target.admin_id', $adminId)
->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL) ->where('system_notice_target.channel', NoticeService::CHANNEL_INMAIL)
->where('system_notice.delete_time', 0)
->where($where) ->where($where)
->page($page, $limit) ->page($page, $limit)
->order($this->sort) ->order($this->sort)
@@ -95,6 +98,8 @@ class NoticeBase extends AdminController
->withJoin('notice', 'LEFT') ->withJoin('notice', 'LEFT')
->where('system_notice_target.id', (int) $id) ->where('system_notice_target.id', (int) $id)
->where('system_notice_target.admin_id', $adminId) ->where('system_notice_target.admin_id', $adminId)
// 软删通知不进个人域(管理页删除后详情不可打开)
->where('system_notice.delete_time', 0)
->find(); ->find();
if (empty($row)) { if (empty($row)) {
$this->error('消息不存在或无权查看'); $this->error('消息不存在或无权查看');

View File

@@ -109,33 +109,39 @@ class NoticeServiceBase
} }
if ($adminIds === 'all') { 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 { } else {
$adminIds = array_values(array_unique(array_map('intval', (array) $adminIds))); $adminIds = array_values(array_unique(array_map('intval', (array) $adminIds)));
} }
$now = time(); $now = time();
$notice = SystemNotice::create([ // 主体创建与投递展开同事务:insertAll 中途异常时回滚,不残留零投递主体
'scene' => $scene, [$notice, $targets] = Db::transaction(function () use ($adminIds, $channels, $content, $now, $options, $registered, $scene, $title) {
'title' => $title, $notice = SystemNotice::create([
'content' => $content, 'scene' => $scene,
'biz_id' => (string) ($options['biz_id'] ?? ''), 'title' => $title,
'send_admin_id' => (int) ($options['send_admin_id'] ?? 0), 'content' => $content,
]); 'biz_id' => (string) ($options['biz_id'] ?? ''),
$noticeId = (int) $notice->id; 'send_admin_id' => (int) ($options['send_admin_id'] ?? 0),
]);
$noticeId = (int) $notice->id;
$targets = []; $targets = [];
if ($adminIds !== []) { if ($adminIds !== []) {
$targets = static::buildTargets($noticeId, $adminIds, $scene, $channels, $registered, $now); $targets = static::buildTargets($noticeId, $adminIds, $scene, $channels, $registered, $now);
if ($targets !== []) { if ($targets !== []) {
Db::name('system_notice_target')->insertAll($targets); Db::name('system_notice_target')->insertAll($targets);
}
} }
}
return [$notice, $targets];
});
Event::trigger('NoticeSended', [$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,走复合索引). * 未读站内信数(channel=inmail AND read_time=0 AND status=2,走复合索引).
* JOIN 过滤软删通知:与个人域 mine/read 口径一致,管理页删除后未读数同步下降.
*/ */
public static function unreadCount(int $adminId): int public static function unreadCount(int $adminId): int
{ {
return Db::name('system_notice_target') return Db::name('system_notice_target')
->where('admin_id', $adminId) ->alias('target')
->where('channel', self::CHANNEL_INMAIL) ->join('system_notice notice', 'notice.id = target.notice_id')
->where('read_time', 0) ->where('target.admin_id', $adminId)
->where('status', SystemNoticeTargetBase::STATUS_SUCCESS) ->where('target.channel', self::CHANNEL_INMAIL)
->where('target.read_time', 0)
->where('target.status', SystemNoticeTargetBase::STATUS_SUCCESS)
->where('notice.delete_time', 0)
->count(); ->count();
} }

View File

@@ -12,7 +12,7 @@ use think\facade\Db;
/** /**
* NoticeService 核心行为集成测试. * NoticeService 核心行为集成测试.
* *
* 覆盖 11 个契约点(与计划任务 3 逐项对齐,每项一个 test 方法): * 覆盖 13 个契约点(与计划任务 3 逐项对齐,每项一个 test 方法):
* 1. send 主体 + target 展开 * 1. send 主体 + target 展开
* 2. 'all' 广播(仅 status=1,含无 email 管理员) * 2. 'all' 广播(仅 status=1,含无 email 管理员)
* 3. channels 缺省按 scene.channels 展开 * 3. channels 缺省按 scene.channels 展开
@@ -24,6 +24,8 @@ use think\facade\Db;
* 9. inmail 全局总闸关闭跳过(键不存在视为开) * 9. inmail 全局总闸关闭跳过(键不存在视为开)
* 10. unreadCount/markRead/markAllRead 数字正确 * 10. unreadCount/markRead/markAllRead 数字正确
* 11. markRead 越权防护 * 11. markRead 越权防护
* 12. 'all' 广播过滤软删管理员(F2-C10)
* 13. unreadCount 不受软删通知影响(F2-C6 个人域口径)
* *
* fixture 均为事务内自造数据,tearDown 自动回滚;缓存清理显式执行 * fixture 均为事务内自造数据,tearDown 自动回滚;缓存清理显式执行
* (Cache 不受事务回滚影响,见 setInmailSwitch/clearInmailSwitch)。 * (Cache 不受事务回滚影响,见 setInmailSwitch/clearInmailSwitch)。
@@ -335,6 +337,44 @@ class NoticeServiceTest extends TestCase
$this->assertSame(1, NoticeService::unreadCount($adminB), 'markAllRead 不得触碰他人行'); $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 自动回滚) // fixture 工厂(事务内运行,tearDown 自动回滚)
// ========================================================================= // =========================================================================