feat: 租户账单公告通知接口

This commit is contained in:
2025-12-03 21:08:28 +08:00
parent 075906266a
commit 9fe7d9606d
47 changed files with 1522 additions and 4 deletions

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 创建公告处理器。
/// </summary>
public sealed class CreateTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository)
: IRequestHandler<CreateTenantAnnouncementCommand, TenantAnnouncementDto>
{
public async Task<TenantAnnouncementDto> Handle(CreateTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
{
throw new BusinessException(ErrorCodes.BadRequest, "公告标题和内容不能为空");
}
var announcement = new TenantAnnouncement
{
TenantId = request.TenantId,
Title = request.Title.Trim(),
Content = request.Content,
AnnouncementType = request.AnnouncementType,
Priority = request.Priority,
EffectiveFrom = request.EffectiveFrom,
EffectiveTo = request.EffectiveTo,
IsActive = request.IsActive
};
await announcementRepository.AddAsync(announcement, cancellationToken);
await announcementRepository.SaveChangesAsync(cancellationToken);
return announcement.ToDto(false, null);
}
}

View File

@@ -0,0 +1,42 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 创建租户账单处理器。
/// </summary>
public sealed class CreateTenantBillingCommandHandler(ITenantBillingRepository billingRepository)
: IRequestHandler<CreateTenantBillingCommand, TenantBillingDto>
{
public async Task<TenantBillingDto> Handle(CreateTenantBillingCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.StatementNo))
{
throw new BusinessException(ErrorCodes.BadRequest, "账单编号不能为空");
}
var bill = new TenantBillingStatement
{
TenantId = request.TenantId,
StatementNo = request.StatementNo.Trim(),
PeriodStart = request.PeriodStart,
PeriodEnd = request.PeriodEnd,
AmountDue = request.AmountDue,
AmountPaid = request.AmountPaid,
Status = request.Status,
DueDate = request.DueDate,
LineItemsJson = request.LineItemsJson
};
await billingRepository.AddAsync(bill, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
return bill.ToDto();
}
}

View File

