完成门店管理后端接口与任务
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过命令。
|
||||
/// </summary>
|
||||
public sealed record ApproveStoreCommand : IRequest<StoreAuditActionResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 强制关闭门店命令。
|
||||
/// </summary>
|
||||
public sealed record ForceCloseStoreCommand : IRequest<StoreAuditActionResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关闭原因。
|
||||
/// </summary>
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 审核驳回命令。
|
||||
/// </summary>
|
||||
public sealed record RejectStoreCommand : IRequest<StoreAuditActionResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回原因 ID。
|
||||
/// </summary>
|
||||
public long RejectionReasonId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回原因补充说明。
|
||||
/// </summary>
|
||||
public string? RejectionReasonText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 解除强制关闭命令。
|
||||
/// </summary>
|
||||
public sealed record ReopenStoreCommand : IRequest<StoreAuditActionResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 待审核门店 DTO。
|
||||
/// </summary>
|
||||
public sealed record PendingStoreAuditDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店名称。
|
||||
/// </summary>
|
||||
public string StoreName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 门店编码。
|
||||
/// </summary>
|
||||
public string StoreCode { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户名称。
|
||||
/// </summary>
|
||||
public string TenantName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long MerchantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户名称。
|
||||
/// </summary>
|
||||
public string MerchantName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 门头招牌图。
|
||||
/// </summary>
|
||||
public string? SignboardImageUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 完整地址。
|
||||
/// </summary>
|
||||
public string FullAddress { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 主体类型。
|
||||
/// </summary>
|
||||
public StoreOwnershipType OwnershipType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交时间。
|
||||
/// </summary>
|
||||
public DateTime? SubmittedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 等待天数。
|
||||
/// </summary>
|
||||
public int WaitingDays { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否超时。
|
||||
/// </summary>
|
||||
public bool IsOverdue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 资质数量。
|
||||
/// </summary>
|
||||
public int QualificationCount { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 审核/风控操作结果 DTO。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditActionResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核状态。
|
||||
/// </summary>
|
||||
public StoreAuditStatus AuditStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经营状态。
|
||||
/// </summary>
|
||||
public StoreBusinessStatus BusinessStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回原因。
|
||||
/// </summary>
|
||||
public string? RejectionReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示信息。
|
||||
/// </summary>
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 审核统计趋势项。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditDailyTrendDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 日期。
|
||||
/// </summary>
|
||||
public DateOnly Date { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交数量。
|
||||
/// </summary>
|
||||
public int Submitted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 通过数量。
|
||||
/// </summary>
|
||||
public int Approved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回数量。
|
||||
/// </summary>
|
||||
public int Rejected { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核详情 DTO。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店信息。
|
||||
/// </summary>
|
||||
public StoreAuditStoreDto Store { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 租户信息。
|
||||
/// </summary>
|
||||
public StoreAuditTenantDto Tenant { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 商户信息。
|
||||
/// </summary>
|
||||
public StoreAuditMerchantDto Merchant { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 资质列表。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreQualificationDto> Qualifications { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 审核记录。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreAuditRecordDto> AuditHistory { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核详情 - 商户信息。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditMerchantDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 法人或主体名称。
|
||||
/// </summary>
|
||||
public string? LegalName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 统一社会信用代码。
|
||||
/// </summary>
|
||||
public string? CreditCode { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核记录 DTO。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditRecordDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核动作。
|
||||
/// </summary>
|
||||
public StoreAuditAction Action { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 动作名称。
|
||||
/// </summary>
|
||||
public string ActionName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 操作人 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long? OperatorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人名称。
|
||||
/// </summary>
|
||||
public string OperatorName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 操作前状态。
|
||||
/// </summary>
|
||||
public StoreAuditStatus? PreviousStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作后状态。
|
||||
/// </summary>
|
||||
public StoreAuditStatus NewStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回理由 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long? RejectionReasonId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 驳回理由。
|
||||
/// </summary>
|
||||
public string? RejectionReasonText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 审核统计 DTO。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditStatisticsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 待审核数量。
|
||||
/// </summary>
|
||||
public int PendingCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时数量。
|
||||
/// </summary>
|
||||
public int OverdueCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过数量。
|
||||
/// </summary>
|
||||
public int ApprovedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核驳回数量。
|
||||
/// </summary>
|
||||
public int RejectedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 平均处理时长(小时)。
|
||||
/// </summary>
|
||||
public double AvgProcessingHours { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 每日趋势。
|
||||
/// </summary>
|
||||
public IReadOnlyList<StoreAuditDailyTrendDto> DailyTrend { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核详情 - 门店信息。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditStoreDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 门店编码。
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? Phone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 门头招牌图。
|
||||
/// </summary>
|
||||
public string? SignboardImageUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 省份。
|
||||
/// </summary>
|
||||
public string? Province { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 城市。
|
||||
/// </summary>
|
||||
public string? City { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 区县。
|
||||
/// </summary>
|
||||
public string? District { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 详细地址。
|
||||
/// </summary>
|
||||
public string? Address { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经度。
|
||||
/// </summary>
|
||||
public double? Longitude { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 纬度。
|
||||
/// </summary>
|
||||
public double? Latitude { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 主体类型。
|
||||
/// </summary>
|
||||
public StoreOwnershipType OwnershipType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核状态。
|
||||
/// </summary>
|
||||
public StoreAuditStatus AuditStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交时间。
|
||||
/// </summary>
|
||||
public DateTime? SubmittedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核详情 - 租户信息。
|
||||
/// </summary>
|
||||
public sealed record StoreAuditTenantDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 联系人。
|
||||
/// </summary>
|
||||
public string? ContactName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string? ContactPhone { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过处理器。
|
||||
/// </summary>
|
||||
public sealed class ApproveStoreCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IDapperExecutor dapperExecutor,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
ILogger<ApproveStoreCommandHandler> logger)
|
||||
: IRequestHandler<ApproveStoreCommand, StoreAuditActionResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditActionResultDto> Handle(ApproveStoreCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取门店快照
|
||||
var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
|
||||
if (!snapshot.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 1.1 (空行后) 校验审核状态
|
||||
if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取门店实体
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 3. (空行后) 更新状态并记录审核
|
||||
var previousStatus = store.AuditStatus;
|
||||
var now = DateTime.UtcNow;
|
||||
store.AuditStatus = StoreAuditStatus.Activated;
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.ActivatedAt ??= now;
|
||||
store.RejectionReason = null;
|
||||
store.ClosureReason = null;
|
||||
store.ClosureReasonText = null;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = StoreAuditAction.Approve,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
Remarks = request.Remark
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 审核通过", store.Id);
|
||||
|
||||
// 4. (空行后) 返回结果
|
||||
return new StoreAuditActionResultDto
|
||||
{
|
||||
StoreId = store.Id,
|
||||
AuditStatus = store.AuditStatus,
|
||||
BusinessStatus = store.BusinessStatus,
|
||||
Message = "门店已激活"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店基础字段
|
||||
return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildStoreSnapshotSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
// 1.1 (空行后) 执行查询
|
||||
await using var reader = await command.ExecuteReaderAsync(token);
|
||||
if (!await reader.ReadAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1.2 (空行后) 返回快照
|
||||
return (
|
||||
reader.GetInt64(reader.GetOrdinal("TenantId")),
|
||||
(StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
|
||||
(StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private long? ResolveOperatorId()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? null : id;
|
||||
}
|
||||
|
||||
private string ResolveOperatorName()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? "system" : $"user:{id}";
|
||||
}
|
||||
|
||||
private static string BuildStoreSnapshotSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
s."TenantId",
|
||||
s."AuditStatus",
|
||||
s."BusinessStatus"
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."Id" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 强制关闭门店处理器。
|
||||
/// </summary>
|
||||
public sealed class ForceCloseStoreCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IDapperExecutor dapperExecutor,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
ILogger<ForceCloseStoreCommandHandler> logger)
|
||||
: IRequestHandler<ForceCloseStoreCommand, StoreAuditActionResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditActionResultDto> Handle(ForceCloseStoreCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取门店快照
|
||||
var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
|
||||
if (!snapshot.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 1.1 (空行后) 校验审核与经营状态
|
||||
if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法强制关闭");
|
||||
}
|
||||
|
||||
if (snapshot.Value.BusinessStatus == StoreBusinessStatus.ForceClosed)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店已处于强制关闭状态");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取门店实体
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 3. (空行后) 更新状态并记录风控
|
||||
var now = DateTime.UtcNow;
|
||||
store.BusinessStatus = StoreBusinessStatus.ForceClosed;
|
||||
store.ClosureReason = StoreClosureReason.PlatformSuspended;
|
||||
store.ClosureReasonText = request.Reason;
|
||||
store.ForceCloseReason = request.Reason;
|
||||
store.ForceClosedAt = now;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = StoreAuditAction.ForceClose,
|
||||
PreviousStatus = store.AuditStatus,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
Remarks = request.Remark
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 强制关闭", store.Id);
|
||||
|
||||
// 4. (空行后) 返回结果
|
||||
return new StoreAuditActionResultDto
|
||||
{
|
||||
StoreId = store.Id,
|
||||
AuditStatus = store.AuditStatus,
|
||||
BusinessStatus = store.BusinessStatus,
|
||||
Message = "门店已强制关闭"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店基础字段
|
||||
return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildStoreSnapshotSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
// 1.1 (空行后) 执行查询
|
||||
await using var reader = await command.ExecuteReaderAsync(token);
|
||||
if (!await reader.ReadAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1.2 (空行后) 返回快照
|
||||
return (
|
||||
reader.GetInt64(reader.GetOrdinal("TenantId")),
|
||||
(StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
|
||||
(StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private long? ResolveOperatorId()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? null : id;
|
||||
}
|
||||
|
||||
private string ResolveOperatorName()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? "system" : $"user:{id}";
|
||||
}
|
||||
|
||||
private static string BuildStoreSnapshotSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
s."TenantId",
|
||||
s."AuditStatus",
|
||||
s."BusinessStatus"
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."Id" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreAuditDetailQueryHandler(
|
||||
IDapperExecutor dapperExecutor)
|
||||
: IRequestHandler<GetStoreAuditDetailQuery, StoreAuditDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditDetailDto?> Handle(GetStoreAuditDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店与主体信息
|
||||
return await dapperExecutor.QueryAsync(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
// 1.1 查询门店基础信息
|
||||
await using var storeCommand = CreateCommand(
|
||||
connection,
|
||||
BuildStoreSql(),
|
||||
[
|
||||
("storeId", request.StoreId)
|
||||
]);
|
||||
|
||||
await using var reader = await storeCommand.ExecuteReaderAsync(token);
|
||||
if (!await reader.ReadAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1.2 (空行后) 映射门店信息
|
||||
var store = new StoreAuditStoreDto
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("StoreId")),
|
||||
Name = reader.GetString(reader.GetOrdinal("StoreName")),
|
||||
Code = reader.GetString(reader.GetOrdinal("StoreCode")),
|
||||
Phone = reader.IsDBNull(reader.GetOrdinal("Phone")) ? null : reader.GetString(reader.GetOrdinal("Phone")),
|
||||
SignboardImageUrl = reader.IsDBNull(reader.GetOrdinal("SignboardImageUrl"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("SignboardImageUrl")),
|
||||
Province = reader.IsDBNull(reader.GetOrdinal("Province")) ? null : reader.GetString(reader.GetOrdinal("Province")),
|
||||
City = reader.IsDBNull(reader.GetOrdinal("City")) ? null : reader.GetString(reader.GetOrdinal("City")),
|
||||
District = reader.IsDBNull(reader.GetOrdinal("District")) ? null : reader.GetString(reader.GetOrdinal("District")),
|
||||
Address = reader.IsDBNull(reader.GetOrdinal("Address")) ? null : reader.GetString(reader.GetOrdinal("Address")),
|
||||
Longitude = reader.IsDBNull(reader.GetOrdinal("Longitude")) ? null : reader.GetDouble(reader.GetOrdinal("Longitude")),
|
||||
Latitude = reader.IsDBNull(reader.GetOrdinal("Latitude")) ? null : reader.GetDouble(reader.GetOrdinal("Latitude")),
|
||||
OwnershipType = (StoreOwnershipType)reader.GetInt32(reader.GetOrdinal("OwnershipType")),
|
||||
AuditStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
|
||||
SubmittedAt = reader.IsDBNull(reader.GetOrdinal("SubmittedAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("SubmittedAt"))
|
||||
};
|
||||
|
||||
// 1.3 (空行后) 映射租户信息
|
||||
var tenant = new StoreAuditTenantDto
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("TenantId")),
|
||||
Name = reader.GetString(reader.GetOrdinal("TenantName")),
|
||||
ContactName = reader.IsDBNull(reader.GetOrdinal("TenantContactName"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("TenantContactName")),
|
||||
ContactPhone = reader.IsDBNull(reader.GetOrdinal("TenantContactPhone"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("TenantContactPhone"))
|
||||
};
|
||||
|
||||
// 1.4 (空行后) 映射商户信息
|
||||
var merchant = new StoreAuditMerchantDto
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("MerchantId")),
|
||||
Name = reader.GetString(reader.GetOrdinal("MerchantName")),
|
||||
LegalName = reader.IsDBNull(reader.GetOrdinal("MerchantLegalName"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("MerchantLegalName")),
|
||||
CreditCode = reader.IsDBNull(reader.GetOrdinal("MerchantCreditCode"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("MerchantCreditCode"))
|
||||
};
|
||||
|
||||
// 2. (空行后) 查询资质列表
|
||||
var qualifications = await QueryQualificationsAsync(connection, request.StoreId, token);
|
||||
|
||||
// 3. (空行后) 查询审核记录
|
||||
var auditHistory = await QueryAuditRecordsAsync(connection, request.StoreId, token);
|
||||
|
||||
// 4. (空行后) 组装结果
|
||||
return new StoreAuditDetailDto
|
||||
{
|
||||
Store = store,
|
||||
Tenant = tenant,
|
||||
Merchant = merchant,
|
||||
Qualifications = qualifications,
|
||||
AuditHistory = auditHistory
|
||||
};
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<StoreQualificationDto>> QueryQualificationsAsync(
|
||||
IDbConnection connection,
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店资质
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildQualificationSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
var items = new List<StoreQualificationDto>();
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
// 2. (空行后) 映射资质 DTO
|
||||
var now = DateTime.UtcNow.Date;
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
DateTime? expiresAt = reader.IsDBNull(reader.GetOrdinal("ExpiresAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("ExpiresAt"));
|
||||
int? daysUntilExpiry = expiresAt.HasValue
|
||||
? (int)Math.Ceiling((expiresAt.Value.Date - now).TotalDays)
|
||||
: null;
|
||||
var isExpired = expiresAt.HasValue && expiresAt.Value < DateTime.UtcNow;
|
||||
var isExpiringSoon = expiresAt.HasValue
|
||||
&& expiresAt.Value >= DateTime.UtcNow
|
||||
&& expiresAt.Value <= DateTime.UtcNow.AddDays(30);
|
||||
|
||||
// 2.1 (空行后) 写入列表
|
||||
items.Add(new StoreQualificationDto
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("Id")),
|
||||
StoreId = reader.GetInt64(reader.GetOrdinal("StoreId")),
|
||||
QualificationType = (StoreQualificationType)reader.GetInt32(reader.GetOrdinal("QualificationType")),
|
||||
FileUrl = reader.GetString(reader.GetOrdinal("FileUrl")),
|
||||
DocumentNumber = reader.IsDBNull(reader.GetOrdinal("DocumentNumber"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("DocumentNumber")),
|
||||
IssuedAt = reader.IsDBNull(reader.GetOrdinal("IssuedAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("IssuedAt")),
|
||||
ExpiresAt = expiresAt,
|
||||
IsExpired = isExpired,
|
||||
IsExpiringSoon = isExpiringSoon,
|
||||
DaysUntilExpiry = daysUntilExpiry,
|
||||
SortOrder = reader.GetInt32(reader.GetOrdinal("SortOrder")),
|
||||
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
|
||||
UpdatedAt = reader.IsDBNull(reader.GetOrdinal("UpdatedAt"))
|
||||
? null
|
||||
: reader.GetDateTime(reader.GetOrdinal("UpdatedAt"))
|
||||
});
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return items;
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<StoreAuditRecordDto>> QueryAuditRecordsAsync(
|
||||
IDbConnection connection,
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询审核记录
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildAuditRecordSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
var items = new List<StoreAuditRecordDto>();
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
// 2. (空行后) 映射审核记录 DTO
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var action = (StoreAuditAction)reader.GetInt32(reader.GetOrdinal("Action"));
|
||||
items.Add(new StoreAuditRecordDto
|
||||
{
|
||||
Id = reader.GetInt64(reader.GetOrdinal("Id")),
|
||||
Action = action,
|
||||
ActionName = StoreAuditActionNameResolver.Resolve(action),
|
||||
OperatorId = reader.IsDBNull(reader.GetOrdinal("OperatorId"))
|
||||
? null
|
||||
: reader.GetInt64(reader.GetOrdinal("OperatorId")),
|
||||
OperatorName = reader.GetString(reader.GetOrdinal("OperatorName")),
|
||||
PreviousStatus = reader.IsDBNull(reader.GetOrdinal("PreviousStatus"))
|
||||
? null
|
||||
: (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("PreviousStatus")),
|
||||
NewStatus = (StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("NewStatus")),
|
||||
RejectionReasonId = reader.IsDBNull(reader.GetOrdinal("RejectionReasonId"))
|
||||
? null
|
||||
: reader.GetInt64(reader.GetOrdinal("RejectionReasonId")),
|
||||
RejectionReasonText = reader.IsDBNull(reader.GetOrdinal("RejectionReason"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("RejectionReason")),
|
||||
Remark = reader.IsDBNull(reader.GetOrdinal("Remarks"))
|
||||
? null
|
||||
: reader.GetString(reader.GetOrdinal("Remarks")),
|
||||
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
|
||||
});
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回结果
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string BuildStoreSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
s."Id" as "StoreId",
|
||||
s."Name" as "StoreName",
|
||||
s."Code" as "StoreCode",
|
||||
s."Phone",
|
||||
s."SignboardImageUrl",
|
||||
s."Province",
|
||||
s."City",
|
||||
s."District",
|
||||
s."Address",
|
||||
s."Longitude",
|
||||
s."Latitude",
|
||||
s."OwnershipType",
|
||||
s."AuditStatus",
|
||||
s."SubmittedAt",
|
||||
s."TenantId",
|
||||
t."Name" as "TenantName",
|
||||
t."ContactName" as "TenantContactName",
|
||||
t."ContactPhone" as "TenantContactPhone",
|
||||
s."MerchantId",
|
||||
m."BrandName" as "MerchantName",
|
||||
m."LegalPerson" as "MerchantLegalName",
|
||||
m."TaxNumber" as "MerchantCreditCode"
|
||||
from public.stores s
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
|
||||
where s."DeletedAt" is null
|
||||
and s."Id" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildQualificationSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
q."Id",
|
||||
q."StoreId",
|
||||
q."QualificationType",
|
||||
q."FileUrl",
|
||||
q."DocumentNumber",
|
||||
q."IssuedAt",
|
||||
q."ExpiresAt",
|
||||
q."SortOrder",
|
||||
q."CreatedAt",
|
||||
q."UpdatedAt"
|
||||
from public.store_qualifications q
|
||||
where q."DeletedAt" is null
|
||||
and q."StoreId" = @storeId
|
||||
order by q."SortOrder", q."QualificationType";
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildAuditRecordSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
r."Id",
|
||||
r."Action",
|
||||
r."OperatorId",
|
||||
r."OperatorName",
|
||||
r."PreviousStatus",
|
||||
r."NewStatus",
|
||||
r."RejectionReasonId",
|
||||
r."RejectionReason",
|
||||
r."Remarks",
|
||||
r."CreatedAt"
|
||||
from public.store_audit_records r
|
||||
where r."DeletedAt" is null
|
||||
and r."StoreId" = @storeId
|
||||
order by r."CreatedAt" desc;
|
||||
""";
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 审核统计查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreAuditStatisticsQueryHandler(
|
||||
IDapperExecutor dapperExecutor)
|
||||
: IRequestHandler<GetStoreAuditStatisticsQuery, StoreAuditStatisticsDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditStatisticsDto> Handle(GetStoreAuditStatisticsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 规范化日期范围
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var dateFrom = request.DateFrom?.Date ?? today.AddDays(-30);
|
||||
var dateTo = request.DateTo?.Date ?? today;
|
||||
if (dateFrom > dateTo)
|
||||
{
|
||||
(dateFrom, dateTo) = (dateTo, dateFrom);
|
||||
}
|
||||
|
||||
// 1.1 (空行后) 计算统计边界
|
||||
var dateToExclusive = dateTo.AddDays(1);
|
||||
var overdueDeadline = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
// 2. (空行后) 查询统计
|
||||
return await dapperExecutor.QueryAsync(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
// 2.1 读取待审核与超时数量
|
||||
var pendingCount = await ExecuteScalarIntAsync(
|
||||
connection,
|
||||
BuildPendingCountSql(),
|
||||
[
|
||||
("pendingStatus", (int)StoreAuditStatus.Pending)
|
||||
],
|
||||
token);
|
||||
var overdueCount = await ExecuteScalarIntAsync(
|
||||
connection,
|
||||
BuildOverdueCountSql(),
|
||||
[
|
||||
("pendingStatus", (int)StoreAuditStatus.Pending),
|
||||
("overdueDeadline", overdueDeadline)
|
||||
],
|
||||
token);
|
||||
|
||||
// 2.2 (空行后) 读取通过/驳回数量
|
||||
var (approvedCount, rejectedCount) = await QueryApproveRejectCountsAsync(
|
||||
connection,
|
||||
dateFrom,
|
||||
dateToExclusive,
|
||||
token);
|
||||
|
||||
// 2.3 (空行后) 读取平均处理时长
|
||||
var avgProcessingHours = await ExecuteScalarDoubleAsync(
|
||||
connection,
|
||||
BuildAvgProcessingSql(),
|
||||
[
|
||||
("dateFrom", dateFrom),
|
||||
("dateTo", dateToExclusive),
|
||||
("approveAction", (int)StoreAuditAction.Approve),
|
||||
("rejectAction", (int)StoreAuditAction.Reject)
|
||||
],
|
||||
token);
|
||||
|
||||
// 2.4 (空行后) 读取每日趋势
|
||||
var dailyTrend = await QueryDailyTrendAsync(
|
||||
connection,
|
||||
dateFrom,
|
||||
dateToExclusive,
|
||||
token);
|
||||
|
||||
// 2.5 (空行后) 组装结果
|
||||
return new StoreAuditStatisticsDto
|
||||
{
|
||||
PendingCount = pendingCount,
|
||||
OverdueCount = overdueCount,
|
||||
ApprovedCount = approvedCount,
|
||||
RejectedCount = rejectedCount,
|
||||
AvgProcessingHours = avgProcessingHours,
|
||||
DailyTrend = dailyTrend
|
||||
};
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<(int ApprovedCount, int RejectedCount)> QueryApproveRejectCountsAsync(
|
||||
IDbConnection connection,
|
||||
DateTime dateFrom,
|
||||
DateTime dateTo,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询通过/驳回统计
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildApproveRejectCountSql(),
|
||||
[
|
||||
("dateFrom", dateFrom),
|
||||
("dateTo", dateTo),
|
||||
("approveAction", (int)StoreAuditAction.Approve),
|
||||
("rejectAction", (int)StoreAuditAction.Reject)
|
||||
]);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// 2. (空行后) 返回统计
|
||||
var approved = reader.IsDBNull(reader.GetOrdinal("ApprovedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("ApprovedCount"));
|
||||
var rejected = reader.IsDBNull(reader.GetOrdinal("RejectedCount")) ? 0 : reader.GetInt32(reader.GetOrdinal("RejectedCount"));
|
||||
return (approved, rejected);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<StoreAuditDailyTrendDto>> QueryDailyTrendAsync(
|
||||
IDbConnection connection,
|
||||
DateTime dateFrom,
|
||||
DateTime dateTo,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询每日趋势
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildDailyTrendSql(),
|
||||
[
|
||||
("dateFrom", dateFrom),
|
||||
("dateTo", dateTo),
|
||||
("submitAction", (int)StoreAuditAction.Submit),
|
||||
("resubmitAction", (int)StoreAuditAction.Resubmit),
|
||||
("approveAction", (int)StoreAuditAction.Approve),
|
||||
("rejectAction", (int)StoreAuditAction.Reject)
|
||||
]);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
var items = new List<StoreAuditDailyTrendDto>();
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
// 2. (空行后) 映射趋势项
|
||||
var dateOrdinal = reader.GetOrdinal("Date");
|
||||
var submittedOrdinal = reader.GetOrdinal("SubmittedCount");
|
||||
var approvedOrdinal = reader.GetOrdinal("ApprovedCount");
|
||||
var rejectedOrdinal = reader.GetOrdinal("RejectedCount");
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
var date = reader.GetDateTime(dateOrdinal);
|
||||
items.Add(new StoreAuditDailyTrendDto
|
||||
{
|
||||
Date = DateOnly.FromDateTime(date),
|
||||
Submitted = reader.GetInt32(submittedOrdinal),
|
||||
Approved = reader.GetInt32(approvedOrdinal),
|
||||
Rejected = reader.GetInt32(rejectedOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回趋势列表
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string BuildPendingCountSql()
|
||||
{
|
||||
return """
|
||||
select count(*)
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."AuditStatus" = @pendingStatus;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildOverdueCountSql()
|
||||
{
|
||||
return """
|
||||
select count(*)
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."AuditStatus" = @pendingStatus
|
||||
and s."SubmittedAt" <= @overdueDeadline;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildApproveRejectCountSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount",
|
||||
sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount"
|
||||
from public.store_audit_records r
|
||||
where r."DeletedAt" is null
|
||||
and r."CreatedAt" >= @dateFrom
|
||||
and r."CreatedAt" < @dateTo
|
||||
and r."Action" in (@approveAction, @rejectAction);
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildAvgProcessingSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
avg(extract(epoch from (r."CreatedAt" - s."SubmittedAt")) / 3600.0) as "AvgHours"
|
||||
from public.store_audit_records r
|
||||
join public.stores s on s."Id" = r."StoreId" and s."DeletedAt" is null
|
||||
where r."DeletedAt" is null
|
||||
and r."Action" in (@approveAction, @rejectAction)
|
||||
and r."CreatedAt" >= @dateFrom
|
||||
and r."CreatedAt" < @dateTo
|
||||
and s."SubmittedAt" is not null;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildDailyTrendSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
date_trunc('day', r."CreatedAt") as "Date",
|
||||
sum(case when r."Action" in (@submitAction, @resubmitAction) then 1 else 0 end) as "SubmittedCount",
|
||||
sum(case when r."Action" = @approveAction then 1 else 0 end) as "ApprovedCount",
|
||||
sum(case when r."Action" = @rejectAction then 1 else 0 end) as "RejectedCount"
|
||||
from public.store_audit_records r
|
||||
where r."DeletedAt" is null
|
||||
and r."CreatedAt" >= @dateFrom
|
||||
and r."CreatedAt" < @dateTo
|
||||
group by date_trunc('day', r."CreatedAt")
|
||||
order by date_trunc('day', r."CreatedAt");
|
||||
""";
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteScalarIntAsync(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(connection, sql, parameters);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken);
|
||||
return result is null or DBNull ? 0 : Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static async Task<double> ExecuteScalarDoubleAsync(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(connection, sql, parameters);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken);
|
||||
return result is null or DBNull ? 0d : Convert.ToDouble(result);
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 待审核门店列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListPendingStoreAuditsQueryHandler(
|
||||
IDapperExecutor dapperExecutor)
|
||||
: IRequestHandler<ListPendingStoreAuditsQuery, PagedResult<PendingStoreAuditDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<PendingStoreAuditDto>> Handle(ListPendingStoreAuditsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 参数规范化
|
||||
var page = request.Page <= 0 ? 1 : request.Page;
|
||||
var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
|
||||
var keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim();
|
||||
var offset = (page - 1) * pageSize;
|
||||
var now = DateTime.UtcNow;
|
||||
var overdueDeadline = now.AddDays(-7);
|
||||
|
||||
// 2. (空行后) 排序白名单
|
||||
var orderBy = request.SortBy?.Trim() switch
|
||||
{
|
||||
"StoreName" => "s.\"Name\"",
|
||||
"MerchantName" => "m.\"BrandName\"",
|
||||
"TenantName" => "t.\"Name\"",
|
||||
"SubmittedAt" => "s.\"SubmittedAt\"",
|
||||
_ => "s.\"SubmittedAt\""
|
||||
};
|
||||
|
||||
// 3. (空行后) 执行查询
|
||||
return await dapperExecutor.QueryAsync(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
// 3.1 统计总数
|
||||
var total = await ExecuteScalarIntAsync(
|
||||
connection,
|
||||
BuildCountSql(),
|
||||
[
|
||||
("tenantId", request.TenantId),
|
||||
("keyword", keyword),
|
||||
("submittedFrom", request.SubmittedFrom),
|
||||
("submittedTo", request.SubmittedTo),
|
||||
("overdueOnly", request.OverdueOnly),
|
||||
("overdueDeadline", overdueDeadline),
|
||||
("pendingStatus", (int)StoreAuditStatus.Pending)
|
||||
],
|
||||
token);
|
||||
|
||||
// 3.2 (空行后) 查询列表
|
||||
var listSql = BuildListSql(orderBy, request.SortDesc);
|
||||
await using var listCommand = CreateCommand(
|
||||
connection,
|
||||
listSql,
|
||||
[
|
||||
("tenantId", request.TenantId),
|
||||
("keyword", keyword),
|
||||
("submittedFrom", request.SubmittedFrom),
|
||||
("submittedTo", request.SubmittedTo),
|
||||
("overdueOnly", request.OverdueOnly),
|
||||
("overdueDeadline", overdueDeadline),
|
||||
("pendingStatus", (int)StoreAuditStatus.Pending),
|
||||
("offset", offset),
|
||||
("limit", pageSize)
|
||||
]);
|
||||
|
||||
await using var reader = await listCommand.ExecuteReaderAsync(token);
|
||||
|
||||
// 3.3 (空行后) 读取并映射
|
||||
var items = new List<PendingStoreAuditDto>();
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return new PagedResult<PendingStoreAuditDto>(items, page, pageSize, total);
|
||||
}
|
||||
|
||||
// 3.3.1 (空行后) 初始化字段序号
|
||||
var storeIdOrdinal = reader.GetOrdinal("StoreId");
|
||||
var storeNameOrdinal = reader.GetOrdinal("StoreName");
|
||||
var storeCodeOrdinal = reader.GetOrdinal("StoreCode");
|
||||
var tenantIdOrdinal = reader.GetOrdinal("TenantId");
|
||||
var tenantNameOrdinal = reader.GetOrdinal("TenantName");
|
||||
var merchantIdOrdinal = reader.GetOrdinal("MerchantId");
|
||||
var merchantNameOrdinal = reader.GetOrdinal("MerchantName");
|
||||
var signboardOrdinal = reader.GetOrdinal("SignboardImageUrl");
|
||||
var provinceOrdinal = reader.GetOrdinal("Province");
|
||||
var cityOrdinal = reader.GetOrdinal("City");
|
||||
var districtOrdinal = reader.GetOrdinal("District");
|
||||
var addressOrdinal = reader.GetOrdinal("Address");
|
||||
var ownershipOrdinal = reader.GetOrdinal("OwnershipType");
|
||||
var submittedAtOrdinal = reader.GetOrdinal("SubmittedAt");
|
||||
var qualificationCountOrdinal = reader.GetOrdinal("QualificationCount");
|
||||
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
DateTime? submittedAt = reader.IsDBNull(submittedAtOrdinal)
|
||||
? null
|
||||
: reader.GetDateTime(submittedAtOrdinal);
|
||||
var waitingDays = submittedAt.HasValue
|
||||
? (int)Math.Floor((now - submittedAt.Value).TotalDays)
|
||||
: 0;
|
||||
if (waitingDays < 0)
|
||||
{
|
||||
waitingDays = 0;
|
||||
}
|
||||
|
||||
// 3.3.2 (空行后) 组装地址信息
|
||||
var province = reader.IsDBNull(provinceOrdinal) ? null : reader.GetString(provinceOrdinal);
|
||||
var city = reader.IsDBNull(cityOrdinal) ? null : reader.GetString(cityOrdinal);
|
||||
var district = reader.IsDBNull(districtOrdinal) ? null : reader.GetString(districtOrdinal);
|
||||
var address = reader.IsDBNull(addressOrdinal) ? null : reader.GetString(addressOrdinal);
|
||||
var fullAddress = string.Concat(
|
||||
province ?? string.Empty,
|
||||
city ?? string.Empty,
|
||||
district ?? string.Empty,
|
||||
address ?? string.Empty);
|
||||
|
||||
items.Add(new PendingStoreAuditDto
|
||||
{
|
||||
StoreId = reader.GetInt64(storeIdOrdinal),
|
||||
StoreName = reader.GetString(storeNameOrdinal),
|
||||
StoreCode = reader.GetString(storeCodeOrdinal),
|
||||
TenantId = reader.GetInt64(tenantIdOrdinal),
|
||||
TenantName = reader.GetString(tenantNameOrdinal),
|
||||
MerchantId = reader.GetInt64(merchantIdOrdinal),
|
||||
MerchantName = reader.GetString(merchantNameOrdinal),
|
||||
SignboardImageUrl = reader.IsDBNull(signboardOrdinal) ? null : reader.GetString(signboardOrdinal),
|
||||
FullAddress = fullAddress,
|
||||
OwnershipType = (StoreOwnershipType)reader.GetInt32(ownershipOrdinal),
|
||||
SubmittedAt = submittedAt,
|
||||
WaitingDays = waitingDays,
|
||||
IsOverdue = submittedAt.HasValue && submittedAt.Value <= overdueDeadline,
|
||||
QualificationCount = reader.GetInt32(qualificationCountOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
// 3.4 (空行后) 返回分页结果
|
||||
return new PagedResult<PendingStoreAuditDto>(items, page, pageSize, total);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static string BuildCountSql()
|
||||
{
|
||||
return """
|
||||
select count(*)
|
||||
from public.stores s
|
||||
join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
where s."DeletedAt" is null
|
||||
and s."AuditStatus" = @pendingStatus
|
||||
and (@tenantId::bigint is null or s."TenantId" = @tenantId)
|
||||
and (
|
||||
@keyword::text is null
|
||||
or s."Name" ilike ('%' || @keyword::text || '%')
|
||||
or s."Code" ilike ('%' || @keyword::text || '%')
|
||||
or m."BrandName" ilike ('%' || @keyword::text || '%')
|
||||
)
|
||||
and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom)
|
||||
and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo)
|
||||
and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline);
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildListSql(string orderBy, bool sortDesc)
|
||||
{
|
||||
var direction = sortDesc ? "desc" : "asc";
|
||||
|
||||
// 1. (空行后) 构造列表 SQL
|
||||
return $"""
|
||||
select
|
||||
s."Id" as "StoreId",
|
||||
s."Name" as "StoreName",
|
||||
s."Code" as "StoreCode",
|
||||
s."TenantId",
|
||||
t."Name" as "TenantName",
|
||||
s."MerchantId",
|
||||
m."BrandName" as "MerchantName",
|
||||
s."SignboardImageUrl",
|
||||
s."Province",
|
||||
s."City",
|
||||
s."District",
|
||||
s."Address",
|
||||
s."OwnershipType",
|
||||
s."SubmittedAt",
|
||||
(
|
||||
select count(1)
|
||||
from public.store_qualifications q
|
||||
where q."StoreId" = s."Id" and q."DeletedAt" is null
|
||||
) as "QualificationCount"
|
||||
from public.stores s
|
||||
join public.merchants m on m."Id" = s."MerchantId" and m."DeletedAt" is null
|
||||
join public.tenants t on t."Id" = s."TenantId" and t."DeletedAt" is null
|
||||
where s."DeletedAt" is null
|
||||
and s."AuditStatus" = @pendingStatus
|
||||
and (@tenantId::bigint is null or s."TenantId" = @tenantId)
|
||||
and (
|
||||
@keyword::text is null
|
||||
or s."Name" ilike ('%' || @keyword::text || '%')
|
||||
or s."Code" ilike ('%' || @keyword::text || '%')
|
||||
or m."BrandName" ilike ('%' || @keyword::text || '%')
|
||||
)
|
||||
and (@submittedFrom::timestamp with time zone is null or s."SubmittedAt" >= @submittedFrom)
|
||||
and (@submittedTo::timestamp with time zone is null or s."SubmittedAt" <= @submittedTo)
|
||||
and (@overdueOnly::boolean = false or s."SubmittedAt" <= @overdueDeadline)
|
||||
order by {orderBy} {direction}
|
||||
offset @offset
|
||||
limit @limit;
|
||||
""";
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteScalarIntAsync(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(connection, sql, parameters);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken);
|
||||
return result is null or DBNull ? 0 : Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核记录查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreAuditRecordsQueryHandler(
|
||||
IDapperExecutor dapperExecutor)
|
||||
: IRequestHandler<ListStoreAuditRecordsQuery, PagedResult<StoreAuditRecordDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<StoreAuditRecordDto>> Handle(ListStoreAuditRecordsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 参数规范化
|
||||
var page = request.Page <= 0 ? 1 : request.Page;
|
||||
var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
|
||||
var offset = (page - 1) * pageSize;
|
||||
|
||||
// 2. (空行后) 查询审核记录
|
||||
return await dapperExecutor.QueryAsync(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
// 2.1 统计总数
|
||||
var total = await ExecuteScalarIntAsync(
|
||||
connection,
|
||||
BuildCountSql(),
|
||||
[
|
||||
("storeId", request.StoreId)
|
||||
],
|
||||
token);
|
||||
|
||||
// 2.2 (空行后) 查询列表
|
||||
await using var listCommand = CreateCommand(
|
||||
connection,
|
||||
BuildListSql(),
|
||||
[
|
||||
("storeId", request.StoreId),
|
||||
("offset", offset),
|
||||
("limit", pageSize)
|
||||
]);
|
||||
|
||||
await using var reader = await listCommand.ExecuteReaderAsync(token);
|
||||
|
||||
// 2.3 (空行后) 映射列表
|
||||
var items = new List<StoreAuditRecordDto>();
|
||||
if (!reader.HasRows)
|
||||
{
|
||||
return new PagedResult<StoreAuditRecordDto>(items, page, pageSize, total);
|
||||
}
|
||||
|
||||
// 2.3.1 (空行后) 初始化字段序号
|
||||
var idOrdinal = reader.GetOrdinal("Id");
|
||||
var actionOrdinal = reader.GetOrdinal("Action");
|
||||
var operatorIdOrdinal = reader.GetOrdinal("OperatorId");
|
||||
var operatorNameOrdinal = reader.GetOrdinal("OperatorName");
|
||||
var previousStatusOrdinal = reader.GetOrdinal("PreviousStatus");
|
||||
var newStatusOrdinal = reader.GetOrdinal("NewStatus");
|
||||
var rejectionReasonIdOrdinal = reader.GetOrdinal("RejectionReasonId");
|
||||
var rejectionReasonOrdinal = reader.GetOrdinal("RejectionReason");
|
||||
var remarksOrdinal = reader.GetOrdinal("Remarks");
|
||||
var createdAtOrdinal = reader.GetOrdinal("CreatedAt");
|
||||
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
var action = (StoreAuditAction)reader.GetInt32(actionOrdinal);
|
||||
items.Add(new StoreAuditRecordDto
|
||||
{
|
||||
Id = reader.GetInt64(idOrdinal),
|
||||
Action = action,
|
||||
ActionName = StoreAuditActionNameResolver.Resolve(action),
|
||||
OperatorId = reader.IsDBNull(operatorIdOrdinal) ? null : reader.GetInt64(operatorIdOrdinal),
|
||||
OperatorName = reader.GetString(operatorNameOrdinal),
|
||||
PreviousStatus = reader.IsDBNull(previousStatusOrdinal)
|
||||
? null
|
||||
: (StoreAuditStatus)reader.GetInt32(previousStatusOrdinal),
|
||||
NewStatus = (StoreAuditStatus)reader.GetInt32(newStatusOrdinal),
|
||||
RejectionReasonId = reader.IsDBNull(rejectionReasonIdOrdinal)
|
||||
? null
|
||||
: reader.GetInt64(rejectionReasonIdOrdinal),
|
||||
RejectionReasonText = reader.IsDBNull(rejectionReasonOrdinal)
|
||||
? null
|
||||
: reader.GetString(rejectionReasonOrdinal),
|
||||
Remark = reader.IsDBNull(remarksOrdinal) ? null : reader.GetString(remarksOrdinal),
|
||||
CreatedAt = reader.GetDateTime(createdAtOrdinal)
|
||||
});
|
||||
}
|
||||
|
||||
// 2.4 (空行后) 返回分页结果
|
||||
return new PagedResult<StoreAuditRecordDto>(items, page, pageSize, total);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static string BuildCountSql()
|
||||
{
|
||||
return """
|
||||
select count(*)
|
||||
from public.store_audit_records r
|
||||
where r."DeletedAt" is null
|
||||
and r."StoreId" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static string BuildListSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
r."Id",
|
||||
r."Action",
|
||||
r."OperatorId",
|
||||
r."OperatorName",
|
||||
r."PreviousStatus",
|
||||
r."NewStatus",
|
||||
r."RejectionReasonId",
|
||||
r."RejectionReason",
|
||||
r."Remarks",
|
||||
r."CreatedAt"
|
||||
from public.store_audit_records r
|
||||
where r."DeletedAt" is null
|
||||
and r."StoreId" = @storeId
|
||||
order by r."CreatedAt" desc
|
||||
offset @offset
|
||||
limit @limit;
|
||||
""";
|
||||
}
|
||||
|
||||
private static async Task<int> ExecuteScalarIntAsync(
|
||||
IDbConnection connection,
|
||||
string sql,
|
||||
(string Name, object? Value)[] parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var command = CreateCommand(connection, sql, parameters);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken);
|
||||
return result is null or DBNull ? 0 : Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 审核驳回处理器。
|
||||
/// </summary>
|
||||
public sealed class RejectStoreCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IDapperExecutor dapperExecutor,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
ILogger<RejectStoreCommandHandler> logger)
|
||||
: IRequestHandler<RejectStoreCommand, StoreAuditActionResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditActionResultDto> Handle(RejectStoreCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取门店快照
|
||||
var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
|
||||
if (!snapshot.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 1.1 (空行后) 校验审核状态
|
||||
if (snapshot.Value.AuditStatus != StoreAuditStatus.Pending)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店不处于待审核状态");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取门店实体
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 3. (空行后) 更新状态并记录审核
|
||||
var previousStatus = store.AuditStatus;
|
||||
store.AuditStatus = StoreAuditStatus.Rejected;
|
||||
store.RejectionReason = request.RejectionReasonText;
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = StoreAuditAction.Reject,
|
||||
PreviousStatus = previousStatus,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
RejectionReasonId = request.RejectionReasonId,
|
||||
RejectionReason = request.RejectionReasonText,
|
||||
Remarks = request.Remark
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 审核驳回", store.Id);
|
||||
|
||||
// 4. (空行后) 返回结果
|
||||
return new StoreAuditActionResultDto
|
||||
{
|
||||
StoreId = store.Id,
|
||||
AuditStatus = store.AuditStatus,
|
||||
BusinessStatus = store.BusinessStatus,
|
||||
RejectionReason = store.RejectionReason,
|
||||
Message = "已驳回,租户可修改后重新提交"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店基础字段
|
||||
return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildStoreSnapshotSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
// 1.1 (空行后) 执行查询
|
||||
await using var reader = await command.ExecuteReaderAsync(token);
|
||||
if (!await reader.ReadAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1.2 (空行后) 返回快照
|
||||
return (
|
||||
reader.GetInt64(reader.GetOrdinal("TenantId")),
|
||||
(StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
|
||||
(StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private long? ResolveOperatorId()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? null : id;
|
||||
}
|
||||
|
||||
private string ResolveOperatorName()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? "system" : $"user:{id}";
|
||||
}
|
||||
|
||||
private static string BuildStoreSnapshotSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
s."TenantId",
|
||||
s."AuditStatus",
|
||||
s."BusinessStatus"
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."Id" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Data;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 解除强制关闭处理器。
|
||||
/// </summary>
|
||||
public sealed class ReopenStoreCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
IDapperExecutor dapperExecutor,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
ILogger<ReopenStoreCommandHandler> logger)
|
||||
: IRequestHandler<ReopenStoreCommand, StoreAuditActionResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreAuditActionResultDto> Handle(ReopenStoreCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取门店快照
|
||||
var snapshot = await QueryStoreSnapshotAsync(request.StoreId, cancellationToken);
|
||||
if (!snapshot.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 1.1 (空行后) 校验审核与经营状态
|
||||
if (snapshot.Value.AuditStatus != StoreAuditStatus.Activated)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店未激活,无法解除关闭");
|
||||
}
|
||||
|
||||
if (snapshot.Value.BusinessStatus != StoreBusinessStatus.ForceClosed)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "门店未处于强制关闭状态");
|
||||
}
|
||||
|
||||
// 2. (空行后) 获取门店实体
|
||||
var store = await storeRepository.FindByIdAsync(request.StoreId, snapshot.Value.TenantId, cancellationToken);
|
||||
if (store is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "门店不存在");
|
||||
}
|
||||
|
||||
// 3. (空行后) 更新状态并记录风控
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.ClosureReason = null;
|
||||
store.ClosureReasonText = null;
|
||||
store.ForceCloseReason = null;
|
||||
store.ForceClosedAt = null;
|
||||
|
||||
await storeRepository.UpdateStoreAsync(store, cancellationToken);
|
||||
await storeRepository.AddAuditRecordAsync(new StoreAuditRecord
|
||||
{
|
||||
StoreId = store.Id,
|
||||
Action = StoreAuditAction.Reopen,
|
||||
PreviousStatus = store.AuditStatus,
|
||||
NewStatus = store.AuditStatus,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
OperatorName = ResolveOperatorName(),
|
||||
Remarks = request.Remark
|
||||
}, cancellationToken);
|
||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("门店 {StoreId} 解除强制关闭", store.Id);
|
||||
|
||||
// 4. (空行后) 返回结果
|
||||
return new StoreAuditActionResultDto
|
||||
{
|
||||
StoreId = store.Id,
|
||||
AuditStatus = store.AuditStatus,
|
||||
BusinessStatus = store.BusinessStatus,
|
||||
Message = "强制关闭已解除,门店恢复为休息中状态"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?> QueryStoreSnapshotAsync(
|
||||
long storeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询门店基础字段
|
||||
return await dapperExecutor.QueryAsync<(long TenantId, StoreAuditStatus AuditStatus, StoreBusinessStatus BusinessStatus)?>(
|
||||
DatabaseConstants.AppDataSource,
|
||||
DatabaseConnectionRole.Read,
|
||||
async (connection, token) =>
|
||||
{
|
||||
await using var command = CreateCommand(
|
||||
connection,
|
||||
BuildStoreSnapshotSql(),
|
||||
[
|
||||
("storeId", storeId)
|
||||
]);
|
||||
|
||||
// 1.1 (空行后) 执行查询
|
||||
await using var reader = await command.ExecuteReaderAsync(token);
|
||||
if (!await reader.ReadAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1.2 (空行后) 返回快照
|
||||
return (
|
||||
reader.GetInt64(reader.GetOrdinal("TenantId")),
|
||||
(StoreAuditStatus)reader.GetInt32(reader.GetOrdinal("AuditStatus")),
|
||||
(StoreBusinessStatus)reader.GetInt32(reader.GetOrdinal("BusinessStatus")));
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private long? ResolveOperatorId()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? null : id;
|
||||
}
|
||||
|
||||
private string ResolveOperatorName()
|
||||
{
|
||||
var id = currentUserAccessor.UserId;
|
||||
return id == 0 ? "system" : $"user:{id}";
|
||||
}
|
||||
|
||||
private static string BuildStoreSnapshotSql()
|
||||
{
|
||||
return """
|
||||
select
|
||||
s."TenantId",
|
||||
s."AuditStatus",
|
||||
s."BusinessStatus"
|
||||
from public.stores s
|
||||
where s."DeletedAt" is null
|
||||
and s."Id" = @storeId;
|
||||
""";
|
||||
}
|
||||
|
||||
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
||||
{
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
|
||||
// 1. (空行后) 绑定参数
|
||||
foreach (var (name, value) in parameters)
|
||||
{
|
||||
var p = command.CreateParameter();
|
||||
p.ParameterName = name;
|
||||
p.Value = value ?? DBNull.Value;
|
||||
command.Parameters.Add(p);
|
||||
}
|
||||
|
||||
return (DbCommand)command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店审核详情查询。
|
||||
/// </summary>
|
||||
public sealed record GetStoreAuditDetailQuery : IRequest<StoreAuditDetailDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 获取审核统计查询。
|
||||
/// </summary>
|
||||
public sealed record GetStoreAuditStatisticsQuery : IRequest<StoreAuditStatisticsDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 起始日期。
|
||||
/// </summary>
|
||||
public DateTime? DateFrom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 截止日期。
|
||||
/// </summary>
|
||||
public DateTime? DateTo { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询待审核门店列表。
|
||||
/// </summary>
|
||||
public sealed record ListPendingStoreAuditsQuery : IRequest<PagedResult<PendingStoreAuditDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交起始时间。
|
||||
/// </summary>
|
||||
public DateTime? SubmittedFrom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 提交截止时间。
|
||||
/// </summary>
|
||||
public DateTime? SubmittedTo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否只显示超时。
|
||||
/// </summary>
|
||||
public bool OverdueOnly { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页数量。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段。
|
||||
/// </summary>
|
||||
public string? SortBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否降序。
|
||||
/// </summary>
|
||||
public bool SortDesc { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Dto;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询门店审核记录。
|
||||
/// </summary>
|
||||
public sealed record ListStoreAuditRecordsQuery : IRequest<PagedResult<StoreAuditRecordDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页数量。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits;
|
||||
|
||||
/// <summary>
|
||||
/// 门店审核动作名称解析器。
|
||||
/// </summary>
|
||||
public static class StoreAuditActionNameResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取动作名称。
|
||||
/// </summary>
|
||||
/// <param name="action">审核动作。</param>
|
||||
/// <returns>动作名称。</returns>
|
||||
public static string Resolve(StoreAuditAction action) => action switch
|
||||
{
|
||||
StoreAuditAction.Submit => "提交审核",
|
||||
StoreAuditAction.Resubmit => "重新提交",
|
||||
StoreAuditAction.Approve => "审核通过",
|
||||
StoreAuditAction.Reject => "审核驳回",
|
||||
StoreAuditAction.ForceClose => "强制关闭",
|
||||
StoreAuditAction.Reopen => "解除关闭",
|
||||
StoreAuditAction.AutoActivate => "自动激活",
|
||||
_ => "未知操作"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 审核通过命令验证器。
|
||||
/// </summary>
|
||||
public sealed class ApproveStoreCommandValidator : AbstractValidator<ApproveStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ApproveStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Remark).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 强制关闭命令验证器。
|
||||
/// </summary>
|
||||
public sealed class ForceCloseStoreCommandValidator : AbstractValidator<ForceCloseStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ForceCloseStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Reason).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.Remark).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 审核驳回命令验证器。
|
||||
/// </summary>
|
||||
public sealed class RejectStoreCommandValidator : AbstractValidator<RejectStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public RejectStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.RejectionReasonId).GreaterThan(0);
|
||||
RuleFor(x => x.RejectionReasonText).MaximumLength(500);
|
||||
RuleFor(x => x.Remark).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.StoreAudits.Commands;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.StoreAudits.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 解除强制关闭命令验证器。
|
||||
/// </summary>
|
||||
public sealed class ReopenStoreCommandValidator : AbstractValidator<ReopenStoreCommand>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ReopenStoreCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Remark).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user