- NotificationsController.cs with 5 endpoints:
* GET /synergy/notifications - list with unreadOnly filter
* GET /synergy/notifications/unread-count - badge count
* POST /synergy/notifications/mark-read - single or bulk
* POST /synergy/notifications/send - player or broadcast
* DELETE /synergy/notifications/{id} - delete by player
- Notification entity added to RR3DbContext (FK to Users, cascade delete)
- NotificationDto, NotificationsResponse, MarkReadRequest,
SendNotificationRequest models added to ApiModels.cs
- Migration AddNotificationsTable applied
- Build: 0 errors, 0 warnings
- All 5 endpoints tested and working
Server now: 75/73 core endpoints (notifications + admin delete = 76)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
258 lines
10 KiB
C#
258 lines
10 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RR3CommunityServer.Data;
|
|
using RR3CommunityServer.Models;
|
|
|
|
namespace RR3CommunityServer.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("synergy/notifications")]
|
|
public class NotificationsController : ControllerBase
|
|
{
|
|
private readonly RR3DbContext _context;
|
|
private readonly ILogger<NotificationsController> _logger;
|
|
|
|
public NotificationsController(RR3DbContext context, ILogger<NotificationsController> logger)
|
|
{
|
|
_context = context;
|
|
_logger = logger;
|
|
}
|
|
|
|
// GET /synergy/notifications?synergyId=xxx&unreadOnly=false&limit=50
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetNotifications(
|
|
[FromQuery] string synergyId,
|
|
[FromQuery] bool unreadOnly = false,
|
|
[FromQuery] int limit = 50)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(synergyId))
|
|
return BadRequest(new SynergyResponse<object> { resultCode = -1, message = "synergyId required" });
|
|
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
|
|
if (user == null)
|
|
return NotFound(new SynergyResponse<object> { resultCode = -404, message = "Player not found" });
|
|
|
|
var now = DateTime.UtcNow;
|
|
var query = _context.Notifications
|
|
.Where(n => n.UserId == user.Id && (n.ExpiresAt == null || n.ExpiresAt > now));
|
|
|
|
if (unreadOnly)
|
|
query = query.Where(n => !n.IsRead);
|
|
|
|
var notifications = await query
|
|
.OrderByDescending(n => n.CreatedAt)
|
|
.Take(limit)
|
|
.ToListAsync();
|
|
|
|
var unreadCount = await _context.Notifications
|
|
.CountAsync(n => n.UserId == user.Id && !n.IsRead && (n.ExpiresAt == null || n.ExpiresAt > now));
|
|
|
|
var dtos = notifications.Select(n => new NotificationDto
|
|
{
|
|
Id = n.Id,
|
|
Type = n.Type,
|
|
Title = n.Title,
|
|
Message = n.Message,
|
|
IsRead = n.IsRead,
|
|
CreatedAt = new DateTimeOffset(n.CreatedAt).ToUnixTimeSeconds(),
|
|
ExpiresAt = n.ExpiresAt.HasValue ? new DateTimeOffset(n.ExpiresAt.Value).ToUnixTimeSeconds() : null
|
|
}).ToList();
|
|
|
|
_logger.LogInformation("Retrieved {Count} notifications for {SynergyId}", dtos.Count, synergyId);
|
|
|
|
return Ok(new SynergyResponse<NotificationsResponse>
|
|
{
|
|
resultCode = 0,
|
|
data = new NotificationsResponse
|
|
{
|
|
Notifications = dtos,
|
|
TotalCount = dtos.Count,
|
|
UnreadCount = unreadCount
|
|
}
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting notifications for {SynergyId}", synergyId);
|
|
return StatusCode(500, new SynergyResponse<object> { resultCode = -500, message = "Internal server error" });
|
|
}
|
|
}
|
|
|
|
// GET /synergy/notifications/unread-count?synergyId=xxx
|
|
[HttpGet("unread-count")]
|
|
public async Task<IActionResult> GetUnreadCount([FromQuery] string synergyId)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(synergyId))
|
|
return BadRequest(new SynergyResponse<object> { resultCode = -1, message = "synergyId required" });
|
|
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
|
|
if (user == null)
|
|
return Ok(new SynergyResponse<UnreadCountResponse> { resultCode = 0, data = new UnreadCountResponse { UnreadCount = 0 } });
|
|
|
|
var now = DateTime.UtcNow;
|
|
var count = await _context.Notifications
|
|
.CountAsync(n => n.UserId == user.Id && !n.IsRead && (n.ExpiresAt == null || n.ExpiresAt > now));
|
|
|
|
return Ok(new SynergyResponse<UnreadCountResponse>
|
|
{
|
|
resultCode = 0,
|
|
data = new UnreadCountResponse { UnreadCount = count }
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error getting unread count for {SynergyId}", synergyId);
|
|
return StatusCode(500, new SynergyResponse<object> { resultCode = -500, message = "Internal server error" });
|
|
}
|
|
}
|
|
|
|
// POST /synergy/notifications/mark-read
|
|
[HttpPost("mark-read")]
|
|
public async Task<IActionResult> MarkRead([FromBody] MarkReadRequest request)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(request.SynergyId))
|
|
return BadRequest(new SynergyResponse<object> { resultCode = -1, message = "synergyId required" });
|
|
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId);
|
|
if (user == null)
|
|
return NotFound(new SynergyResponse<object> { resultCode = -404, message = "Player not found" });
|
|
|
|
IQueryable<Notification> query = _context.Notifications.Where(n => n.UserId == user.Id && !n.IsRead);
|
|
|
|
// If specific IDs provided, only mark those; otherwise mark all
|
|
if (request.NotificationIds != null && request.NotificationIds.Count > 0)
|
|
query = query.Where(n => request.NotificationIds.Contains(n.Id));
|
|
|
|
var notifications = await query.ToListAsync();
|
|
foreach (var n in notifications)
|
|
n.IsRead = true;
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Marked {Count} notifications read for {SynergyId}", notifications.Count, request.SynergyId);
|
|
|
|
return Ok(new SynergyResponse<object>
|
|
{
|
|
resultCode = 0,
|
|
message = $"Marked {notifications.Count} notification(s) as read"
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error marking notifications read for {SynergyId}", request.SynergyId);
|
|
return StatusCode(500, new SynergyResponse<object> { resultCode = -500, message = "Internal server error" });
|
|
}
|
|
}
|
|
|
|
// POST /synergy/notifications/send (admin/system use)
|
|
[HttpPost("send")]
|
|
public async Task<IActionResult> SendNotification([FromBody] SendNotificationRequest request)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(request.Title) || string.IsNullOrEmpty(request.Message))
|
|
return BadRequest(new SynergyResponse<object> { resultCode = -1, message = "title and message required" });
|
|
|
|
DateTime? expiresAt = request.ExpiresInHours.HasValue
|
|
? DateTime.UtcNow.AddHours(request.ExpiresInHours.Value)
|
|
: null;
|
|
|
|
int sentCount = 0;
|
|
|
|
if (string.IsNullOrEmpty(request.SynergyId))
|
|
{
|
|
// Broadcast to all users
|
|
var allUsers = await _context.Users.Select(u => u.Id).ToListAsync();
|
|
var bulk = allUsers.Select(uid => new Notification
|
|
{
|
|
UserId = uid,
|
|
Type = request.Type,
|
|
Title = request.Title,
|
|
Message = request.Message,
|
|
CreatedAt = DateTime.UtcNow,
|
|
ExpiresAt = expiresAt
|
|
}).ToList();
|
|
|
|
_context.Notifications.AddRange(bulk);
|
|
sentCount = bulk.Count;
|
|
}
|
|
else
|
|
{
|
|
// Send to specific player
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId);
|
|
if (user == null)
|
|
return NotFound(new SynergyResponse<object> { resultCode = -404, message = "Player not found" });
|
|
|
|
_context.Notifications.Add(new Notification
|
|
{
|
|
UserId = user.Id,
|
|
Type = request.Type,
|
|
Title = request.Title,
|
|
Message = request.Message,
|
|
CreatedAt = DateTime.UtcNow,
|
|
ExpiresAt = expiresAt
|
|
});
|
|
sentCount = 1;
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Sent notification '{Title}' to {Count} player(s)", request.Title, sentCount);
|
|
|
|
return Ok(new SynergyResponse<object>
|
|
{
|
|
resultCode = 0,
|
|
message = $"Notification sent to {sentCount} player(s)"
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error sending notification");
|
|
return StatusCode(500, new SynergyResponse<object> { resultCode = -500, message = "Internal server error" });
|
|
}
|
|
}
|
|
|
|
// DELETE /synergy/notifications/{id}?synergyId=xxx
|
|
[HttpDelete("{id:int}")]
|
|
public async Task<IActionResult> DeleteNotification(int id, [FromQuery] string synergyId)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(synergyId))
|
|
return BadRequest(new SynergyResponse<object> { resultCode = -1, message = "synergyId required" });
|
|
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId);
|
|
if (user == null)
|
|
return NotFound(new SynergyResponse<object> { resultCode = -404, message = "Player not found" });
|
|
|
|
var notification = await _context.Notifications
|
|
.FirstOrDefaultAsync(n => n.Id == id && n.UserId == user.Id);
|
|
|
|
if (notification == null)
|
|
return NotFound(new SynergyResponse<object> { resultCode = -404, message = "Notification not found" });
|
|
|
|
_context.Notifications.Remove(notification);
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Deleted notification {Id} for {SynergyId}", id, synergyId);
|
|
|
|
return Ok(new SynergyResponse<object>
|
|
{
|
|
resultCode = 0,
|
|
message = "Notification deleted"
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error deleting notification {Id}", id);
|
|
return StatusCode(500, new SynergyResponse<object> { resultCode = -500, message = "Internal server error" });
|
|
}
|
|
}
|
|
}
|