feat: 租户账单公告通知接口
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建租户公告命令。
|
||||
/// </summary>
|
||||
public sealed record CreateTenantAnnouncementCommand : IRequest<TenantAnnouncementDto>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Content { get; init; } = string.Empty;
|
||||
public TenantAnnouncementType AnnouncementType { get; init; } = TenantAnnouncementType.System;
|
||||
public int Priority { get; init; } = 0;
|
||||
public DateTime EffectiveFrom { get; init; } = DateTime.UtcNow;
|
||||
public DateTime? EffectiveTo { get; init; }
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 创建租户账单命令。
|
||||
/// </summary>
|
||||
public sealed record CreateTenantBillingCommand : IRequest<TenantBillingDto>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public string StatementNo { get; init; } = string.Empty;
|
||||
public DateTime PeriodStart { get; init; }
|
||||
public DateTime PeriodEnd { get; init; }
|
||||
public decimal AmountDue { get; init; }
|
||||
public decimal AmountPaid { get; init; }
|
||||
public TenantBillingStatus Status { get; init; } = TenantBillingStatus.Pending;
|
||||
public DateTime DueDate { get; init; }
|
||||
public string? LineItemsJson { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除租户公告命令。
|
||||
/// </summary>
|
||||
public sealed record DeleteTenantAnnouncementCommand : IRequest<bool>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long AnnouncementId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读命令。
|
||||
/// </summary>
|
||||
public sealed record MarkTenantAnnouncementReadCommand : IRequest<TenantAnnouncementDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long AnnouncementId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 标记租户账单已支付命令。
|
||||
/// </summary>
|
||||
public sealed record MarkTenantBillingPaidCommand : IRequest<TenantBillingDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long BillingId { get; init; }
|
||||
public decimal AmountPaid { get; init; }
|
||||
public DateTime PaidAt { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 标记通知已读命令。
|
||||
/// </summary>
|
||||
public sealed record MarkTenantNotificationReadCommand : IRequest<TenantNotificationDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long NotificationId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新租户公告命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateTenantAnnouncementCommand : IRequest<TenantAnnouncementDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long AnnouncementId { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Content { get; init; } = string.Empty;
|
||||
public TenantAnnouncementType AnnouncementType { get; init; } = TenantAnnouncementType.System;
|
||||
public int Priority { get; init; } = 0;
|
||||
public DateTime EffectiveFrom { get; init; } = DateTime.UtcNow;
|
||||
public DateTime? EffectiveTo { get; init; }
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告 DTO。
|
||||
/// </summary>
|
||||
public sealed class TenantAnnouncementDto
|
||||
{
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
public TenantAnnouncementType AnnouncementType { get; init; }
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public DateTime EffectiveFrom { get; init; }
|
||||
|
||||
public DateTime? EffectiveTo { get; init; }
|
||||
|
||||
public bool IsActive { get; init; }
|
||||
|
||||
public bool IsRead { get; init; }
|
||||
|
||||
public DateTime? ReadAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 租户账单 DTO。
|
||||
/// </summary>
|
||||
public sealed class TenantBillingDto
|
||||
{
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
public string StatementNo { get; init; } = string.Empty;
|
||||
|
||||
public DateTime PeriodStart { get; init; }
|
||||
|
||||
public DateTime PeriodEnd { get; init; }
|
||||
|
||||
public decimal AmountDue { get; init; }
|
||||
|
||||
public decimal AmountPaid { get; init; }
|
||||
|
||||
public TenantBillingStatus Status { get; init; }
|
||||
|
||||
public DateTime DueDate { get; init; }
|
||||
|
||||
public string? LineItemsJson { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 租户通知 DTO。
|
||||
/// </summary>
|
||||
public sealed class TenantNotificationDto
|
||||
{
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public TenantNotificationChannel Channel { get; init; }
|
||||
|
||||
public TenantNotificationSeverity Severity { get; init; }
|
||||
|
||||
public DateTime SentAt { get; init; }
|
||||
|
||||
public DateTime? ReadAt { get; init; }
|
||||
|
||||
public string? MetadataJson { get; init; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 公告详情查询。
|
||||
/// </summary>
|
||||
public sealed record GetTenantAnnouncementQuery : IRequest<TenantAnnouncementDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long AnnouncementId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取账单详情查询。
|
||||
/// </summary>
|
||||
public sealed record GetTenantBillQuery : IRequest<TenantBillingDto?>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public long BillingId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询租户公告。
|
||||
/// </summary>
|
||||
public sealed record SearchTenantAnnouncementsQuery : IRequest<PagedResult<TenantAnnouncementDto>>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public TenantAnnouncementType? AnnouncementType { get; init; }
|
||||
public bool? IsActive { get; init; }
|
||||
public bool? OnlyEffective { get; init; }
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询租户账单。
|
||||
/// </summary>
|
||||
public sealed record SearchTenantBillsQuery : IRequest<PagedResult<TenantBillingDto>>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public TenantBillingStatus? Status { get; init; }
|
||||
public DateTime? From { get; init; }
|
||||
public DateTime? To { get; init; }
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询租户通知。
|
||||
/// </summary>
|
||||
public sealed record SearchTenantNotificationsQuery : IRequest<PagedResult<TenantNotificationDto>>
|
||||
{
|
||||
public long TenantId { get; init; }
|
||||
public TenantNotificationSeverity? Severity { get; init; }
|
||||
public bool? UnreadOnly { get; init; }
|
||||
public int Page { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -91,4 +91,49 @@ internal static class TenantMapping
|
||||
FeaturePoliciesJson = package.FeaturePoliciesJson,
|
||||
IsActive = package.IsActive
|
||||
};
|
||||
|
||||
public static TenantBillingDto ToDto(this TenantBillingStatement bill)
|
||||
=> new()
|
||||
{
|
||||
Id = bill.Id,
|
||||
TenantId = bill.TenantId,
|
||||
StatementNo = bill.StatementNo,
|
||||
PeriodStart = bill.PeriodStart,
|
||||
PeriodEnd = bill.PeriodEnd,
|
||||
AmountDue = bill.AmountDue,
|
||||
AmountPaid = bill.AmountPaid,
|
||||
Status = bill.Status,
|
||||
DueDate = bill.DueDate,
|
||||
LineItemsJson = bill.LineItemsJson
|
||||
};
|
||||
|
||||
public static TenantAnnouncementDto ToDto(this TenantAnnouncement announcement, bool isRead, DateTime? readAt)
|
||||
=> new()
|
||||
{
|
||||
Id = announcement.Id,
|
||||
TenantId = announcement.TenantId,
|
||||
Title = announcement.Title,
|
||||
Content = announcement.Content,
|
||||
AnnouncementType = announcement.AnnouncementType,
|
||||
Priority = announcement.Priority,
|
||||
EffectiveFrom = announcement.EffectiveFrom,
|
||||
EffectiveTo = announcement.EffectiveTo,
|
||||
IsActive = announcement.IsActive,
|
||||
IsRead = isRead,
|
||||
ReadAt = readAt
|
||||
};
|
||||
|
||||
public static TenantNotificationDto ToDto(this TenantNotification notification)
|
||||
=> new()
|
||||
{
|
||||
Id = notification.Id,
|
||||
TenantId = notification.TenantId,
|
||||
Title = notification.Title,
|
||||
Message = notification.Message,
|
||||
Channel = notification.Channel,
|
||||
Severity = notification.Severity,
|
||||
SentAt = notification.SentAt,
|
||||
ReadAt = notification.ReadAt,
|
||||
MetadataJson = notification.MetadataJson
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user