feat: 添加推送通知功能,支持订阅和发送通知
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 40s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s

This commit is contained in:
2026-01-02 12:25:44 +08:00
parent a055e43451
commit e250a7df2f
17 changed files with 382 additions and 13 deletions

View File

@@ -0,0 +1,8 @@
namespace Service.AppSettingModel;
public class NotificationSettings
{
public string Subject { get; set; } = string.Empty;
public string PublicKey { get; set; } = string.Empty;
public string PrivateKey { get; set; } = string.Empty;
}

View File

@@ -11,4 +11,5 @@ global using FreeSql;
global using System.Linq;
global using Service.AppSettingModel;
global using System.Text.Json.Serialization;
global using System.Text.Json.Nodes;
global using System.Text.Json.Nodes;
global using Microsoft.Extensions.Configuration;

View File

@@ -12,7 +12,7 @@ public interface IMessageRecordService
Task<long> GetUnreadCountAsync();
}
public class MessageRecordService(IMessageRecordRepository messageRepo) : IMessageRecordService
public class MessageRecordService(IMessageRecordRepository messageRepo, INotificationService notificationService) : IMessageRecordService
{
public async Task<(IEnumerable<MessageRecord> List, long Total)> GetPagedListAsync(int pageIndex, int pageSize)
{
@@ -37,7 +37,12 @@ public class MessageRecordService(IMessageRecordRepository messageRepo) : IMessa
Content = content,
IsRead = false
};
return await messageRepo.AddAsync(message);
var result = await messageRepo.AddAsync(message);
if (result)
{
await notificationService.SendNotificationAsync($"新增的账单通知: {title}");
}
return result;
}
public async Task<bool> MarkAsReadAsync(long id)

View File

@@ -0,0 +1,93 @@
using WebPush;
namespace Service;
public interface INotificationService
{
Task<string> GetVapidPublicKeyAsync();
Task SubscribeAsync(PushSubscriptionEntity subscription);
Task SendNotificationAsync(string message, string? url = null);
}
public class NotificationService(
IPushSubscriptionRepository subscriptionRepo,
IConfiguration configuration,
ILogger<NotificationService> logger) : INotificationService
{
private NotificationSettings GetSettings()
{
var settings = configuration.GetSection("NotificationSettings").Get<NotificationSettings>();
if (settings == null)
{
// Fallback or throw. For now, let's return empty to avoid crashing if not configured,
// but logging error is better.
logger.LogWarning("NotificationSettings not configured");
return new NotificationSettings();
}
return settings;
}
public Task<string> GetVapidPublicKeyAsync()
{
return Task.FromResult(GetSettings().PublicKey);
}
public async Task SubscribeAsync(PushSubscriptionEntity subscription)
{
var existing = await subscriptionRepo.GetByEndpointAsync(subscription.Endpoint);
if (existing != null)
{
existing.P256DH = subscription.P256DH;
existing.Auth = subscription.Auth;
existing.UpdateTime = DateTime.Now;
await subscriptionRepo.UpdateAsync(existing);
}
else
{
await subscriptionRepo.AddAsync(subscription);
}
}
public async Task SendNotificationAsync(string message, string? url = null)
{
var settings = GetSettings();
if (string.IsNullOrEmpty(settings.PublicKey) || string.IsNullOrEmpty(settings.PrivateKey))
{
logger.LogWarning("VAPID keys not configured, skipping notification");
return;
}
var vapidDetails = new VapidDetails(settings.Subject, settings.PublicKey, settings.PrivateKey);
var webPushClient = new WebPushClient();
var subscriptions = await subscriptionRepo.GetAllAsync();
var payload = System.Text.Json.JsonSerializer.Serialize(new
{
title = "System Notification",
body = message,
url = url ?? "/",
icon = "/pwa-192x192.png"
});
foreach (var sub in subscriptions)
{
try
{
var pushSubscription = new PushSubscription(sub.Endpoint, sub.P256DH, sub.Auth);
await webPushClient.SendNotificationAsync(pushSubscription, payload, vapidDetails);
}
catch (WebPushException ex)
{
if (ex.StatusCode == System.Net.HttpStatusCode.Gone || ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
await subscriptionRepo.DeleteAsync(sub.Id);
}
logger.LogError(ex, "Error sending push notification to {Endpoint}", sub.Endpoint);
}
catch (Exception ex)
{
logger.LogError(ex, "Error sending push notification to {Endpoint}", sub.Endpoint);
}
}
}
}

View File

@@ -21,6 +21,7 @@
<PackageReference Include="Quartz.Extensions.Hosting" />
<PackageReference Include="JiebaNet.Analyser" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="WebPush" />
</ItemGroup>
</Project>