xiangyixiangqin/server/src/XiangYi.Application/Services/NotificationService.cs
zpc b1daa6c6c8
Some checks are pending
continuous-integration/drone/push Build is running
21
2026-03-29 21:36:21 +08:00

815 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 通知服务实现
/// </summary>
public class NotificationService : INotificationService
{
private readonly IRepository<SystemNotification> _notificationRepository;
private readonly IRepository<UserNotificationRead> _readRepository;
private readonly IRepository<User> _userRepository;
private readonly IWeChatService _weChatService;
private readonly WeChatOptions _weChatOptions;
private readonly ISystemConfigService _configService;
private readonly ILogger<NotificationService> _logger;
/// <summary>
/// 通知状态:草稿
/// </summary>
public const int StatusDraft = 1;
/// <summary>
/// 通知状态:已发布
/// </summary>
public const int StatusPublished = 2;
/// <summary>
/// 目标类型:全部用户
/// </summary>
public const int TargetTypeAll = 1;
/// <summary>
/// 目标类型:指定用户
/// </summary>
public const int TargetTypeSpecific = 2;
public NotificationService(
IRepository<SystemNotification> notificationRepository,
IRepository<UserNotificationRead> readRepository,
IRepository<User> userRepository,
IWeChatService weChatService,
IOptions<WeChatOptions> weChatOptions,
ISystemConfigService configService,
ILogger<NotificationService> logger)
{
_notificationRepository = notificationRepository;
_readRepository = readRepository;
_userRepository = userRepository;
_weChatService = weChatService;
_weChatOptions = weChatOptions.Value;
_configService = configService;
_logger = logger;
}
/// <inheritdoc />
public async Task<PagedResult<NotificationResponse>> 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<NotificationResponse>
{
Items = responses,
Total = total,
PageIndex = pageIndex,
PageSize = pageSize
};
}
/// <inheritdoc />
public async Task<bool> 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;
}
/// <inheritdoc />
public async Task<int> MarkAsReadBatchAsync(long userId, List<long> 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;
}
/// <inheritdoc />
public async Task<int> 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;
}
/// <inheritdoc />
public async Task<int> 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
/// <inheritdoc />
public async Task<bool> 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<string, TemplateDataItem>
{
["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;
}
}
/// <inheritdoc />
public async Task<bool> 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<string, TemplateDataItem>
{
["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;
}
}
/// <inheritdoc />
public async Task<bool> 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<string, TemplateDataItem>
{
["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;
}
}
/// <inheritdoc />
public async Task<bool> 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<string, TemplateDataItem>
{
["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;
}
}
/// <inheritdoc />
public async Task<bool> 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<string, TemplateDataItem>
{
["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;
}
}
/// <summary>
/// 发送服务号模板消息
/// </summary>
private async Task<bool> 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<string, TemplateDataItem> data;
if (fieldMapping != null && fieldMapping.Count > 0)
{
// 使用后台配置的字段映射动态构建
data = new Dictionary<string, TemplateDataItem>();
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;
}
}
/// <summary>
/// 获取服务号模板字段映射
/// </summary>
private async Task<Dictionary<string, string>?> 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<Dictionary<string, string>>(json);
}
catch
{
_logger.LogWarning("解析字段映射失败: Key={Key}, Value={Value}", configKey, json);
return null;
}
}
#endregion
#region
/// <summary>
/// 检查通知是否对用户可见
/// </summary>
/// <param name="notification">通知</param>
/// <param name="userId">用户ID</param>
/// <returns>是否可见</returns>
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<List<long>>(notification.TargetUsers);
return targetUserIds?.Contains(userId) ?? false;
}
catch
{
return false;
}
}
return false;
}
/// <summary>
/// 映射通知实体到响应DTO
/// </summary>
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
};
}
/// <summary>
/// 获取模板ID小程序订阅消息
/// </summary>
/// <param name="templateType">模板类型</param>
/// <returns>模板ID</returns>
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
};
}
/// <summary>
/// 获取服务号模板ID优先从数据库配置读取回退到appsettings.json
/// </summary>
private async Task<string> 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
/// <summary>
/// 计算未读通知数量(静态方法,用于测试)
/// </summary>
/// <param name="notifications">通知列表</param>
/// <param name="readNotificationIds">已读通知ID集合</param>
/// <param name="userId">用户ID</param>
/// <returns>未读数量</returns>
public static int CalculateUnreadCount(
List<SystemNotification> notifications,
HashSet<long> readNotificationIds,
long userId)
{
return notifications
.Where(n => n.Status == StatusPublished && IsNotificationVisibleToUser(n, userId))
.Count(n => !readNotificationIds.Contains(n.Id));
}
/// <summary>
/// 验证标记已读后的状态(静态方法,用于测试)
/// </summary>
/// <param name="readRecordsBefore">标记前的已读记录</param>
/// <param name="notificationId">通知ID</param>
/// <param name="userId">用户ID</param>
/// <returns>标记后应该存在的已读记录数</returns>
public static int CalculateReadRecordsAfterMark(
List<UserNotificationRead> readRecordsBefore,
long notificationId,
long userId)
{
// 如果已存在,返回原数量(幂等性)
if (readRecordsBefore.Any(r => r.UserId == userId && r.NotificationId == notificationId))
{
return readRecordsBefore.Count;
}
// 否则返回原数量+1
return readRecordsBefore.Count + 1;
}
#endregion
}
/// <summary>
/// 通知模板类型
/// </summary>
public enum NotificationTemplateType
{
/// <summary>
/// 解锁通知
/// </summary>
Unlock = 1,
/// <summary>
/// 收藏通知
/// </summary>
Favorite = 2,
/// <summary>
/// 首次消息通知
/// </summary>
FirstMessage = 3,
/// <summary>
/// 消息提醒
/// </summary>
MessageReminder = 4,
/// <summary>
/// 每日推荐通知
/// </summary>
DailyRecommend = 5
}