完成门店管理后端接口与任务

This commit is contained in:
2026-01-01 07:26:14 +08:00
parent dc9f6136d6
commit fc55003d3d
131 changed files with 15333 additions and 201 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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; } = [];
}

View File

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

View File

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

View File

@@ -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; } = [];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 => "自动激活",
_ => "未知操作"
};
}

View File

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

View File

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

View File

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

View File

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