using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Text.Json; using XiangYi.Application.DTOs.Responses; using XiangYi.Application.Interfaces; using XiangYi.Core.Entities.Biz; using XiangYi.Core.Interfaces; using XiangYi.Infrastructure.WeChat; namespace XiangYi.Application.Services; /// /// 通知服务实现 /// public class NotificationService : INotificationService { private readonly IRepository _notificationRepository; private readonly IRepository _readRepository; private readonly IRepository _userRepository; private readonly IWeChatService _weChatService; private readonly WeChatOptions _weChatOptions; private readonly ISystemConfigService _configService; private readonly ILogger _logger; /// /// 通知状态:草稿 /// public const int StatusDraft = 1; /// /// 通知状态:已发布 /// public const int StatusPublished = 2; /// /// 目标类型:全部用户 /// public const int TargetTypeAll = 1; /// /// 目标类型:指定用户 /// public const int TargetTypeSpecific = 2; public NotificationService( IRepository notificationRepository, IRepository readRepository, IRepository userRepository, IWeChatService weChatService, IOptions weChatOptions, ISystemConfigService configService, ILogger logger) { _notificationRepository = notificationRepository; _readRepository = readRepository; _userRepository = userRepository; _weChatService = weChatService; _weChatOptions = weChatOptions.Value; _configService = configService; _logger = logger; } /// public async Task> GetNotificationListAsync(long userId, int pageIndex, int pageSize) { // 获取已发布的通知 var (notifications, total) = await _notificationRepository.GetPagedListAsync( n => n.Status == StatusPublished, pageIndex, pageSize, n => n.PublishTime!, isDescending: true); // 过滤出用户可见的通知(全部用户或指定用户包含当前用户) var visibleNotifications = notifications .Where(n => IsNotificationVisibleToUser(n, userId)) .ToList(); // 获取用户的已读记录 var notificationIds = visibleNotifications.Select(n => n.Id).ToList(); var readRecords = await _readRepository.GetListAsync(r => r.UserId == userId && notificationIds.Contains(r.NotificationId)); var readDict = readRecords.ToDictionary(r => r.NotificationId, r => r); // 映射响应 var responses = visibleNotifications.Select(n => { var isRead = readDict.TryGetValue(n.Id, out var readRecord); return MapToNotificationResponse(n, isRead, readRecord?.ReadTime); }).ToList(); return new PagedResult { Items = responses, Total = total, PageIndex = pageIndex, PageSize = pageSize }; } /// public async Task MarkAsReadAsync(long userId, long notificationId) { // 检查通知是否存在 var notification = await _notificationRepository.GetByIdAsync(notificationId); if (notification == null || notification.Status != StatusPublished) { _logger.LogWarning("通知不存在或未发布: NotificationId={NotificationId}", notificationId); return false; } // 检查用户是否可见该通知 if (!IsNotificationVisibleToUser(notification, userId)) { _logger.LogWarning("用户无权查看该通知: UserId={UserId}, NotificationId={NotificationId}", userId, notificationId); return false; } // 检查是否已读(幂等性) var existingRead = await _readRepository.GetListAsync(r => r.UserId == userId && r.NotificationId == notificationId); if (existingRead.Any()) { _logger.LogInformation("通知已标记为已读: UserId={UserId}, NotificationId={NotificationId}", userId, notificationId); return true; } // 创建已读记录 var readRecord = new UserNotificationRead { UserId = userId, NotificationId = notificationId, ReadTime = DateTime.Now, CreateTime = DateTime.Now, UpdateTime = DateTime.Now }; await _readRepository.AddAsync(readRecord); _logger.LogInformation("标记通知为已读: UserId={UserId}, NotificationId={NotificationId}", userId, notificationId); return true; } /// public async Task MarkAsReadBatchAsync(long userId, List notificationIds) { if (notificationIds == null || !notificationIds.Any()) { return 0; } var markedCount = 0; foreach (var notificationId in notificationIds) { var success = await MarkAsReadAsync(userId, notificationId); if (success) { markedCount++; } } _logger.LogInformation("批量标记通知为已读: UserId={UserId}, MarkedCount={MarkedCount}", userId, markedCount); return markedCount; } /// public async Task MarkAllAsReadAsync(long userId) { // 获取所有已发布的通知 var notifications = await _notificationRepository.GetListAsync(n => n.Status == StatusPublished); // 过滤出用户可见的通知 var visibleNotifications = notifications .Where(n => IsNotificationVisibleToUser(n, userId)) .ToList(); // 获取用户已读的通知ID var readRecords = await _readRepository.GetListAsync(r => r.UserId == userId); var readNotificationIds = readRecords.Select(r => r.NotificationId).ToHashSet(); // 找出未读的通知 var unreadNotifications = visibleNotifications .Where(n => !readNotificationIds.Contains(n.Id)) .ToList(); if (!unreadNotifications.Any()) { return 0; } // 批量创建已读记录 var now = DateTime.Now; var newReadRecords = unreadNotifications.Select(n => new UserNotificationRead { UserId = userId, NotificationId = n.Id, ReadTime = now, CreateTime = now, UpdateTime = now }).ToList(); await _readRepository.AddRangeAsync(newReadRecords); _logger.LogInformation("标记所有通知为已读: UserId={UserId}, MarkedCount={MarkedCount}", userId, newReadRecords.Count); return newReadRecords.Count; } /// public async Task GetUnreadCountAsync(long userId) { // 获取所有已发布的通知 var notifications = await _notificationRepository.GetListAsync(n => n.Status == StatusPublished); // 过滤出用户可见的通知 var visibleNotifications = notifications .Where(n => IsNotificationVisibleToUser(n, userId)) .ToList(); // 获取用户已读的通知ID var readRecords = await _readRepository.GetListAsync(r => r.UserId == userId); var readNotificationIds = readRecords.Select(r => r.NotificationId).ToHashSet(); // 计算未读数量 var unreadCount = visibleNotifications.Count(n => !readNotificationIds.Contains(n.Id)); return unreadCount; } #region 服务号消息推送 /// public async Task SendUnlockNotificationAsync(long targetUserId, long unlockerUserId) { try { var targetUser = await _userRepository.GetByIdAsync(targetUserId); var unlockerUser = await _userRepository.GetByIdAsync(unlockerUserId); if (targetUser == null) { _logger.LogWarning("目标用户不存在: TargetUserId={TargetUserId}", targetUserId); return false; } // 优先使用服务号模板消息 if (targetUser.IsFollowServiceAccount && !string.IsNullOrEmpty(targetUser.ServiceAccountOpenId)) { return await SendServiceAccountTemplateMessageAsync( targetUser.ServiceAccountOpenId, NotificationTemplateType.Unlock, "来访通知", unlockerUser?.Nickname ?? "有人", $"编号{unlockerUser?.XiangQinNo ?? "未知"}刚刚访问了您", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "pages/interact/unlockedMe"); } // 回退到小程序订阅消息 if (!string.IsNullOrEmpty(targetUser.OpenId)) { var request = new SubscribeMessageRequest { ToUser = targetUser.OpenId, TemplateId = GetTemplateId(NotificationTemplateType.Unlock), Page = "pages/interact/unlockedMe", Data = new Dictionary { ["thing1"] = new TemplateDataItem { Value = unlockerUser?.Nickname ?? "有人" }, ["thing2"] = new TemplateDataItem { Value = "解锁了您的联系方式" }, ["time3"] = new TemplateDataItem { Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") } } }; var success = await _weChatService.SendSubscribeMessageAsync(request); _logger.LogInformation("发送解锁通知(订阅消息): TargetUserId={TargetUserId}, Success={Success}", targetUserId, success); return success; } _logger.LogWarning("目标用户无可用通知渠道: TargetUserId={TargetUserId}", targetUserId); return false; } catch (Exception ex) { _logger.LogError(ex, "发送解锁通知异常: TargetUserId={TargetUserId}, UnlockerUserId={UnlockerUserId}", targetUserId, unlockerUserId); return false; } } /// public async Task SendFavoriteNotificationAsync(long targetUserId, long favoriterUserId) { try { var targetUser = await _userRepository.GetByIdAsync(targetUserId); var favoriterUser = await _userRepository.GetByIdAsync(favoriterUserId); if (targetUser == null) { _logger.LogWarning("目标用户不存在: TargetUserId={TargetUserId}", targetUserId); return false; } // 优先使用服务号模板消息 if (targetUser.IsFollowServiceAccount && !string.IsNullOrEmpty(targetUser.ServiceAccountOpenId)) { return await SendServiceAccountTemplateMessageAsync( targetUser.ServiceAccountOpenId, NotificationTemplateType.Favorite, "收藏通知", favoriterUser?.Nickname ?? "有人", $"编号{favoriterUser?.XiangQinNo ?? "未知"}收藏了您", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "pages/interact/favoritedMe"); } // 回退到小程序订阅消息 if (!string.IsNullOrEmpty(targetUser.OpenId)) { var request = new SubscribeMessageRequest { ToUser = targetUser.OpenId, TemplateId = GetTemplateId(NotificationTemplateType.Favorite), Page = "pages/interact/favoritedMe", Data = new Dictionary { ["thing1"] = new TemplateDataItem { Value = favoriterUser?.Nickname ?? "有人" }, ["thing2"] = new TemplateDataItem { Value = "收藏了您" }, ["time3"] = new TemplateDataItem { Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") } } }; var success = await _weChatService.SendSubscribeMessageAsync(request); _logger.LogInformation("发送收藏通知(订阅消息): TargetUserId={TargetUserId}, Success={Success}", targetUserId, success); return success; } _logger.LogWarning("目标用户无可用通知渠道: TargetUserId={TargetUserId}", targetUserId); return false; } catch (Exception ex) { _logger.LogError(ex, "发送收藏通知异常: TargetUserId={TargetUserId}, FavoriterUserId={FavoriterUserId}", targetUserId, favoriterUserId); return false; } } /// public async Task SendFirstMessageNotificationAsync(long targetUserId, long senderUserId) { try { var targetUser = await _userRepository.GetByIdAsync(targetUserId); var senderUser = await _userRepository.GetByIdAsync(senderUserId); if (targetUser == null) { _logger.LogWarning("目标用户不存在: TargetUserId={TargetUserId}", targetUserId); return false; } // 优先使用服务号模板消息 if (targetUser.IsFollowServiceAccount && !string.IsNullOrEmpty(targetUser.ServiceAccountOpenId)) { return await SendServiceAccountTemplateMessageAsync( targetUser.ServiceAccountOpenId, NotificationTemplateType.FirstMessage, "消息通知", senderUser?.Nickname ?? "有人", $"编号{senderUser?.XiangQinNo ?? "未知"}给您发送了一条消息", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "pages/chat/index"); } // 回退到小程序订阅消息 if (!string.IsNullOrEmpty(targetUser.OpenId)) { var request = new SubscribeMessageRequest { ToUser = targetUser.OpenId, TemplateId = GetTemplateId(NotificationTemplateType.FirstMessage), Page = "pages/chat/index", Data = new Dictionary { ["thing1"] = new TemplateDataItem { Value = senderUser?.Nickname ?? "有人" }, ["thing2"] = new TemplateDataItem { Value = "给您发送了一条消息" }, ["time3"] = new TemplateDataItem { Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") } } }; var success = await _weChatService.SendSubscribeMessageAsync(request); _logger.LogInformation("发送首次消息通知(订阅消息): TargetUserId={TargetUserId}, Success={Success}", targetUserId, success); return success; } _logger.LogWarning("目标用户无可用通知渠道: TargetUserId={TargetUserId}", targetUserId); return false; } catch (Exception ex) { _logger.LogError(ex, "发送首次消息通知异常: TargetUserId={TargetUserId}, SenderUserId={SenderUserId}", targetUserId, senderUserId); return false; } } /// public async Task SendMessageReminderAsync(long targetUserId, long senderUserId) { try { var targetUser = await _userRepository.GetByIdAsync(targetUserId); var senderUser = await _userRepository.GetByIdAsync(senderUserId); if (targetUser == null) { _logger.LogWarning("目标用户不存在: TargetUserId={TargetUserId}", targetUserId); return false; } // 优先使用服务号模板消息 if (targetUser.IsFollowServiceAccount && !string.IsNullOrEmpty(targetUser.ServiceAccountOpenId)) { return await SendServiceAccountTemplateMessageAsync( targetUser.ServiceAccountOpenId, NotificationTemplateType.MessageReminder, "消息提醒", senderUser?.Nickname ?? "有人", $"编号{senderUser?.XiangQinNo ?? "未知"}正在等待您的回复", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "pages/chat/index"); } // 回退到小程序订阅消息 if (!string.IsNullOrEmpty(targetUser.OpenId)) { var request = new SubscribeMessageRequest { ToUser = targetUser.OpenId, TemplateId = GetTemplateId(NotificationTemplateType.MessageReminder), Page = "pages/chat/index", Data = new Dictionary { ["thing1"] = new TemplateDataItem { Value = senderUser?.Nickname ?? "有人" }, ["thing2"] = new TemplateDataItem { Value = "正在等待您的回复" }, ["time3"] = new TemplateDataItem { Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") } } }; var success = await _weChatService.SendSubscribeMessageAsync(request); _logger.LogInformation("发送消息提醒(订阅消息): TargetUserId={TargetUserId}, Success={Success}", targetUserId, success); return success; } _logger.LogWarning("目标用户无可用通知渠道: TargetUserId={TargetUserId}", targetUserId); return false; } catch (Exception ex) { _logger.LogError(ex, "发送消息提醒异常: TargetUserId={TargetUserId}, SenderUserId={SenderUserId}", targetUserId, senderUserId); return false; } } /// public async Task SendDailyRecommendNotificationAsync(long userId) { try { var user = await _userRepository.GetByIdAsync(userId); if (user == null) { _logger.LogWarning("用户不存在: UserId={UserId}", userId); return false; } // 优先使用服务号模板消息 if (user.IsFollowServiceAccount && !string.IsNullOrEmpty(user.ServiceAccountOpenId)) { return await SendServiceAccountTemplateMessageAsync( user.ServiceAccountOpenId, NotificationTemplateType.DailyRecommend, "每日推荐", "今日推荐", "您的每日推荐已更新,快来看看吧", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "pages/index/index"); } // 回退到小程序订阅消息 if (!string.IsNullOrEmpty(user.OpenId)) { var request = new SubscribeMessageRequest { ToUser = user.OpenId, TemplateId = GetTemplateId(NotificationTemplateType.DailyRecommend), Page = "pages/index/index", Data = new Dictionary { ["thing1"] = new TemplateDataItem { Value = "今日推荐" }, ["thing2"] = new TemplateDataItem { Value = "您的每日推荐已更新,快来看看吧" }, ["time3"] = new TemplateDataItem { Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") } } }; var success = await _weChatService.SendSubscribeMessageAsync(request); _logger.LogInformation("发送每日推荐通知(订阅消息): UserId={UserId}, Success={Success}", userId, success); return success; } _logger.LogWarning("用户无可用通知渠道: UserId={UserId}", userId); return false; } catch (Exception ex) { _logger.LogError(ex, "发送每日推荐通知异常: UserId={UserId}", userId); return false; } } /// /// 发送服务号模板消息 /// private async Task SendServiceAccountTemplateMessageAsync( string serviceAccountOpenId, NotificationTemplateType templateType, string title, string name, string content, string time, string page) { try { var templateId = await GetServiceAccountTemplateIdAsync(templateType); if (string.IsNullOrEmpty(templateId)) { _logger.LogWarning("服务号模板ID未配置: TemplateType={TemplateType}", templateType); return false; } // 从数据库读取字段映射 var fieldMapping = await GetServiceAccountFieldMappingAsync(templateType); _logger.LogInformation("服务号字段映射: TemplateType={TemplateType}, FieldMapping={FieldMapping}", templateType, fieldMapping != null ? System.Text.Json.JsonSerializer.Serialize(fieldMapping) : "null"); Dictionary data; if (fieldMapping != null && fieldMapping.Count > 0) { // 使用后台配置的字段映射动态构建 data = new Dictionary(); foreach (var (fieldName, valueKey) in fieldMapping) { var value = valueKey?.ToLower() switch { "title" => title, "name" => name, "content" => content, "time" => time, "remark" => "点击查看详情", _ => valueKey ?? "" }; data[fieldName] = new TemplateDataItem { Value = value }; } } else { // 兜底:根据通知类型使用默认字段映射 data = BuildDefaultTemplateData(templateType, title, name, content, time); } var request = new ServiceAccountTemplateMessageRequest { ToUser = serviceAccountOpenId, TemplateId = templateId, MiniProgram = new MiniProgramInfo { AppId = _weChatOptions.MiniProgram.AppId, PagePath = page }, Data = data }; var success = await _weChatService.SendServiceAccountTemplateMessageAsync(request); _logger.LogInformation("发送服务号模板消息: OpenId={OpenId}, TemplateType={TemplateType}, Success={Success}", serviceAccountOpenId, templateType, success); return success; } catch (Exception ex) { _logger.LogError(ex, "发送服务号模板消息异常: OpenId={OpenId}, TemplateType={TemplateType}", serviceAccountOpenId, templateType); return false; } } /// /// 获取服务号模板字段映射 /// private async Task?> GetServiceAccountFieldMappingAsync(NotificationTemplateType templateType) { string? configKey = templateType switch { NotificationTemplateType.Unlock => SystemConfigService.SaUnlockFieldMappingKey, NotificationTemplateType.Favorite => SystemConfigService.SaFavoriteFieldMappingKey, NotificationTemplateType.FirstMessage => SystemConfigService.SaMessageFieldMappingKey, NotificationTemplateType.MessageReminder => SystemConfigService.SaMessageFieldMappingKey, NotificationTemplateType.DailyRecommend => SystemConfigService.SaDailyRecommendFieldMappingKey, _ => null }; if (configKey == null) return null; var json = await _configService.GetConfigValueAsync(configKey); if (string.IsNullOrEmpty(json)) return null; try { return System.Text.Json.JsonSerializer.Deserialize>(json); } catch { _logger.LogWarning("解析字段映射失败: Key={Key}, Value={Value}", configKey, json); return null; } } #endregion #region 私有方法 /// /// 检查通知是否对用户可见 /// /// 通知 /// 用户ID /// 是否可见 public static bool IsNotificationVisibleToUser(SystemNotification notification, long userId) { // 全部用户可见 if (notification.TargetType == TargetTypeAll) { return true; } // 指定用户可见 if (notification.TargetType == TargetTypeSpecific && !string.IsNullOrEmpty(notification.TargetUsers)) { try { var targetUserIds = JsonSerializer.Deserialize>(notification.TargetUsers); return targetUserIds?.Contains(userId) ?? false; } catch { return false; } } return false; } /// /// 映射通知实体到响应DTO /// public static NotificationResponse MapToNotificationResponse( SystemNotification notification, bool isRead, DateTime? readTime) { return new NotificationResponse { Id = notification.Id, Title = notification.Title, Content = notification.Content, IsRead = isRead, ReadTime = readTime, PublishTime = notification.PublishTime, CreateTime = notification.CreateTime }; } /// /// 获取模板ID(小程序订阅消息) /// /// 模板类型 /// 模板ID private string GetTemplateId(NotificationTemplateType templateType) { return templateType switch { NotificationTemplateType.Unlock => _weChatOptions.SubscribeMessage.UnlockTemplateId, NotificationTemplateType.Favorite => _weChatOptions.SubscribeMessage.FavoriteTemplateId, NotificationTemplateType.FirstMessage => _weChatOptions.SubscribeMessage.MessageTemplateId, NotificationTemplateType.MessageReminder => _weChatOptions.SubscribeMessage.MessageTemplateId, NotificationTemplateType.DailyRecommend => _weChatOptions.SubscribeMessage.DailyRecommendTemplateId, _ => string.Empty }; } /// /// 获取服务号模板ID(优先从数据库配置读取,回退到appsettings.json) /// private async Task GetServiceAccountTemplateIdAsync(NotificationTemplateType templateType) { string? configKey = templateType switch { NotificationTemplateType.Unlock => SystemConfigService.SaUnlockTemplateIdKey, NotificationTemplateType.Favorite => SystemConfigService.SaFavoriteTemplateIdKey, NotificationTemplateType.FirstMessage => SystemConfigService.SaMessageTemplateIdKey, NotificationTemplateType.MessageReminder => SystemConfigService.SaMessageTemplateIdKey, NotificationTemplateType.DailyRecommend => SystemConfigService.SaDailyRecommendTemplateIdKey, _ => null }; if (configKey != null) { var dbValue = await _configService.GetConfigValueAsync(configKey); if (!string.IsNullOrEmpty(dbValue)) return dbValue; } return templateType switch { NotificationTemplateType.Unlock => _weChatOptions.ServiceAccount.UnlockTemplateId, NotificationTemplateType.Favorite => _weChatOptions.ServiceAccount.FavoriteTemplateId, NotificationTemplateType.FirstMessage => _weChatOptions.ServiceAccount.MessageTemplateId, NotificationTemplateType.MessageReminder => _weChatOptions.ServiceAccount.MessageTemplateId, NotificationTemplateType.DailyRecommend => _weChatOptions.ServiceAccount.DailyRecommendTemplateId, _ => string.Empty }; } #endregion #region 静态方法(用于属性测试) /// /// 计算未读通知数量(静态方法,用于测试) /// /// 通知列表 /// 已读通知ID集合 /// 用户ID /// 未读数量 public static int CalculateUnreadCount( List notifications, HashSet readNotificationIds, long userId) { return notifications .Where(n => n.Status == StatusPublished && IsNotificationVisibleToUser(n, userId)) .Count(n => !readNotificationIds.Contains(n.Id)); } /// /// 验证标记已读后的状态(静态方法,用于测试) /// /// 标记前的已读记录 /// 通知ID /// 用户ID /// 标记后应该存在的已读记录数 public static int CalculateReadRecordsAfterMark( List readRecordsBefore, long notificationId, long userId) { // 如果已存在,返回原数量(幂等性) if (readRecordsBefore.Any(r => r.UserId == userId && r.NotificationId == notificationId)) { return readRecordsBefore.Count; } // 否则返回原数量+1 return readRecordsBefore.Count + 1; } #endregion } /// /// 通知模板类型 /// public enum NotificationTemplateType { /// /// 解锁通知 /// Unlock = 1, /// /// 收藏通知 /// Favorite = 2, /// /// 首次消息通知 /// FirstMessage = 3, /// /// 消息提醒 /// MessageReminder = 4, /// /// 每日推荐通知 /// DailyRecommend = 5 }