feat: 租户账单公告通知接口
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/announcements")]
|
||||
public sealed class TenantAnnouncementsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询公告。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantAnnouncementDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantAnnouncementDto>>> Search(long tenantId, [FromQuery] SearchTenantAnnouncementsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
query = query with { TenantId = tenantId };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantAnnouncementDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公告详情。
|
||||
/// </summary>
|
||||
[HttpGet("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Detail(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetTenantAnnouncementQuery { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建公告。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-announcement:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Create(long tenantId, [FromBody, Required] CreateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新公告。
|
||||
/// </summary>
|
||||
[HttpPut("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> Update(long tenantId, long announcementId, [FromBody, Required] UpdateTenantAnnouncementCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = tenantId, AnnouncementId = announcementId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除公告。
|
||||
/// </summary>
|
||||
[HttpDelete("{announcementId:long}")]
|
||||
[PermissionAuthorize("tenant-announcement:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<bool>> Delete(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new DeleteTenantAnnouncementCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return ApiResponse<bool>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记公告已读。
|
||||
/// </summary>
|
||||
[HttpPost("{announcementId:long}/read")]
|
||||
[PermissionAuthorize("tenant-announcement:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantAnnouncementDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantAnnouncementDto>> MarkRead(long tenantId, long announcementId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new MarkTenantAnnouncementReadCommand { TenantId = tenantId, AnnouncementId = announcementId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantAnnouncementDto>.Error(StatusCodes.Status404NotFound, "公告不存在")
|
||||
: ApiResponse<TenantAnnouncementDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户账单管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/billings")]
|
||||
public sealed class TenantBillingsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询账单。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantBillingDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantBillingDto>>> Search(long tenantId, [FromQuery] SearchTenantBillsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
query = query with { TenantId = tenantId };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantBillingDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账单详情。
|
||||
/// </summary>
|
||||
[HttpGet("{billingId:long}")]
|
||||
[PermissionAuthorize("tenant-bill:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> Detail(long tenantId, long billingId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetTenantBillQuery { TenantId = tenantId, BillingId = billingId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantBillingDto>.Error(StatusCodes.Status404NotFound, "账单不存在")
|
||||
: ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建账单。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("tenant-bill:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> Create(long tenantId, [FromBody, Required] CreateTenantBillingCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = tenantId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记账单已支付。
|
||||
/// </summary>
|
||||
[HttpPost("{billingId:long}/pay")]
|
||||
[PermissionAuthorize("tenant-bill:pay")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantBillingDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantBillingDto>> MarkPaid(long tenantId, long billingId, [FromBody, Required] MarkTenantBillingPaidCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
command = command with { TenantId = tenantId, BillingId = billingId };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantBillingDto>.Error(StatusCodes.Status404NotFound, "账单不存在")
|
||||
: ApiResponse<TenantBillingDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Application.App.Tenants.Queries;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户通知接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/tenants/{tenantId:long}/notifications")]
|
||||
public sealed class TenantNotificationsController(IMediator mediator) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询通知。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("tenant-notification:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<TenantNotificationDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<TenantNotificationDto>>> Search(long tenantId, [FromQuery] SearchTenantNotificationsQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
query = query with { TenantId = tenantId };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<TenantNotificationDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记通知已读。
|
||||
/// </summary>
|
||||
[HttpPost("{notificationId:long}/read")]
|
||||
[PermissionAuthorize("tenant-notification:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantNotificationDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<TenantNotificationDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<TenantNotificationDto>> MarkRead(long tenantId, long notificationId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new MarkTenantNotificationReadCommand { TenantId = tenantId, NotificationId = notificationId }, cancellationToken);
|
||||
return result is null
|
||||
? ApiResponse<TenantNotificationDto>.Error(StatusCodes.Status404NotFound, "通知不存在")
|
||||
: ApiResponse<TenantNotificationDto>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,15 @@
|
||||
"role-template:create",
|
||||
"role-template:update",
|
||||
"role-template:delete",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
"tenant:read",
|
||||
"tenant:review",
|
||||
@@ -127,6 +136,15 @@
|
||||
"identity:permission:create",
|
||||
"identity:permission:update",
|
||||
"identity:permission:delete",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:read",
|
||||
"tenant:subscription",
|
||||
"tenant:quota:check",
|
||||
@@ -226,6 +244,15 @@
|
||||
"role-template:create",
|
||||
"role-template:update",
|
||||
"role-template:delete",
|
||||
"tenant-bill:read",
|
||||
"tenant-bill:create",
|
||||
"tenant-bill:pay",
|
||||
"tenant-announcement:read",
|
||||
"tenant-announcement:create",
|
||||
"tenant-announcement:update",
|
||||
"tenant-announcement:delete",
|
||||
"tenant-notification:read",
|
||||
"tenant-notification:update",
|
||||
"tenant:create",
|
||||
"tenant:read",
|
||||
"tenant:review",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告。
|
||||
/// </summary>
|
||||
public sealed class TenantAnnouncement : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 公告标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 公告正文(可为 Markdown/HTML,前端自行渲染)。
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 公告类型。
|
||||
/// </summary>
|
||||
public TenantAnnouncementType AnnouncementType { get; set; } = TenantAnnouncementType.System;
|
||||
|
||||
/// <summary>
|
||||
/// 展示优先级,数值越大越靠前。
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 生效时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime EffectiveFrom { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 失效时间(UTC),为空表示长期有效。
|
||||
/// </summary>
|
||||
public DateTime? EffectiveTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告已读记录。
|
||||
/// </summary>
|
||||
public sealed class TenantAnnouncementRead : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 公告 ID。
|
||||
/// </summary>
|
||||
public long AnnouncementId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已读用户 ID(后台账号),为空表示租户级已读。
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已读时间。
|
||||
/// </summary>
|
||||
public DateTime ReadAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告类型。
|
||||
/// </summary>
|
||||
public enum TenantAnnouncementType
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统公告。
|
||||
/// </summary>
|
||||
System = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 账单/订阅相关提醒。
|
||||
/// </summary>
|
||||
Billing = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 运营通知。
|
||||
/// </summary>
|
||||
Operation = 2
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 公告已读仓储。
|
||||
/// </summary>
|
||||
public interface ITenantAnnouncementReadRepository
|
||||
{
|
||||
Task<IReadOnlyList<TenantAnnouncementRead>> GetByAnnouncementAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantAnnouncementRead?> FindAsync(long tenantId, long announcementId, long? userId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(TenantAnnouncementRead record, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户公告仓储。
|
||||
/// </summary>
|
||||
public interface ITenantAnnouncementRepository
|
||||
{
|
||||
Task<IReadOnlyList<TenantAnnouncement>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantAnnouncementType? type,
|
||||
bool? isActive,
|
||||
DateTime? effectiveAt,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantAnnouncement?> FindByIdAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户账单仓储。
|
||||
/// </summary>
|
||||
public interface ITenantBillingRepository
|
||||
{
|
||||
Task<IReadOnlyList<TenantBillingStatement>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantBillingStatus? status,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantBillingStatement?> FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantBillingStatement?> FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户通知仓储。
|
||||
/// </summary>
|
||||
public interface ITenantNotificationRepository
|
||||
{
|
||||
Task<IReadOnlyList<TenantNotification>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantNotificationSeverity? severity,
|
||||
bool? unreadOnly,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TenantNotification?> FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(TenantNotification notification, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateAsync(TenantNotification notification, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -39,6 +39,10 @@ public static class AppServiceCollectionExtensions
|
||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
||||
services.AddScoped<ITenantRepository, EfTenantRepository>();
|
||||
services.AddScoped<ITenantBillingRepository, EfTenantBillingRepository>();
|
||||
services.AddScoped<ITenantAnnouncementRepository, EfTenantAnnouncementRepository>();
|
||||
services.AddScoped<ITenantAnnouncementReadRepository, EfTenantAnnouncementReadRepository>();
|
||||
services.AddScoped<ITenantNotificationRepository, EfTenantNotificationRepository>();
|
||||
services.AddScoped<ITenantPackageRepository, EfTenantPackageRepository>();
|
||||
services.AddScoped<ITenantQuotaUsageRepository, EfTenantQuotaUsageRepository>();
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ public sealed class TakeoutAppDbContext(
|
||||
public DbSet<TenantQuotaUsage> TenantQuotaUsages => Set<TenantQuotaUsage>();
|
||||
public DbSet<TenantBillingStatement> TenantBillingStatements => Set<TenantBillingStatement>();
|
||||
public DbSet<TenantNotification> TenantNotifications => Set<TenantNotification>();
|
||||
public DbSet<TenantAnnouncement> TenantAnnouncements => Set<TenantAnnouncement>();
|
||||
public DbSet<TenantAnnouncementRead> TenantAnnouncementReads => Set<TenantAnnouncementRead>();
|
||||
public DbSet<TenantVerificationProfile> TenantVerificationProfiles => Set<TenantVerificationProfile>();
|
||||
public DbSet<TenantAuditLog> TenantAuditLogs => Set<TenantAuditLog>();
|
||||
|
||||
@@ -141,6 +143,8 @@ public sealed class TakeoutAppDbContext(
|
||||
ConfigureTenantQuotaUsage(modelBuilder.Entity<TenantQuotaUsage>());
|
||||
ConfigureTenantBilling(modelBuilder.Entity<TenantBillingStatement>());
|
||||
ConfigureTenantNotification(modelBuilder.Entity<TenantNotification>());
|
||||
ConfigureTenantAnnouncement(modelBuilder.Entity<TenantAnnouncement>());
|
||||
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
|
||||
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
|
||||
ConfigureTenantAuditLog(modelBuilder.Entity<TenantAuditLog>());
|
||||
ConfigureMerchantDocument(modelBuilder.Entity<MerchantDocument>());
|
||||
@@ -465,6 +469,35 @@ public sealed class TakeoutAppDbContext(
|
||||
builder.HasIndex(x => new { x.TenantId, x.Channel, x.SentAt });
|
||||
}
|
||||
|
||||
private static void ConfigureTenantAnnouncement(EntityTypeBuilder<TenantAnnouncement> builder)
|
||||
{
|
||||
builder.ToTable("tenant_announcements");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
|
||||
builder.Property(x => x.Content).HasColumnType("text").IsRequired();
|
||||
builder.Property(x => x.AnnouncementType).HasConversion<int>();
|
||||
builder.Property(x => x.Priority).IsRequired();
|
||||
builder.Property(x => x.IsActive).IsRequired();
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => new { x.TenantId, x.AnnouncementType, x.IsActive });
|
||||
builder.HasIndex(x => new { x.TenantId, x.EffectiveFrom, x.EffectiveTo });
|
||||
}
|
||||
|
||||
private static void ConfigureTenantAnnouncementRead(EntityTypeBuilder<TenantAnnouncementRead> builder)
|
||||
{
|
||||
builder.ToTable("tenant_announcement_reads");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.AnnouncementId).IsRequired();
|
||||
builder.Property(x => x.UserId);
|
||||
builder.Property(x => x.ReadAt).IsRequired();
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => new { x.TenantId, x.AnnouncementId, x.UserId }).IsUnique();
|
||||
}
|
||||
|
||||
private static void ConfigureMerchantDocument(EntityTypeBuilder<MerchantDocument> builder)
|
||||
{
|
||||
builder.ToTable("merchant_documents");
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EF 公告已读仓储。
|
||||
/// </summary>
|
||||
public sealed class EfTenantAnnouncementReadRepository(TakeoutAppDbContext context) : ITenantAnnouncementReadRepository
|
||||
{
|
||||
public Task<IReadOnlyList<TenantAnnouncementRead>> GetByAnnouncementAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantAnnouncementReads.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.AnnouncementId == announcementId)
|
||||
.OrderBy(x => x.ReadAt)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<TenantAnnouncementRead>)t.Result, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantAnnouncementRead?> FindAsync(long tenantId, long announcementId, long? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantAnnouncementReads
|
||||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.AnnouncementId == announcementId && x.UserId == userId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddAsync(TenantAnnouncementRead record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantAnnouncementReads.AddAsync(record, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EF 租户公告仓储。
|
||||
/// </summary>
|
||||
public sealed class EfTenantAnnouncementRepository(TakeoutAppDbContext context) : ITenantAnnouncementRepository
|
||||
{
|
||||
public Task<IReadOnlyList<TenantAnnouncement>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantAnnouncementType? type,
|
||||
bool? isActive,
|
||||
DateTime? effectiveAt,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.TenantAnnouncements.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (type.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AnnouncementType == type.Value);
|
||||
}
|
||||
|
||||
if (isActive.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsActive == isActive.Value);
|
||||
}
|
||||
|
||||
if (effectiveAt.HasValue)
|
||||
{
|
||||
var at = effectiveAt.Value;
|
||||
query = query.Where(x => x.EffectiveFrom <= at && (x.EffectiveTo == null || x.EffectiveTo >= at));
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderByDescending(x => x.Priority)
|
||||
.ThenByDescending(x => x.CreatedAt)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<TenantAnnouncement>)t.Result, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantAnnouncement?> FindByIdAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantAnnouncements.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == announcementId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantAnnouncements.AddAsync(announcement, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public Task UpdateAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantAnnouncements.Update(announcement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await context.TenantAnnouncements.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == announcementId, cancellationToken);
|
||||
if (entity != null)
|
||||
{
|
||||
context.TenantAnnouncements.Remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EF 租户账单仓储。
|
||||
/// </summary>
|
||||
public sealed class EfTenantBillingRepository(TakeoutAppDbContext context) : ITenantBillingRepository
|
||||
{
|
||||
public Task<IReadOnlyList<TenantBillingStatement>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantBillingStatus? status,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.TenantBillingStatements.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == status.Value);
|
||||
}
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.PeriodStart >= from.Value);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.PeriodEnd <= to.Value);
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderByDescending(x => x.PeriodEnd)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<TenantBillingStatement>)t.Result, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantBillingStatement?> FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantBillingStatements.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == billingId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantBillingStatement?> FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantBillingStatements.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StatementNo == statementNo, cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantBillingStatements.AddAsync(bill, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public Task UpdateAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantBillingStatements.Update(bill);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Tenants.Entities;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// EF 租户通知仓储。
|
||||
/// </summary>
|
||||
public sealed class EfTenantNotificationRepository(TakeoutAppDbContext context) : ITenantNotificationRepository
|
||||
{
|
||||
public Task<IReadOnlyList<TenantNotification>> SearchAsync(
|
||||
long tenantId,
|
||||
TenantNotificationSeverity? severity,
|
||||
bool? unreadOnly,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.TenantNotifications.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (severity.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Severity == severity.Value);
|
||||
}
|
||||
|
||||
if (unreadOnly == true)
|
||||
{
|
||||
query = query.Where(x => x.ReadAt == null);
|
||||
}
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt >= from.Value);
|
||||
}
|
||||
|
||||
if (to.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt <= to.Value);
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<TenantNotification>)t.Result, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantNotification?> FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantNotifications
|
||||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == notificationId, cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddAsync(TenantNotification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantNotifications.AddAsync(notification, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public Task UpdateAsync(TenantNotification notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantNotifications.Update(notification);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user