Files
2026-10-08 10:43:59 +08:00

421 lines
17 KiB
C#

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<List<NotificationTypeDto>> GetSubscribeListAsync(long userId);
Task SubscribeAsync(long userId, long typeId, bool subscribe);
Task<PageResult<NotificationMessageDto>> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category);
Task MarkReadAsync(long userId, long messageId);
Task MarkAllReadAsync(long userId);
Task<int> GetUnreadCountAsync(long userId);
// ---- 待办 ----
Task<List<TodoItemDto>> GetMyTodosAsync(long userId, int? status);
Task CompleteTodoAsync(long userId, long todoId, string handleResult);
// ---- 管理端:消息类型维护 ----
Task<List<NotificationTypeDto>> GetTypeListAsync();
Task<NotificationTypeDto> CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId);
Task<NotificationTypeDto> UpdateTypeAsync(long id, NotificationTypeCreateDto dto);
Task DeleteTypeAsync(long id);
}
public class NotificationService : INotificationService
{
private readonly IRepository<NotificationType> _typeRepo;
private readonly IRepository<UserNotificationSubscription> _subRepo;
private readonly IRepository<NotificationMessage> _msgRepo;
private readonly IRepository<NotificationReceiver> _receiverRepo;
private readonly IRepository<TodoItem> _todoRepo;
private readonly IRepository<RBAC.RolePermission> _roleMenuRepo;
private readonly IRepository<RBAC.Permission> _permissionRepo;
private readonly ISnowflakeIdGenerator _snowflake;
private readonly IAuthService _authService;
public NotificationService(
IRepository<NotificationType> typeRepo,
IRepository<UserNotificationSubscription> subRepo,
IRepository<NotificationMessage> msgRepo,
IRepository<NotificationReceiver> receiverRepo,
IRepository<TodoItem> todoRepo,
IRepository<RBAC.RolePermission> roleMenuRepo,
IRepository<RBAC.Permission> 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<List<NotificationTypeDto>> 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<PageResult<NotificationMessageDto>> 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<NotificationMessageDto>
{
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<int> GetUnreadCountAsync(long userId)
=> (int)await _receiverRepo.CountAsync(r => r.UserId == userId && r.ReadStatus == NotificationReadStatus.Unread);
// ==================== 待办 ====================
public async Task<List<TodoItemDto>> 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<List<NotificationTypeDto>> 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<NotificationTypeDto> 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<NotificationTypeDto> 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();
}
/// <summary>
/// 将消息类型的订阅权限同步为 RBAC 的权限单元(ResourceType = 消息订阅),
/// 这样在「角色管理 → 分配权限」里就能给角色勾选该消息类型的订阅权限。
/// </summary>
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();
}
}