feat:商户管理

This commit is contained in:
2025-12-29 16:40:27 +08:00
parent 57f4c2d394
commit dd91c1010a
62 changed files with 10536 additions and 165 deletions

View File

@@ -1,6 +1,7 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
@@ -41,36 +42,32 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
/// <summary>
/// 查询商户列表。
/// </summary>
/// <param name="status">状态筛选。</param>
/// <param name="page">页码。</param>
/// <param name="pageSize">每页大小。</param>
/// <param name="sortBy">排序字段。</param>
/// <param name="sortDesc">是否倒序。</param>
/// <param name="query">查询参数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>商户分页结果。</returns>
[HttpGet]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<MerchantDto>>> List(
[FromQuery] MerchantStatus? status,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? sortBy = null,
[FromQuery] bool sortDesc = true,
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantListItemDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<MerchantListItemDto>>> List(
[FromQuery] GetMerchantListQuery query,
CancellationToken cancellationToken = default)
{
// 1. 组装查询参数并执行查询
var result = await mediator.Send(new SearchMerchantsQuery
{
Status = status,
Page = page,
PageSize = pageSize,
SortBy = sortBy,
SortDescending = sortDesc
}, cancellationToken);
var result = await mediator.Send(query, cancellationToken);
return ApiResponse<PagedResult<MerchantListItemDto>>.Ok(result);
}
// 2. 返回分页结果
return ApiResponse<PagedResult<MerchantDto>>.Ok(result);
/// <summary>
/// 待审核商户列表。
/// </summary>
[HttpGet("pending-review")]
[PermissionAuthorize("merchant:review")]
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantReviewListItemDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<PagedResult<MerchantReviewListItemDto>>> PendingReviewList(
[FromQuery] GetPendingReviewListQuery query,
CancellationToken cancellationToken)
{
var result = await mediator.Send(query, cancellationToken);
return ApiResponse<PagedResult<MerchantReviewListItemDto>>.Ok(result);
}
/// <summary>
@@ -82,23 +79,36 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
/// <returns>更新后的商户或未找到。</returns>
[HttpPut("{merchantId:long}")]
[PermissionAuthorize("merchant:update")]
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status422UnprocessableEntity)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<MerchantDto>> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken)
public async Task<ApiResponse<UpdateMerchantResultDto>> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken)
{
// 1. 绑定商户标识
if (command.MerchantId == 0)
if (command.MerchantId != 0 && command.MerchantId != merchantId)
{
command = command with { MerchantId = merchantId };
return ApiResponse<UpdateMerchantResultDto>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
}
command = command with { MerchantId = merchantId };
// 2. 执行更新
var result = await mediator.Send(command, cancellationToken);
// 3. 返回更新结果或 404
return result == null
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
: ApiResponse<MerchantDto>.Ok(result);
if (result == null)
{
return ApiResponse<UpdateMerchantResultDto>.Error(ErrorCodes.NotFound, "商户不存在");
}
if (result.RequiresReview)
{
return ApiResponse<UpdateMerchantResultDto>.Error(
ErrorCodes.ValidationFailed,
"关键信息修改,商户已进入待审核状态,业务已冻结")
with { Data = result };
}
return ApiResponse<UpdateMerchantResultDto>.Ok(result);
}
/// <summary>
@@ -130,17 +140,51 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
/// <returns>商户概览或未找到。</returns>
[HttpGet("{merchantId:long}")]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<MerchantDetailDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<MerchantDto>> Detail(long merchantId, CancellationToken cancellationToken)
public async Task<ApiResponse<MerchantDetailDto>> Detail(long merchantId, CancellationToken cancellationToken)
{
// 1. 查询商户概览
var result = await mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken);
var result = await mediator.Send(new GetMerchantDetailQuery(merchantId), cancellationToken);
// 2. 返回结果或 404
return result == null
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
: ApiResponse<MerchantDto>.Ok(result);
// 2. 返回结果
return ApiResponse<MerchantDetailDto>.Ok(result);
}
/// <summary>
/// 获取审核领取信息。
/// </summary>
[HttpGet("{merchantId:long}/review/claim")]
[PermissionAuthorize("merchant:review")]
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
public async Task<ApiResponse<ClaimInfoDto?>> GetReviewClaim(long merchantId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMerchantReviewClaimQuery(merchantId), cancellationToken);
return ApiResponse<ClaimInfoDto?>.Ok(result);
}
/// <summary>
/// 领取审核。
/// </summary>
[HttpPost("{merchantId:long}/review/claim")]
[PermissionAuthorize("merchant:review")]
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<ClaimInfoDto>> ClaimReview(long merchantId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new ClaimMerchantReviewCommand { MerchantId = merchantId }, cancellationToken);
return ApiResponse<ClaimInfoDto>.Ok(result);
}
/// <summary>
/// 释放审核领取。
/// </summary>
[HttpDelete("{merchantId:long}/review/claim")]
[PermissionAuthorize("merchant:review")]
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
public async Task<ApiResponse<ClaimInfoDto?>> ReleaseReviewClaim(long merchantId, CancellationToken cancellationToken)
{
var result = await mediator.Send(new ReleaseClaimCommand { MerchantId = merchantId }, cancellationToken);
return ApiResponse<ClaimInfoDto?>.Ok(result);
}
/// <summary>
@@ -290,6 +334,60 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
return ApiResponse<MerchantDto>.Ok(result);
}
/// <summary>
/// 撤销审核。
/// </summary>
[HttpPost("{merchantId:long}/review/revoke")]
[PermissionAuthorize("merchant:review:revoke")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> RevokeReview(
long merchantId,
[FromBody] RevokeMerchantReviewCommand body,
CancellationToken cancellationToken)
{
if (body.MerchantId != 0 && body.MerchantId != merchantId)
{
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
}
var command = new RevokeMerchantReviewCommand
{
MerchantId = merchantId,
Reason = body.Reason
};
await mediator.Send(command, cancellationToken);
return ApiResponse<object>.Ok(null);
}
/// <summary>
/// 审核历史。
/// </summary>
[HttpGet("{merchantId:long}/audit-history")]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantAuditLogDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<MerchantAuditLogDto>>> AuditHistory(
long merchantId,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMerchantAuditHistoryQuery(merchantId), cancellationToken);
return ApiResponse<IReadOnlyList<MerchantAuditLogDto>>.Ok(result);
}
/// <summary>
/// 变更历史。
/// </summary>
[HttpGet("{merchantId:long}/change-history")]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantChangeLogDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<MerchantChangeLogDto>>> ChangeHistory(
long merchantId,
[FromQuery] string? fieldName,
CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetMerchantChangeHistoryQuery(merchantId, fieldName), cancellationToken);
return ApiResponse<IReadOnlyList<MerchantChangeLogDto>>.Ok(result);
}
/// <summary>
/// 审核日志。
/// </summary>
@@ -310,6 +408,27 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
return ApiResponse<PagedResult<MerchantAuditLogDto>>.Ok(result);
}
/// <summary>
/// 导出商户 PDF。
/// </summary>
[HttpGet("{merchantId:long}/export-pdf")]
[PermissionAuthorize("merchant:export")]
[Produces("application/pdf")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public async Task<IActionResult> ExportPdf(long merchantId, CancellationToken cancellationToken)
{
var bytes = await mediator.Send(new ExportMerchantPdfQuery(merchantId), cancellationToken);
var fileName = $"merchant_{merchantId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.pdf";
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
{
FileName = fileName,
FileNameStar = fileName
}.ToString();
return File(bytes, "application/pdf");
}
/// <summary>
/// 可选商户类目列表。
/// </summary>