@@ -0,0 +1,19 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 删除公告处理器。
/// </summary>
public sealed class DeleteTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository)
: IRequestHandler<DeleteTenantAnnouncementCommand, bool>
{
public async Task<bool> Handle(DeleteTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
await announcementRepository.DeleteAsync(request.TenantId, request.AnnouncementId, cancellationToken);
await announcementRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,28 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 公告详情查询处理器。
/// </summary>
public sealed class GetTenantAnnouncementQueryHandler(
ITenantAnnouncementRepository announcementRepository,
ITenantAnnouncementReadRepository readRepository)
: IRequestHandler<GetTenantAnnouncementQuery, TenantAnnouncementDto?>
{
public async Task<TenantAnnouncementDto?> Handle(GetTenantAnnouncementQuery request, CancellationToken cancellationToken)
{
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
if (announcement == null)
{
return null;
}
var reads = await readRepository.GetByAnnouncementAsync(request.TenantId, request.AnnouncementId, cancellationToken);
var readRecord = reads.FirstOrDefault();
return announcement.ToDto(readRecord != null, readRecord?.ReadAt);
}
}

View File

@@ -0,0 +1,19 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 账单详情查询处理器。
/// </summary>
public sealed class GetTenantBillQueryHandler(ITenantBillingRepository billingRepository)
: IRequestHandler<GetTenantBillQuery, TenantBillingDto?>
{
public async Task<TenantBillingDto?> Handle(GetTenantBillQuery request, CancellationToken cancellationToken)
{
var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken);
return bill?.ToDto();
}
}

View File

@@ -0,0 +1,49 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 标记公告已读处理器。
/// </summary>
public sealed class MarkTenantAnnouncementReadCommandHandler(
ITenantAnnouncementRepository announcementRepository,
ITenantAnnouncementReadRepository readRepository,
ICurrentUserAccessor? currentUserAccessor = null)
: IRequestHandler<MarkTenantAnnouncementReadCommand, TenantAnnouncementDto?>
{
public async Task<TenantAnnouncementDto?> Handle(MarkTenantAnnouncementReadCommand request, CancellationToken cancellationToken)
{
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
if (announcement == null)
{
return null;
}
var userId = currentUserAccessor?.UserId ?? 0;
var existing = await readRepository.FindAsync(request.TenantId, request.AnnouncementId, userId == 0 ? null : userId, cancellationToken);
if (existing == null)
{
var record = new TenantAnnouncementRead
{
TenantId = request.TenantId,
AnnouncementId = request.AnnouncementId,
UserId = userId == 0 ? null : userId,
ReadAt = DateTime.UtcNow
};
await readRepository.AddAsync(record, cancellationToken);
await readRepository.SaveChangesAsync(cancellationToken);
existing = record;
}
return announcement.ToDto(true, existing.ReadAt);
}
}

View File

@@ -0,0 +1,32 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 标记账单支付处理器。
/// </summary>
public sealed class MarkTenantBillingPaidCommandHandler(ITenantBillingRepository billingRepository)
: IRequestHandler<MarkTenantBillingPaidCommand, TenantBillingDto?>
{
public async Task<TenantBillingDto?> Handle(MarkTenantBillingPaidCommand request, CancellationToken cancellationToken)
{
var bill = await billingRepository.FindByIdAsync(request.TenantId, request.BillingId, cancellationToken);
if (bill == null)
{
return null;
}
bill.AmountPaid = request.AmountPaid;
bill.Status = TenantBillingStatus.Paid;
bill.DueDate = bill.DueDate;
await billingRepository.UpdateAsync(bill, cancellationToken);
await billingRepository.SaveChangesAsync(cancellationToken);
return bill.ToDto();
}
}

View File

@@ -0,0 +1,31 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Repositories;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 标记通知已读处理器。
/// </summary>
public sealed class MarkTenantNotificationReadCommandHandler(ITenantNotificationRepository notificationRepository)
: IRequestHandler<MarkTenantNotificationReadCommand, TenantNotificationDto?>
{
public async Task<TenantNotificationDto?> Handle(MarkTenantNotificationReadCommand request, CancellationToken cancellationToken)
{
var notification = await notificationRepository.FindByIdAsync(request.TenantId, request.NotificationId, cancellationToken);
if (notification == null)
{
return null;
}
if (notification.ReadAt == null)
{
notification.ReadAt = DateTime.UtcNow;
await notificationRepository.UpdateAsync(notification, cancellationToken);
await notificationRepository.SaveChangesAsync(cancellationToken);
}
return notification.ToDto();
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 公告分页查询处理器。
/// </summary>
public sealed class SearchTenantAnnouncementsQueryHandler(
ITenantAnnouncementRepository announcementRepository,
ITenantAnnouncementReadRepository announcementReadRepository)
: IRequestHandler<SearchTenantAnnouncementsQuery, PagedResult<TenantAnnouncementDto>>
{
public async Task<PagedResult<TenantAnnouncementDto>> Handle(SearchTenantAnnouncementsQuery request, CancellationToken cancellationToken)
{
var effectiveAt = request.OnlyEffective == true ? DateTime.UtcNow : (DateTime?)null;
var announcements = await announcementRepository.SearchAsync(request.TenantId, request.AnnouncementType, request.IsActive, effectiveAt, cancellationToken);
var readMap = new Dictionary<long, (bool isRead, DateTime? readAt)>();
foreach (var announcement in announcements)
{
var reads = await announcementReadRepository.GetByAnnouncementAsync(request.TenantId, announcement.Id, cancellationToken);
var readRecord = reads.FirstOrDefault();
if (readRecord != null)
{
readMap[announcement.Id] = (true, readRecord.ReadAt);
}
}
var ordered = announcements
.OrderByDescending(x => x.Priority)
.ThenByDescending(x => x.CreatedAt)
.ToList();
var page = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var items = ordered
.Skip((page - 1) * size)
.Take(size)
.Select(a =>
{
readMap.TryGetValue(a.Id, out var read);
return a.ToDto(read.isRead, read.readAt);
})
.ToList();
return new PagedResult<TenantAnnouncementDto>(items, page, size, ordered.Count);
}
}

View File

@@ -0,0 +1,27 @@
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 账单分页查询处理器。
/// </summary>
public sealed class SearchTenantBillsQueryHandler(ITenantBillingRepository billingRepository)
: IRequestHandler<SearchTenantBillsQuery, PagedResult<TenantBillingDto>>
{
public async Task<PagedResult<TenantBillingDto>> Handle(SearchTenantBillsQuery request, CancellationToken cancellationToken)
{
var bills = await billingRepository.SearchAsync(request.TenantId, request.Status, request.From, request.To, cancellationToken);
var ordered = bills.OrderByDescending(x => x.PeriodEnd).ToList();
var page = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList();
return new PagedResult<TenantBillingDto>(items, page, size, ordered.Count);
}
}

View File

@@ -0,0 +1,33 @@
using System.Linq;
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Application.App.Tenants.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 通知分页查询处理器。
/// </summary>
public sealed class SearchTenantNotificationsQueryHandler(ITenantNotificationRepository notificationRepository)
: IRequestHandler<SearchTenantNotificationsQuery, PagedResult<TenantNotificationDto>>
{
public async Task<PagedResult<TenantNotificationDto>> Handle(SearchTenantNotificationsQuery request, CancellationToken cancellationToken)
{
var notifications = await notificationRepository.SearchAsync(
request.TenantId,
request.Severity,
request.UnreadOnly,
null,
null,
cancellationToken);
var ordered = notifications.OrderByDescending(x => x.SentAt).ToList();
var page = request.Page <= 0 ? 1 : request.Page;
var size = request.PageSize <= 0 ? 20 : request.PageSize;
var items = ordered.Skip((page - 1) * size).Take(size).Select(x => x.ToDto()).ToList();
return new PagedResult<TenantNotificationDto>(items, page, size, ordered.Count);
}
}

View File

@@ -0,0 +1,42 @@
using MediatR;
using TakeoutSaaS.Application.App.Tenants.Commands;
using TakeoutSaaS.Application.App.Tenants.Dto;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
/// <summary>
/// 更新公告处理器。
/// </summary>
public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRepository announcementRepository)
: IRequestHandler<UpdateTenantAnnouncementCommand, TenantAnnouncementDto?>
{
public async Task<TenantAnnouncementDto?> Handle(UpdateTenantAnnouncementCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Title) || string.IsNullOrWhiteSpace(request.Content))
{
throw new BusinessException(ErrorCodes.BadRequest, "公告标题和内容不能为空");
}
var announcement = await announcementRepository.FindByIdAsync(request.TenantId, request.AnnouncementId, cancellationToken);
if (announcement == null)
{
return null;
}
announcement.Title = request.Title.Trim();
announcement.Content = request.Content;
announcement.AnnouncementType = request.AnnouncementType;
announcement.Priority = request.Priority;
announcement.EffectiveFrom = request.EffectiveFrom;
announcement.EffectiveTo = request.EffectiveTo;
announcement.IsActive = request.IsActive;
await announcementRepository.UpdateAsync(announcement, cancellationToken);
await announcementRepository.SaveChangesAsync(cancellationToken);
return announcement.ToDto(false, null);
}
}