815 lines
31 KiB
C#
815 lines
31 KiB
C#
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
|
||
}
|