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,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;
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

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);
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
};
}