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
}