using F9WebAiTest.Application.Common; using F9WebAiTest.Application.Dtos; using F9WebAiTest.Application.Dtos.Notice; using F9WebAiTest.Application.Services.Auth; using F9WebAiTest.Application.Interfaces.SnowflakeID; using F9WebAiTest.Domain.Common; using F9WebAiTest.Domain.Entities; using F9WebAiTest.Domain.Entities.Notice; using F9WebAiTest.Domain.Enums.SYS.RBAC; using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; namespace F9WebAiTest.Application.Services.Notice; public interface INotificationService { Task> GetSubscribeListAsync(long userId); Task SubscribeAsync(long userId, long typeId, bool subscribe); Task> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category); Task MarkReadAsync(long userId, long messageId); Task MarkAllReadAsync(long userId); Task GetUnreadCountAsync(long userId); // ---- 待办 ---- Task> GetMyTodosAsync(long userId, int? status); Task CompleteTodoAsync(long userId, long todoId, string handleResult); // ---- 管理端:消息类型维护 ---- Task> GetTypeListAsync(); Task CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId); Task UpdateTypeAsync(long id, NotificationTypeCreateDto dto); Task DeleteTypeAsync(long id); } public class NotificationService : INotificationService { private readonly IRepository _typeRepo; private readonly IRepository _subRepo; private readonly IRepository _msgRepo; private readonly IRepository _receiverRepo; private readonly IRepository _todoRepo; private readonly IRepository _roleMenuRepo; private readonly IRepository _permissionRepo; private readonly ISnowflakeIdGenerator _snowflake; private readonly IAuthService _authService; public NotificationService( IRepository typeRepo, IRepository subRepo, IRepository msgRepo, IRepository receiverRepo, IRepository todoRepo, IRepository roleMenuRepo, IRepository permissionRepo, ISnowflakeIdGenerator snowflake, IAuthService authService) { _typeRepo = typeRepo; _subRepo = subRepo; _msgRepo = msgRepo; _receiverRepo = receiverRepo; _todoRepo = todoRepo; _roleMenuRepo = roleMenuRepo; _permissionRepo = permissionRepo; _snowflake = snowflake; _authService = authService; } public async Task> GetSubscribeListAsync(long userId) { var types = (await _typeRepo.FindAsync(t => t.IsActive)).OrderBy(t => t.Sort).ThenBy(t => t.Id).ToList(); var perms = await _authService.GetUserPermissionsAsync(userId); var subs = await _subRepo.FindAsync(s => s.UserId == userId && s.IsActive); var subIds = subs.Select(s => s.NotificationTypeId).ToHashSet(); return types.Select(t => new NotificationTypeDto { Id = t.Id, Code = t.Code, Name = t.Name, Description = t.Description, RequiredPermission = t.RequiredPermission, IsActive = t.IsActive, CreatedBy = t.CreatedBy, Sort = t.Sort, Category = (int)t.Category, Icon = t.Icon, RouteTemplate = t.RouteTemplate, Subscribed = subIds.Contains(t.Id), CanSubscribe = string.IsNullOrEmpty(t.RequiredPermission) || perms.Contains(t.RequiredPermission) }).ToList(); } public async Task SubscribeAsync(long userId, long typeId, bool subscribe) { var type = await _typeRepo.GetByIdAsync(typeId); if (type == null || !type.IsActive) throw new ServiceException("消息类型不存在或已关闭订阅"); // 权限校验:RequiredPermission 为空表示所有用户可订阅 if (!string.IsNullOrEmpty(type.RequiredPermission)) { var perms = await _authService.GetUserPermissionsAsync(userId); if (!perms.Contains(type.RequiredPermission)) throw new ServiceException($"您没有订阅「{type.Name}」的权限", 403); } var sub = await _subRepo.FirstOrDefaultAsync(s => s.UserId == userId && s.NotificationTypeId == typeId); if (subscribe) { if (sub == null) { await _subRepo.AddAsync(new UserNotificationSubscription { UserId = userId, NotificationTypeId = typeId, IsActive = true, SubscribedTime = DateTime.Now }); } else { sub.IsActive = true; sub.SubscribedTime = DateTime.Now; sub.CancelTime = null; await _subRepo.UpdateAsync(sub); } } else { if (sub != null && sub.IsActive) { sub.IsActive = false; sub.CancelTime = DateTime.Now; await _subRepo.UpdateAsync(sub); } } await _subRepo.SaveChangesAsync(); } public async Task> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category) { var receivers = await _receiverRepo.FindAsync(r => r.UserId == userId); if (readStatus.HasValue) receivers = receivers.Where(r => (int)r.ReadStatus == readStatus.Value).ToList(); var msgIds = receivers.Select(r => r.MessageId).Distinct().ToList(); var msgs = await _msgRepo.FindAsync(m => msgIds.Contains(m.Id)); if (category.HasValue) msgs = msgs.Where(m => (int)m.Category == category.Value).ToList(); var msgDict = msgs.ToDictionary(m => m.Id); var typeIds = msgs.Select(m => m.NotificationTypeId).Distinct().ToList(); var types = await _typeRepo.FindAsync(t => typeIds.Contains(t.Id)); var typeDict = types.ToDictionary(t => t.Id, t => t.Name); var items = receivers .Where(r => msgDict.ContainsKey(r.MessageId)) .Select(r => { var m = msgDict[r.MessageId]; return new NotificationMessageDto { Id = m.Id, NotificationTypeId = m.NotificationTypeId, MessageTypeCode = m.MessageTypeCode, TypeName = typeDict.GetValueOrDefault(m.NotificationTypeId, ""), Title = m.Title, Content = m.Content, Priority = (int)m.Priority, BusinessType = m.BusinessType, BusinessId = m.BusinessId, Category = (int)m.Category, RoutePath = m.RoutePath, CreatedTime = m.CreatedAt, ReadStatus = (int)r.ReadStatus, ReadTime = r.ReadTime }; }) .OrderByDescending(x => x.CreatedTime) .ToList(); var total = items.Count; var paged = items.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); return new PageResult { Items = paged, Total = total, PageIndex = pageIndex, PageSize = pageSize }; } public async Task MarkReadAsync(long userId, long messageId) { var receiver = await _receiverRepo.FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId); if (receiver == null) throw new ServiceException("消息不存在", 404); receiver.ReadStatus = NotificationReadStatus.Read; receiver.ReadTime = DateTime.Now; await _receiverRepo.UpdateAsync(receiver); await _receiverRepo.SaveChangesAsync(); } public async Task MarkAllReadAsync(long userId) { var unread = await _receiverRepo.FindAsync(r => r.UserId == userId && r.ReadStatus == NotificationReadStatus.Unread); foreach (var r in unread) { r.ReadStatus = NotificationReadStatus.Read; r.ReadTime = DateTime.Now; await _receiverRepo.UpdateAsync(r); } } public async Task GetUnreadCountAsync(long userId) => (int)await _receiverRepo.CountAsync(r => r.UserId == userId && r.ReadStatus == NotificationReadStatus.Unread); // ==================== 待办 ==================== public async Task> GetMyTodosAsync(long userId, int? status) { var todos = await _todoRepo.FindAsync(t => t.AssigneeId == userId); if (status.HasValue) todos = todos.Where(t => (int)t.TodoStatus == status.Value).ToList(); var msgIds = todos.Select(t => t.MessageId).Distinct().ToList(); var msgs = await _msgRepo.FindAsync(m => msgIds.Contains(m.Id)); var msgDict = msgs.ToDictionary(m => m.Id); return todos .Where(t => msgDict.ContainsKey(t.MessageId)) .OrderByDescending(t => t.CreatedAt) .Select(t => { var m = msgDict[t.MessageId]; return new TodoItemDto { Id = t.Id, MessageId = t.MessageId, Title = m.Title, Content = m.Content, Priority = (int)m.Priority, BusinessType = m.BusinessType, BusinessId = m.BusinessId, RoutePath = m.RoutePath, TodoStatus = (int)t.TodoStatus, DueTime = t.DueTime, CompletedTime = t.CompletedTime, CreatedTime = t.CreatedAt }; }) .ToList(); } public async Task CompleteTodoAsync(long userId, long todoId, string handleResult) { var todo = await _todoRepo.GetByIdAsync(todoId); if (todo == null) throw new ServiceException("待办不存在", 404); if (todo.AssigneeId != userId) throw new ServiceException("您不是该待办的处理人", 403); if (todo.TodoStatus == TodoItemStatus.Completed) throw new ServiceException("该待办已处理"); todo.TodoStatus = TodoItemStatus.Completed; todo.CompletedTime = DateTime.Now; todo.HandleResult = handleResult ?? ""; await _todoRepo.UpdateAsync(todo); await _todoRepo.SaveChangesAsync(); // 完成待办后,标记对应消息已读(处理完了才从"未读提醒"中消失) var receiver = await _receiverRepo.FirstOrDefaultAsync(r => r.MessageId == todo.MessageId && r.UserId == userId); if (receiver != null && receiver.ReadStatus == NotificationReadStatus.Unread) { receiver.ReadStatus = NotificationReadStatus.Read; receiver.ReadTime = DateTime.Now; await _receiverRepo.UpdateAsync(receiver); await _receiverRepo.SaveChangesAsync(); } } // ==================== 管理端:消息类型维护 ==================== public async Task> GetTypeListAsync() { var types = (await _typeRepo.GetAllAsync()).OrderBy(t => t.Sort).ThenBy(t => t.Id).ToList(); return types.Select(t => new NotificationTypeDto { Id = t.Id, Code = t.Code, Name = t.Name, Description = t.Description, RequiredPermission = t.RequiredPermission, IsActive = t.IsActive, CreatedBy = t.CreatedBy, Sort = t.Sort, Category = (int)t.Category, Icon = t.Icon, RouteTemplate = t.RouteTemplate }).ToList(); } public async Task CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId) { if (string.IsNullOrWhiteSpace(dto.Code)) throw new ServiceException("消息类型编码不能为空"); if (string.IsNullOrWhiteSpace(dto.Name)) throw new ServiceException("消息类型名称不能为空"); if (await _typeRepo.AnyAsync(t => t.Code == dto.Code)) throw new ServiceException($"编码「{dto.Code}」已存在"); var e = new NotificationType { Code = dto.Code.Trim(), Name = dto.Name, Description = dto.Description ?? "", RequiredPermission = dto.RequiredPermission ?? "", IsActive = dto.IsActive, CreatedBy = creatorId, Sort = dto.Sort, Category = (NotificationCategory)dto.Category, Icon = dto.Icon ?? "", RouteTemplate = dto.RouteTemplate ?? "" }; await _typeRepo.AddAsync(e); await _typeRepo.SaveChangesAsync(); // 同步订阅权限到 RBAC 权限单元(角色管理里可给角色分配) await SyncSubscribePermissionAsync(e); return new NotificationTypeDto { Id = e.Id, Code = e.Code, Name = e.Name, Description = e.Description, RequiredPermission = e.RequiredPermission, IsActive = e.IsActive, CreatedBy = e.CreatedBy, Sort = e.Sort, Category = (int)e.Category, Icon = e.Icon, RouteTemplate = e.RouteTemplate }; } public async Task UpdateTypeAsync(long id, NotificationTypeCreateDto dto) { var e = await _typeRepo.GetByIdAsync(id); if (e == null) throw new ServiceException("消息类型不存在", 404); if (string.IsNullOrWhiteSpace(dto.Name)) throw new ServiceException("消息类型名称不能为空"); e.Name = dto.Name; e.Description = dto.Description ?? ""; e.RequiredPermission = dto.RequiredPermission ?? ""; e.IsActive = dto.IsActive; e.Sort = dto.Sort; e.Category = (NotificationCategory)dto.Category; e.Icon = dto.Icon ?? ""; e.RouteTemplate = dto.RouteTemplate ?? ""; await _typeRepo.UpdateAsync(e); await _typeRepo.SaveChangesAsync(); // 同步订阅权限到 RBAC 权限单元 await SyncSubscribePermissionAsync(e); return new NotificationTypeDto { Id = e.Id, Code = e.Code, Name = e.Name, Description = e.Description, RequiredPermission = e.RequiredPermission, IsActive = e.IsActive, CreatedBy = e.CreatedBy, Sort = e.Sort, Category = (int)e.Category, Icon = e.Icon, RouteTemplate = e.RouteTemplate }; } public async Task DeleteTypeAsync(long id) { var e = await _typeRepo.GetByIdAsync(id); if (e == null) throw new ServiceException("消息类型不存在", 404); // 清理对应的订阅权限及其角色关联 var permission = await _permissionRepo.FirstOrDefaultAsync(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == e.Id); if (permission != null) { var relations = await _roleMenuRepo.FindAsync(rp => rp.PermissionId == permission.Id); if (relations.Any()) await _roleMenuRepo.HardDeleteRangeAsync(relations); await _permissionRepo.HardDeleteAsync(permission); await _permissionRepo.SaveChangesAsync(); } await _typeRepo.DeleteAsync(e); await _typeRepo.SaveChangesAsync(); } /// /// 将消息类型的订阅权限同步为 RBAC 的权限单元(ResourceType = 消息订阅), /// 这样在「角色管理 → 分配权限」里就能给角色勾选该消息类型的订阅权限。 /// private async Task SyncSubscribePermissionAsync(NotificationType type) { // 无 RequiredPermission 表示所有用户可订阅,无需权限单元(清理旧权限) if (string.IsNullOrEmpty(type.RequiredPermission)) { var old = await _permissionRepo.FirstOrDefaultAsync(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == type.Id); if (old != null) { var relations = await _roleMenuRepo.FindAsync(rp => rp.PermissionId == old.Id); if (relations.Any()) await _roleMenuRepo.HardDeleteRangeAsync(relations); await _permissionRepo.HardDeleteAsync(old); } await _permissionRepo.SaveChangesAsync(); return; } // 查找该消息类型的订阅权限单元(按 ResourceType + ResourceId 幂等) var permission = await _permissionRepo.FirstOrDefaultAsync(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == type.Id); if (permission == null) { permission = new RBAC.Permission( _snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.消息订阅, type.Id.ToString(), type.Id); await _permissionRepo.AddAsync(permission); } // 业务权限码与 RBAC 权限码保持一致(SubscribeAsync 用 RequiredPermission 校验) permission.SetCode(type.RequiredPermission); await _permissionRepo.UpdateAsync(permission); await _permissionRepo.SaveChangesAsync(); } }