feat:商户管理
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user