feat:商户管理
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
@@ -41,36 +42,32 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询商户列表。
|
/// 查询商户列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="status">状态筛选。</param>
|
/// <param name="query">查询参数。</param>
|
||||||
/// <param name="page">页码。</param>
|
|
||||||
/// <param name="pageSize">每页大小。</param>
|
|
||||||
/// <param name="sortBy">排序字段。</param>
|
|
||||||
/// <param name="sortDesc">是否倒序。</param>
|
|
||||||
/// <param name="cancellationToken">取消标记。</param>
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
/// <returns>商户分页结果。</returns>
|
/// <returns>商户分页结果。</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[PermissionAuthorize("merchant:read")]
|
[PermissionAuthorize("merchant:read")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantListItemDto>>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<PagedResult<MerchantDto>>> List(
|
public async Task<ApiResponse<PagedResult<MerchantListItemDto>>> List(
|
||||||
[FromQuery] MerchantStatus? status,
|
[FromQuery] GetMerchantListQuery query,
|
||||||
[FromQuery] int page = 1,
|
|
||||||
[FromQuery] int pageSize = 20,
|
|
||||||
[FromQuery] string? sortBy = null,
|
|
||||||
[FromQuery] bool sortDesc = true,
|
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// 1. 组装查询参数并执行查询
|
var result = await mediator.Send(query, cancellationToken);
|
||||||
var result = await mediator.Send(new SearchMerchantsQuery
|
return ApiResponse<PagedResult<MerchantListItemDto>>.Ok(result);
|
||||||
{
|
}
|
||||||
Status = status,
|
|
||||||
Page = page,
|
|
||||||
PageSize = pageSize,
|
|
||||||
SortBy = sortBy,
|
|
||||||
SortDescending = sortDesc
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
// 2. 返回分页结果
|
/// <summary>
|
||||||
return ApiResponse<PagedResult<MerchantDto>>.Ok(result);
|
/// 待审核商户列表。
|
||||||
|
/// </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>
|
/// <summary>
|
||||||
@@ -82,23 +79,36 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
|||||||
/// <returns>更新后的商户或未找到。</returns>
|
/// <returns>更新后的商户或未找到。</returns>
|
||||||
[HttpPut("{merchantId:long}")]
|
[HttpPut("{merchantId:long}")]
|
||||||
[PermissionAuthorize("merchant:update")]
|
[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)]
|
[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 && command.MerchantId != merchantId)
|
||||||
if (command.MerchantId == 0)
|
|
||||||
{
|
{
|
||||||
command = command with { MerchantId = merchantId };
|
return ApiResponse<UpdateMerchantResultDto>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command = command with { MerchantId = merchantId };
|
||||||
|
|
||||||
// 2. 执行更新
|
// 2. 执行更新
|
||||||
var result = await mediator.Send(command, cancellationToken);
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
// 3. 返回更新结果或 404
|
// 3. 返回更新结果或 404
|
||||||
return result == null
|
if (result == null)
|
||||||
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
{
|
||||||
: ApiResponse<MerchantDto>.Ok(result);
|
return ApiResponse<UpdateMerchantResultDto>.Error(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.RequiresReview)
|
||||||
|
{
|
||||||
|
return ApiResponse<UpdateMerchantResultDto>.Error(
|
||||||
|
ErrorCodes.ValidationFailed,
|
||||||
|
"关键信息修改,商户已进入待审核状态,业务已冻结")
|
||||||
|
with { Data = result };
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<UpdateMerchantResultDto>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -130,17 +140,51 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
|||||||
/// <returns>商户概览或未找到。</returns>
|
/// <returns>商户概览或未找到。</returns>
|
||||||
[HttpGet("{merchantId:long}")]
|
[HttpGet("{merchantId:long}")]
|
||||||
[PermissionAuthorize("merchant:read")]
|
[PermissionAuthorize("merchant:read")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<MerchantDetailDto>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
[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. 查询商户概览
|
// 1. 查询商户概览
|
||||||
var result = await mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken);
|
var result = await mediator.Send(new GetMerchantDetailQuery(merchantId), cancellationToken);
|
||||||
|
|
||||||
// 2. 返回结果或 404
|
// 2. 返回结果
|
||||||
return result == null
|
return ApiResponse<MerchantDetailDto>.Ok(result);
|
||||||
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
}
|
||||||
: ApiResponse<MerchantDto>.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>
|
/// <summary>
|
||||||
@@ -290,6 +334,60 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
|||||||
return ApiResponse<MerchantDto>.Ok(result);
|
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>
|
||||||
/// 审核日志。
|
/// 审核日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -310,6 +408,27 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
|||||||
return ApiResponse<PagedResult<MerchantAuditLogDto>>.Ok(result);
|
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>
|
||||||
/// 可选商户类目列表。
|
/// 可选商户类目列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取商户审核命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ClaimMerchantReviewCommand : IRequest<ClaimInfoDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放商户审核领取命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReleaseClaimCommand : IRequest<ClaimInfoDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销商户审核命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RevokeMerchantReviewCommand : IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销原因。
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新商户命令。
|
/// 更新商户命令。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
public sealed record UpdateMerchantCommand : IRequest<UpdateMerchantResultDto?>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 商户 ID。
|
/// 商户 ID。
|
||||||
@@ -15,29 +14,29 @@ public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
|||||||
public long MerchantId { get; init; }
|
public long MerchantId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 品牌名称。
|
/// 商户名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BrandName { get; init; } = string.Empty;
|
public string? Name { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 品牌简称。
|
/// 营业执照号。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? BrandAlias { get; init; }
|
public string? LicenseNumber { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logo 地址。
|
/// 法人或负责人。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? LogoUrl { get; init; }
|
public string? LegalRepresentative { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 品类。
|
/// 注册地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Category { get; init; }
|
public string? RegisteredAddress { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 联系电话。
|
/// 联系电话。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ContactPhone { get; init; } = string.Empty;
|
public string? ContactPhone { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 联系邮箱。
|
/// 联系邮箱。
|
||||||
@@ -45,7 +44,7 @@ public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
|||||||
public string? ContactEmail { get; init; }
|
public string? ContactEmail { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 入驻状态。
|
/// 并发控制版本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MerchantStatus Status { get; init; }
|
public byte[] RowVersion { get; init; } = Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核领取信息 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ClaimInfoDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long MerchantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? ClaimedBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ClaimedByName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取过期时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimExpiresAt { get; init; }
|
||||||
|
}
|
||||||
@@ -26,6 +26,12 @@ public sealed class MerchantAuditLogDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MerchantAuditAction Action { get; init; }
|
public MerchantAuditAction Action { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? OperatorId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标题。
|
/// 标题。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -41,6 +47,11 @@ public sealed class MerchantAuditLogDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? OperatorName { get; init; }
|
public string? OperatorName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作 IP。
|
||||||
|
/// </summary>
|
||||||
|
public string? IpAddress { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建时间。
|
/// 创建时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户变更日志 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MerchantChangeLogDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更字段。
|
||||||
|
/// </summary>
|
||||||
|
public string FieldName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更前值。
|
||||||
|
/// </summary>
|
||||||
|
public string? OldValue { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更后值。
|
||||||
|
/// </summary>
|
||||||
|
public string? NewValue { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更人 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? ChangedBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChangedByName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ChangedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChangeReason { get; init; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -6,17 +11,117 @@ namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
|||||||
public sealed class MerchantDetailDto
|
public sealed class MerchantDetailDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基础信息。
|
/// 商户 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MerchantDto Merchant { get; init; } = new();
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 证照列表。
|
/// 租户 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<MerchantDocumentDto> Documents { get; init; } = [];
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合同列表。
|
/// 租户名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<MerchantContractDto> Contracts { get; init; } = [];
|
public string? TenantName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营业执照号。
|
||||||
|
/// </summary>
|
||||||
|
public string? LicenseNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 法人或负责人。
|
||||||
|
/// </summary>
|
||||||
|
public string? LegalRepresentative { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? RegisteredAddress { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? ContactPhone { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系邮箱。
|
||||||
|
/// </summary>
|
||||||
|
public string? ContactEmail { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核状态。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务冻结标记。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFrozen { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? FrozenReason { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FrozenAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过人。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? ApprovedBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ApprovedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<StoreDto> Stores { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 并发控制版本。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] RowVersion { get; init; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建人。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? CreatedBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新人。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||||
|
public long? UpdatedBy { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户列表项 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MerchantListItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? TenantName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营业执照号。
|
||||||
|
/// </summary>
|
||||||
|
public string? LicenseNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核状态。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否冻结业务。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFrozen { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店数量。
|
||||||
|
/// </summary>
|
||||||
|
public int StoreCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UpdatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待审核商户列表项 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MerchantReviewListItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? TenantName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营业执照号。
|
||||||
|
/// </summary>
|
||||||
|
public string? LicenseNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核状态。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantStatus Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ClaimedByName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取过期时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimExpiresAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户详情门店 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营业执照号(主体不一致模式使用)。
|
||||||
|
/// </summary>
|
||||||
|
public string? LicenseNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 联系电话。
|
||||||
|
/// </summary>
|
||||||
|
public string? ContactPhone { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店地址。
|
||||||
|
/// </summary>
|
||||||
|
public string? Address { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店状态。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStatus Status { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户更新结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateMerchantResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 更新后的商户详情。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantDetailDto Merchant { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否触发重新审核。
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresReview { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取商户审核处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ClaimMerchantReviewHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor)
|
||||||
|
: IRequestHandler<ClaimMerchantReviewCommand, ClaimInfoDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ClaimInfoDto> Handle(ClaimMerchantReviewCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
|
||||||
|
if (merchant.Status != MerchantStatus.Pending)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (merchant.ClaimedBy.HasValue && merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt > now)
|
||||||
|
{
|
||||||
|
if (merchant.ClaimedBy != currentUserAccessor.UserId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToDto(merchant);
|
||||||
|
}
|
||||||
|
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
merchant.ClaimedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
|
||||||
|
merchant.ClaimedByName = actorName;
|
||||||
|
merchant.ClaimedAt = now;
|
||||||
|
merchant.ClaimExpiresAt = now.AddMinutes(30);
|
||||||
|
|
||||||
|
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
|
{
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
Action = MerchantAuditAction.ReviewClaimed,
|
||||||
|
Title = "领取审核",
|
||||||
|
Description = $"领取人:{actorName}",
|
||||||
|
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName
|
||||||
|
}, cancellationToken);
|
||||||
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ToDto(merchant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClaimInfoDto ToDto(Domain.Merchants.Entities.Merchant merchant)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
ClaimedBy = merchant.ClaimedBy,
|
||||||
|
ClaimedByName = merchant.ClaimedByName,
|
||||||
|
ClaimedAt = merchant.ClaimedAt,
|
||||||
|
ClaimExpiresAt = merchant.ClaimExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出商户 PDF 处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportMerchantPdfQueryHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
IMerchantExportService exportService,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService)
|
||||||
|
: IRequestHandler<ExportMerchantPdfQuery, byte[]>
|
||||||
|
{
|
||||||
|
public async Task<byte[]> Handle(ExportMerchantPdfQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
|
var merchant = isSuperAdmin
|
||||||
|
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (merchant == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止导出其他租户商户");
|
||||||
|
}
|
||||||
|
|
||||||
|
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
|
||||||
|
var auditLogs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
|
||||||
|
|
||||||
|
return await exportService.ExportToPdfAsync(merchant, tenant?.Name, stores, auditLogs, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户审核历史处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetMerchantAuditHistoryQueryHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService)
|
||||||
|
: IRequestHandler<GetMerchantAuditHistoryQuery, IReadOnlyList<MerchantAuditLogDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MerchantAuditLogDto>> Handle(
|
||||||
|
GetMerchantAuditHistoryQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
|
var merchant = isSuperAdmin
|
||||||
|
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (merchant == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户审核历史");
|
||||||
|
}
|
||||||
|
|
||||||
|
var logs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
|
||||||
|
return logs.Select(MerchantMapping.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户变更历史处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetMerchantChangeHistoryQueryHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService)
|
||||||
|
: IRequestHandler<GetMerchantChangeHistoryQuery, IReadOnlyList<MerchantChangeLogDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MerchantChangeLogDto>> Handle(
|
||||||
|
GetMerchantChangeHistoryQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
|
var merchant = isSuperAdmin
|
||||||
|
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (merchant == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户变更历史");
|
||||||
|
}
|
||||||
|
|
||||||
|
var logs = await merchantRepository.GetChangeLogsAsync(merchant.Id, merchant.TenantId, request.FieldName, cancellationToken);
|
||||||
|
return logs.Select(MerchantMapping.ToDto).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
@@ -13,7 +18,11 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetMerchantDetailQueryHandler(
|
public sealed class GetMerchantDetailQueryHandler(
|
||||||
IMerchantRepository merchantRepository,
|
IMerchantRepository merchantRepository,
|
||||||
ITenantProvider tenantProvider)
|
IStoreRepository storeRepository,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService)
|
||||||
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
|
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -24,21 +33,31 @@ public sealed class GetMerchantDetailQueryHandler(
|
|||||||
/// <returns>商户详情 DTO。</returns>
|
/// <returns>商户详情 DTO。</returns>
|
||||||
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
|
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. 获取租户上下文并查询商户
|
// 1. 获取权限与商户
|
||||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
// 2. 查询证照与合同
|
var merchant = isSuperAdmin
|
||||||
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
|
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
|
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (merchant == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询门店与租户信息
|
||||||
|
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
|
||||||
|
var storeDtos = MerchantMapping.ToStoreDtos(stores);
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
|
||||||
|
|
||||||
// 3. 返回明细 DTO
|
// 3. 返回明细 DTO
|
||||||
return new MerchantDetailDto
|
return MerchantMapping.ToDetailDto(merchant, tenant?.Name, storeDtos);
|
||||||
{
|
|
||||||
Merchant = MerchantMapping.ToDto(merchant),
|
|
||||||
Documents = MerchantMapping.ToDocumentDtos(documents),
|
|
||||||
Contracts = MerchantMapping.ToContractDtos(contracts)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetMerchantListQueryHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService)
|
||||||
|
: IRequestHandler<GetMerchantListQuery, PagedResult<MerchantListItemDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PagedResult<MerchantListItemDto>> Handle(
|
||||||
|
GetMerchantListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 校验跨租户访问权限
|
||||||
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
|
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询商户");
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveTenantId = isSuperAdmin ? request.TenantId : currentTenantId;
|
||||||
|
|
||||||
|
// 2. 查询商户列表
|
||||||
|
var merchants = await merchantRepository.SearchAsync(
|
||||||
|
effectiveTenantId,
|
||||||
|
request.Status,
|
||||||
|
request.OperatingMode,
|
||||||
|
request.Keyword,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (merchants.Count == 0)
|
||||||
|
{
|
||||||
|
return new PagedResult<MerchantListItemDto>(Array.Empty<MerchantListItemDto>(), request.Page, request.PageSize, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 排序 & 分页
|
||||||
|
var sorted = ApplySorting(merchants, request.SortBy, request.SortOrder);
|
||||||
|
var total = sorted.Count;
|
||||||
|
var paged = sorted
|
||||||
|
.Skip((request.Page - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (paged.Count == 0)
|
||||||
|
{
|
||||||
|
return new PagedResult<MerchantListItemDto>(Array.Empty<MerchantListItemDto>(), request.Page, request.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 批量查询租户名称
|
||||||
|
var tenantIds = paged.Select(x => x.TenantId).Distinct().ToArray();
|
||||||
|
var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken);
|
||||||
|
var tenantLookup = tenants.ToDictionary(x => x.Id, x => x.Name);
|
||||||
|
|
||||||
|
// 5. 批量查询门店数量
|
||||||
|
var merchantIds = paged.Select(x => x.Id).ToArray();
|
||||||
|
var storeCounts = await storeRepository.GetStoreCountsAsync(effectiveTenantId, merchantIds, cancellationToken);
|
||||||
|
|
||||||
|
// 6. 组装 DTO
|
||||||
|
var items = paged.Select(merchant =>
|
||||||
|
{
|
||||||
|
var tenantName = tenantLookup.TryGetValue(merchant.TenantId, out var name) ? name : null;
|
||||||
|
var count = storeCounts.TryGetValue(merchant.Id, out var value) ? value : 0;
|
||||||
|
return MerchantMapping.ToListItemDto(merchant, tenantName, count);
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<MerchantListItemDto>(items, request.Page, request.PageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Domain.Merchants.Entities.Merchant> ApplySorting(
|
||||||
|
IReadOnlyList<Domain.Merchants.Entities.Merchant> merchants,
|
||||||
|
string? sortBy,
|
||||||
|
string? sortOrder)
|
||||||
|
{
|
||||||
|
var descending = !string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return (sortBy ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"name" => descending ? merchants.OrderByDescending(x => x.BrandName).ToList() : merchants.OrderBy(x => x.BrandName).ToList(),
|
||||||
|
"status" => descending ? merchants.OrderByDescending(x => x.Status).ToList() : merchants.OrderBy(x => x.Status).ToList(),
|
||||||
|
"updatedat" => descending ? merchants.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).ToList() : merchants.OrderBy(x => x.UpdatedAt ?? x.CreatedAt).ToList(),
|
||||||
|
_ => descending ? merchants.OrderByDescending(x => x.CreatedAt).ToList() : merchants.OrderBy(x => x.CreatedAt).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户审核领取信息查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetMerchantReviewClaimQueryHandler(IMerchantRepository merchantRepository)
|
||||||
|
: IRequestHandler<GetMerchantReviewClaimQuery, ClaimInfoDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ClaimInfoDto?> Handle(GetMerchantReviewClaimQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
|
||||||
|
if (!merchant.ClaimedBy.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClaimInfoDto
|
||||||
|
{
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
ClaimedBy = merchant.ClaimedBy,
|
||||||
|
ClaimedByName = merchant.ClaimedByName,
|
||||||
|
ClaimedAt = merchant.ClaimedAt,
|
||||||
|
ClaimExpiresAt = merchant.ClaimExpiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待审核商户列表处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPendingReviewListQueryHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ITenantRepository tenantRepository)
|
||||||
|
: IRequestHandler<GetPendingReviewListQuery, PagedResult<MerchantReviewListItemDto>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PagedResult<MerchantReviewListItemDto>> Handle(
|
||||||
|
GetPendingReviewListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var merchants = await merchantRepository.SearchAsync(
|
||||||
|
request.TenantId,
|
||||||
|
MerchantStatus.Pending,
|
||||||
|
request.OperatingMode,
|
||||||
|
request.Keyword,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var total = merchants.Count;
|
||||||
|
var paged = merchants
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.Skip((request.Page - 1) * request.PageSize)
|
||||||
|
.Take(request.PageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var tenantIds = paged.Select(x => x.TenantId).Distinct().ToArray();
|
||||||
|
var tenants = await tenantRepository.FindByIdsAsync(tenantIds, cancellationToken);
|
||||||
|
var tenantLookup = tenants.ToDictionary(x => x.Id, x => x.Name);
|
||||||
|
|
||||||
|
var items = paged.Select(merchant => new MerchantReviewListItemDto
|
||||||
|
{
|
||||||
|
Id = merchant.Id,
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
TenantName = tenantLookup.TryGetValue(merchant.TenantId, out var name) ? name : null,
|
||||||
|
Name = merchant.BrandName,
|
||||||
|
OperatingMode = merchant.OperatingMode,
|
||||||
|
LicenseNumber = merchant.BusinessLicenseNumber,
|
||||||
|
Status = merchant.Status,
|
||||||
|
ClaimedByName = merchant.ClaimedByName,
|
||||||
|
ClaimedAt = merchant.ClaimedAt,
|
||||||
|
ClaimExpiresAt = merchant.ClaimExpiresAt,
|
||||||
|
CreatedAt = merchant.CreatedAt
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<MerchantReviewListItemDto>(items, request.Page, request.PageSize, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放审核领取处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReleaseClaimHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor)
|
||||||
|
: IRequestHandler<ReleaseClaimCommand, ClaimInfoDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ClaimInfoDto?> Handle(ReleaseClaimCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
|
||||||
|
if (!merchant.ClaimedBy.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var claimExpired = merchant.ClaimExpiresAt.HasValue && merchant.ClaimExpiresAt <= now;
|
||||||
|
|
||||||
|
if (!claimExpired && merchant.ClaimedBy != currentUserAccessor.UserId)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
|
||||||
|
}
|
||||||
|
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
merchant.ClaimedBy = null;
|
||||||
|
merchant.ClaimedByName = null;
|
||||||
|
merchant.ClaimedAt = null;
|
||||||
|
merchant.ClaimExpiresAt = null;
|
||||||
|
|
||||||
|
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
|
{
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
Action = MerchantAuditAction.ReviewReleased,
|
||||||
|
Title = "释放审核",
|
||||||
|
Description = $"释放人:{actorName}",
|
||||||
|
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName
|
||||||
|
}, cancellationToken);
|
||||||
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ using TakeoutSaaS.Domain.Merchants.Repositories;
|
|||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ReviewMerchantCommandHandler(
|
public sealed class ReviewMerchantCommandHandler(
|
||||||
IMerchantRepository merchantRepository,
|
IMerchantRepository merchantRepository,
|
||||||
ITenantProvider tenantProvider,
|
|
||||||
ICurrentUserAccessor currentUserAccessor)
|
ICurrentUserAccessor currentUserAccessor)
|
||||||
: IRequestHandler<ReviewMerchantCommand, MerchantDto>
|
: IRequestHandler<ReviewMerchantCommand, MerchantDto>
|
||||||
{
|
{
|
||||||
@@ -29,33 +27,62 @@ public sealed class ReviewMerchantCommandHandler(
|
|||||||
public async Task<MerchantDto> Handle(ReviewMerchantCommand request, CancellationToken cancellationToken)
|
public async Task<MerchantDto> Handle(ReviewMerchantCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. 读取商户
|
// 1. 读取商户
|
||||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
|
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
|
||||||
// 2. 已审核通过则直接返回
|
var now = DateTime.UtcNow;
|
||||||
if (request.Approve && merchant.Status == MerchantStatus.Approved)
|
if (!merchant.ClaimedBy.HasValue || !merchant.ClaimExpiresAt.HasValue || merchant.ClaimExpiresAt <= now)
|
||||||
{
|
{
|
||||||
return MerchantMapping.ToDto(merchant);
|
throw new BusinessException(ErrorCodes.Conflict, "请先领取审核");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 更新审核状态
|
if (merchant.ClaimedBy != currentUserAccessor.UserId)
|
||||||
var previousStatus = merchant.Status;
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, $"该审核已被 {merchant.ClaimedByName} 领取");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merchant.Status != MerchantStatus.Pending)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "商户不在待审核状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 更新审核状态
|
||||||
merchant.Status = request.Approve ? MerchantStatus.Approved : MerchantStatus.Rejected;
|
merchant.Status = request.Approve ? MerchantStatus.Approved : MerchantStatus.Rejected;
|
||||||
merchant.ReviewRemarks = request.Remarks;
|
merchant.ReviewRemarks = request.Remarks;
|
||||||
merchant.LastReviewedAt = DateTime.UtcNow;
|
merchant.LastReviewedAt = now;
|
||||||
|
merchant.LastReviewedBy = ResolveOperatorId();
|
||||||
if (request.Approve && merchant.JoinedAt == null)
|
if (request.Approve && merchant.JoinedAt == null)
|
||||||
{
|
{
|
||||||
merchant.JoinedAt = DateTime.UtcNow;
|
merchant.JoinedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 持久化与审计
|
if (request.Approve)
|
||||||
|
{
|
||||||
|
merchant.IsFrozen = false;
|
||||||
|
merchant.FrozenReason = null;
|
||||||
|
merchant.FrozenAt = null;
|
||||||
|
merchant.ApprovedAt = now;
|
||||||
|
merchant.ApprovedBy = ResolveOperatorId();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
merchant.IsFrozen = false;
|
||||||
|
merchant.FrozenReason = null;
|
||||||
|
merchant.FrozenAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
merchant.ClaimedBy = null;
|
||||||
|
merchant.ClaimedByName = null;
|
||||||
|
merchant.ClaimedAt = null;
|
||||||
|
merchant.ClaimExpiresAt = null;
|
||||||
|
|
||||||
|
// 3. 持久化与审计
|
||||||
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
||||||
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = merchant.TenantId,
|
||||||
MerchantId = merchant.Id,
|
MerchantId = merchant.Id,
|
||||||
Action = MerchantAuditAction.MerchantReviewed,
|
Action = request.Approve ? MerchantAuditAction.ReviewApproved : MerchantAuditAction.ReviewRejected,
|
||||||
Title = request.Approve ? "商户审核通过" : "商户审核驳回",
|
Title = request.Approve ? "商户审核通过" : "商户审核驳回",
|
||||||
Description = request.Remarks,
|
Description = request.Remarks,
|
||||||
OperatorId = ResolveOperatorId(),
|
OperatorId = ResolveOperatorId(),
|
||||||
@@ -63,7 +90,7 @@ public sealed class ReviewMerchantCommandHandler(
|
|||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// 5. 返回 DTO
|
// 4. 返回 DTO
|
||||||
return MerchantMapping.ToDto(merchant);
|
return MerchantMapping.ToDto(merchant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销商户审核处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RevokeMerchantReviewHandler(
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
|
ICurrentUserAccessor currentUserAccessor)
|
||||||
|
: IRequestHandler<RevokeMerchantReviewCommand>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Handle(RevokeMerchantReviewCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||||
|
|
||||||
|
if (merchant.Status != MerchantStatus.Approved)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "商户不在已审核状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var actorName = currentUserAccessor.IsAuthenticated
|
||||||
|
? $"user:{currentUserAccessor.UserId}"
|
||||||
|
: "system";
|
||||||
|
|
||||||
|
merchant.Status = MerchantStatus.Pending;
|
||||||
|
merchant.IsFrozen = true;
|
||||||
|
merchant.FrozenReason = request.Reason;
|
||||||
|
merchant.FrozenAt = now;
|
||||||
|
merchant.ReviewRemarks = request.Reason;
|
||||||
|
merchant.LastReviewedAt = now;
|
||||||
|
merchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
|
||||||
|
merchant.ClaimedBy = null;
|
||||||
|
merchant.ClaimedByName = null;
|
||||||
|
merchant.ClaimedAt = null;
|
||||||
|
merchant.ClaimExpiresAt = null;
|
||||||
|
|
||||||
|
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
|
{
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
Action = MerchantAuditAction.ReviewRevoked,
|
||||||
|
Title = "撤销审核",
|
||||||
|
Description = request.Reason,
|
||||||
|
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName
|
||||||
|
}, cancellationToken);
|
||||||
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,16 @@ using MediatR;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Application.Identity;
|
||||||
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||||
@@ -12,51 +21,175 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateMerchantCommandHandler(
|
public sealed class UpdateMerchantCommandHandler(
|
||||||
IMerchantRepository merchantRepository,
|
IMerchantRepository merchantRepository,
|
||||||
|
IStoreRepository storeRepository,
|
||||||
|
ITenantRepository tenantRepository,
|
||||||
ITenantProvider tenantProvider,
|
ITenantProvider tenantProvider,
|
||||||
|
ICurrentUserAccessor currentUserAccessor,
|
||||||
|
IAdminAuthService adminAuthService,
|
||||||
ILogger<UpdateMerchantCommandHandler> logger)
|
ILogger<UpdateMerchantCommandHandler> logger)
|
||||||
: IRequestHandler<UpdateMerchantCommand, MerchantDto?>
|
: IRequestHandler<UpdateMerchantCommand, UpdateMerchantResultDto?>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<MerchantDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
|
public async Task<UpdateMerchantResultDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. 读取现有商户
|
if (request.RowVersion == null || request.RowVersion.Length == 0)
|
||||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
{
|
||||||
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
|
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
|
||||||
if (existing == null)
|
}
|
||||||
|
|
||||||
|
// 1. 获取操作者权限
|
||||||
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||||
|
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||||
|
|
||||||
|
// 2. 读取商户信息
|
||||||
|
var merchant = isSuperAdmin
|
||||||
|
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||||
|
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (merchant == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 更新字段
|
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||||
existing.BrandName = request.BrandName.Trim();
|
{
|
||||||
existing.BrandAlias = request.BrandAlias?.Trim();
|
return null;
|
||||||
existing.LogoUrl = request.LogoUrl?.Trim();
|
|
||||||
existing.Category = request.Category?.Trim();
|
|
||||||
existing.ContactPhone = request.ContactPhone.Trim();
|
|
||||||
existing.ContactEmail = request.ContactEmail?.Trim();
|
|
||||||
existing.Status = request.Status;
|
|
||||||
|
|
||||||
// 3. 持久化
|
|
||||||
await merchantRepository.UpdateMerchantAsync(existing, cancellationToken);
|
|
||||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
|
||||||
logger.LogInformation("更新商户 {MerchantId} - {BrandName}", existing.Id, existing.BrandName);
|
|
||||||
|
|
||||||
// 4. 返回 DTO
|
|
||||||
return MapToDto(existing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MerchantDto MapToDto(Domain.Merchants.Entities.Merchant merchant) => new()
|
// 3. 规范化输入
|
||||||
|
var name = NormalizeRequired(request.Name, "商户名称");
|
||||||
|
var contactPhone = NormalizeRequired(request.ContactPhone, "联系电话");
|
||||||
|
var licenseNumber = NormalizeOptional(request.LicenseNumber);
|
||||||
|
var legalRepresentative = NormalizeOptional(request.LegalRepresentative);
|
||||||
|
var registeredAddress = NormalizeOptional(request.RegisteredAddress);
|
||||||
|
var contactEmail = NormalizeOptional(request.ContactEmail);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var actorId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
|
||||||
|
var actorName = ResolveActorName();
|
||||||
|
var changes = new List<MerchantChangeLog>();
|
||||||
|
var criticalChanged = false;
|
||||||
|
|
||||||
|
TrackChange("name", merchant.BrandName, name, isCritical: true);
|
||||||
|
TrackChange("licenseNumber", merchant.BusinessLicenseNumber, licenseNumber, isCritical: true);
|
||||||
|
TrackChange("legalRepresentative", merchant.LegalPerson, legalRepresentative, isCritical: true);
|
||||||
|
TrackChange("registeredAddress", merchant.Address, registeredAddress, isCritical: true);
|
||||||
|
TrackChange("contactPhone", merchant.ContactPhone, contactPhone, isCritical: false);
|
||||||
|
TrackChange("contactEmail", merchant.ContactEmail, contactEmail, isCritical: false);
|
||||||
|
|
||||||
|
// 4. 写入字段
|
||||||
|
merchant.BrandName = name;
|
||||||
|
merchant.BusinessLicenseNumber = licenseNumber;
|
||||||
|
merchant.LegalPerson = legalRepresentative;
|
||||||
|
merchant.Address = registeredAddress;
|
||||||
|
merchant.ContactPhone = contactPhone;
|
||||||
|
merchant.ContactEmail = contactEmail;
|
||||||
|
merchant.RowVersion = request.RowVersion;
|
||||||
|
|
||||||
|
var requiresReview = merchant.Status == MerchantStatus.Approved && criticalChanged;
|
||||||
|
if (requiresReview)
|
||||||
|
{
|
||||||
|
merchant.Status = MerchantStatus.Pending;
|
||||||
|
merchant.IsFrozen = true;
|
||||||
|
merchant.FrozenReason = "关键信息变更待审核";
|
||||||
|
merchant.FrozenAt = now;
|
||||||
|
}
|
||||||
|
else if (merchant.Status == MerchantStatus.Rejected)
|
||||||
|
{
|
||||||
|
merchant.Status = MerchantStatus.Pending;
|
||||||
|
merchant.IsFrozen = false;
|
||||||
|
merchant.FrozenReason = null;
|
||||||
|
merchant.FrozenAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 持久化日志与数据
|
||||||
|
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
|
||||||
|
foreach (var log in changes)
|
||||||
|
{
|
||||||
|
await merchantRepository.AddChangeLogAsync(log, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresReview)
|
||||||
|
{
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
{
|
{
|
||||||
Id = merchant.Id,
|
|
||||||
TenantId = merchant.TenantId,
|
TenantId = merchant.TenantId,
|
||||||
BrandName = merchant.BrandName,
|
MerchantId = merchant.Id,
|
||||||
BrandAlias = merchant.BrandAlias,
|
Action = MerchantAuditAction.ReviewPendingReApproval,
|
||||||
LogoUrl = merchant.LogoUrl,
|
Title = "关键信息变更待审核",
|
||||||
Category = merchant.Category,
|
Description = "关键信息修改后已进入待审核状态",
|
||||||
ContactPhone = merchant.ContactPhone,
|
OperatorId = actorId,
|
||||||
ContactEmail = merchant.ContactEmail,
|
OperatorName = actorName
|
||||||
Status = merchant.Status,
|
}, cancellationToken);
|
||||||
JoinedAt = merchant.JoinedAt,
|
}
|
||||||
CreatedAt = merchant.CreatedAt
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (IsConcurrencyException(exception))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "商户信息已被修改,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("更新商户 {MerchantId} - {Name}", merchant.Id, merchant.BrandName);
|
||||||
|
|
||||||
|
// 6. 返回更新结果
|
||||||
|
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
|
||||||
|
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);
|
||||||
|
var detail = MerchantMapping.ToDetailDto(merchant, tenant?.Name, MerchantMapping.ToStoreDtos(stores));
|
||||||
|
|
||||||
|
return new UpdateMerchantResultDto
|
||||||
|
{
|
||||||
|
Merchant = detail,
|
||||||
|
RequiresReview = requiresReview
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void TrackChange(string fieldName, string? oldValue, string? newValue, bool isCritical)
|
||||||
|
{
|
||||||
|
var normalizedOld = NormalizeOptional(oldValue);
|
||||||
|
var normalizedNew = NormalizeOptional(newValue);
|
||||||
|
if (string.Equals(normalizedOld, normalizedNew, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCritical)
|
||||||
|
{
|
||||||
|
criticalChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.Add(new MerchantChangeLog
|
||||||
|
{
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
FieldName = fieldName,
|
||||||
|
OldValue = normalizedOld,
|
||||||
|
NewValue = normalizedNew,
|
||||||
|
ChangedBy = actorId,
|
||||||
|
ChangedByName = actorName,
|
||||||
|
ChangeType = "Update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeRequired(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.ValidationFailed, $"{fieldName}不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptional(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
private string ResolveActorName()
|
||||||
|
=> currentUserAccessor.IsAuthenticated ? $"user:{currentUserAccessor.UserId}" : "system";
|
||||||
|
|
||||||
|
private static bool IsConcurrencyException(Exception exception)
|
||||||
|
=> string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Merchants;
|
namespace TakeoutSaaS.Application.App.Merchants;
|
||||||
|
|
||||||
@@ -28,6 +29,53 @@ internal static class MerchantMapping
|
|||||||
CreatedAt = merchant.CreatedAt
|
CreatedAt = merchant.CreatedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将商户实体映射为列表项 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public static MerchantListItemDto ToListItemDto(Merchant merchant, string? tenantName, int storeCount) => new()
|
||||||
|
{
|
||||||
|
Id = merchant.Id,
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
TenantName = tenantName,
|
||||||
|
Name = merchant.BrandName,
|
||||||
|
OperatingMode = merchant.OperatingMode,
|
||||||
|
LicenseNumber = merchant.BusinessLicenseNumber,
|
||||||
|
Status = merchant.Status,
|
||||||
|
IsFrozen = merchant.IsFrozen,
|
||||||
|
StoreCount = storeCount,
|
||||||
|
CreatedAt = merchant.CreatedAt,
|
||||||
|
UpdatedAt = merchant.UpdatedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将商户实体映射为详情 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public static MerchantDetailDto ToDetailDto(Merchant merchant, string? tenantName, IReadOnlyList<StoreDto> stores) => new()
|
||||||
|
{
|
||||||
|
Id = merchant.Id,
|
||||||
|
TenantId = merchant.TenantId,
|
||||||
|
TenantName = tenantName,
|
||||||
|
Name = merchant.BrandName,
|
||||||
|
OperatingMode = merchant.OperatingMode,
|
||||||
|
LicenseNumber = merchant.BusinessLicenseNumber,
|
||||||
|
LegalRepresentative = merchant.LegalPerson,
|
||||||
|
RegisteredAddress = merchant.Address,
|
||||||
|
ContactPhone = merchant.ContactPhone,
|
||||||
|
ContactEmail = merchant.ContactEmail,
|
||||||
|
Status = merchant.Status,
|
||||||
|
IsFrozen = merchant.IsFrozen,
|
||||||
|
FrozenReason = merchant.FrozenReason,
|
||||||
|
FrozenAt = merchant.FrozenAt,
|
||||||
|
ApprovedBy = merchant.ApprovedBy,
|
||||||
|
ApprovedAt = merchant.ApprovedAt,
|
||||||
|
Stores = stores,
|
||||||
|
RowVersion = merchant.RowVersion,
|
||||||
|
CreatedAt = merchant.CreatedAt,
|
||||||
|
CreatedBy = merchant.CreatedBy,
|
||||||
|
UpdatedAt = merchant.UpdatedAt,
|
||||||
|
UpdatedBy = merchant.UpdatedBy
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将商户证照实体映射为 DTO。
|
/// 将商户证照实体映射为 DTO。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,12 +124,42 @@ internal static class MerchantMapping
|
|||||||
Id = log.Id,
|
Id = log.Id,
|
||||||
MerchantId = log.MerchantId,
|
MerchantId = log.MerchantId,
|
||||||
Action = log.Action,
|
Action = log.Action,
|
||||||
|
OperatorId = log.OperatorId,
|
||||||
Title = log.Title,
|
Title = log.Title,
|
||||||
Description = log.Description,
|
Description = log.Description,
|
||||||
OperatorName = log.OperatorName,
|
OperatorName = log.OperatorName,
|
||||||
|
IpAddress = log.IpAddress,
|
||||||
CreatedAt = log.CreatedAt
|
CreatedAt = log.CreatedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将商户变更日志实体映射为 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public static MerchantChangeLogDto ToDto(MerchantChangeLog log) => new()
|
||||||
|
{
|
||||||
|
Id = log.Id,
|
||||||
|
FieldName = log.FieldName,
|
||||||
|
OldValue = log.OldValue,
|
||||||
|
NewValue = log.NewValue,
|
||||||
|
ChangedBy = log.ChangedBy,
|
||||||
|
ChangedByName = log.ChangedByName,
|
||||||
|
ChangedAt = log.CreatedAt,
|
||||||
|
ChangeReason = log.ChangeReason
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将门店实体映射为 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public static StoreDto ToStoreDto(Store store) => new()
|
||||||
|
{
|
||||||
|
Id = store.Id,
|
||||||
|
Name = store.Name,
|
||||||
|
LicenseNumber = store.BusinessLicenseNumber,
|
||||||
|
ContactPhone = store.Phone,
|
||||||
|
Address = store.Address,
|
||||||
|
Status = store.Status
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将商户分类实体映射为 DTO。
|
/// 将商户分类实体映射为 DTO。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -119,4 +197,10 @@ internal static class MerchantMapping
|
|||||||
/// <returns>分类 DTO 列表。</returns>
|
/// <returns>分类 DTO 列表。</returns>
|
||||||
public static IReadOnlyList<MerchantCategoryDto> ToCategoryDtos(IEnumerable<MerchantCategory> categories)
|
public static IReadOnlyList<MerchantCategoryDto> ToCategoryDtos(IEnumerable<MerchantCategory> categories)
|
||||||
=> categories.Select(ToDto).ToList();
|
=> categories.Select(ToDto).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将门店集合映射为 DTO 集合。
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<StoreDto> ToStoreDtos(IEnumerable<Store> stores)
|
||||||
|
=> stores.Select(ToStoreDto).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出商户 PDF 查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportMerchantPdfQuery(long MerchantId) : IRequest<byte[]>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户审核历史查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetMerchantAuditHistoryQuery(long MerchantId) : IRequest<IReadOnlyList<MerchantAuditLogDto>>;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商户变更历史。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetMerchantChangeHistoryQuery(long MerchantId, string? FieldName = null)
|
||||||
|
: IRequest<IReadOnlyList<MerchantChangeLogDto>>;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetMerchantListQuery : IRequest<PagedResult<MerchantListItemDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词(商户名称/营业执照号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态过滤。
|
||||||
|
/// </summary>
|
||||||
|
public MerchantStatus? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式过滤。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户过滤(管理员可用)。
|
||||||
|
/// </summary>
|
||||||
|
public long? TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段(createdAt/updatedAt/name/status)。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortBy { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序方向(asc/desc)。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortOrder { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取商户审核领取信息查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record GetMerchantReviewClaimQuery(long MerchantId) : IRequest<ClaimInfoDto?>;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待审核商户列表查询。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPendingReviewListQuery : IRequest<PagedResult<MerchantReviewListItemDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 关键词(商户名称/营业执照号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式筛选。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户筛选。
|
||||||
|
/// </summary>
|
||||||
|
public long? TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Merchants.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户审核命令验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReviewMerchantValidator : AbstractValidator<ReviewMerchantCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReviewMerchantValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.MerchantId).GreaterThan(0);
|
||||||
|
RuleFor(x => x.Remarks)
|
||||||
|
.NotEmpty()
|
||||||
|
.When(x => !x.Approve);
|
||||||
|
RuleFor(x => x.Remarks)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.Remarks));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,13 @@ public sealed class UpdateMerchantCommandValidator : AbstractValidator<UpdateMer
|
|||||||
public UpdateMerchantCommandValidator()
|
public UpdateMerchantCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.MerchantId).GreaterThan(0);
|
RuleFor(x => x.MerchantId).GreaterThan(0);
|
||||||
RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||||
RuleFor(x => x.BrandAlias).MaximumLength(64);
|
RuleFor(x => x.LicenseNumber).MaximumLength(64);
|
||||||
RuleFor(x => x.LogoUrl).MaximumLength(256);
|
RuleFor(x => x.LegalRepresentative).MaximumLength(64);
|
||||||
RuleFor(x => x.Category).MaximumLength(64);
|
RuleFor(x => x.RegisteredAddress).MaximumLength(256);
|
||||||
RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32);
|
RuleFor(x => x.ContactPhone).NotEmpty().MaximumLength(32);
|
||||||
RuleFor(x => x.ContactEmail).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ContactEmail));
|
RuleFor(x => x.ContactEmail).EmailAddress().MaximumLength(128)
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.ContactEmail));
|
||||||
|
RuleFor(x => x.RowVersion).NotEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
@@ -29,4 +30,9 @@ public sealed record ReviewTenantCommand : IRequest<TenantDto>
|
|||||||
/// 审核通过后续费时长(月)。
|
/// 审核通过后续费时长(月)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? RenewMonths { get; init; }
|
public int? RenewMonths { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式(审核通过时必填)。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ public sealed class TenantDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TenantVerificationStatus VerificationStatus { get; init; }
|
public TenantVerificationStatus VerificationStatus { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当前套餐 ID。
|
/// 当前套餐 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
using TakeoutSaaS.Domain.Tenants.Repositories;
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
@@ -14,6 +17,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ReviewTenantCommandHandler(
|
public sealed class ReviewTenantCommandHandler(
|
||||||
ITenantRepository tenantRepository,
|
ITenantRepository tenantRepository,
|
||||||
|
IMerchantRepository merchantRepository,
|
||||||
ICurrentUserAccessor currentUserAccessor)
|
ICurrentUserAccessor currentUserAccessor)
|
||||||
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
||||||
{
|
{
|
||||||
@@ -53,19 +57,25 @@ public sealed class ReviewTenantCommandHandler(
|
|||||||
// 4. 更新租户与订阅状态
|
// 4. 更新租户与订阅状态
|
||||||
if (request.Approve)
|
if (request.Approve)
|
||||||
{
|
{
|
||||||
|
if (!request.OperatingMode.HasValue)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.ValidationFailed, "审核通过时必须选择经营模式");
|
||||||
|
}
|
||||||
|
|
||||||
var renewMonths = request.RenewMonths ?? 0;
|
var renewMonths = request.RenewMonths ?? 0;
|
||||||
if (renewMonths <= 0)
|
if (renewMonths <= 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.ValidationFailed, "续费时长必须为正整数(月)");
|
throw new BusinessException(ErrorCodes.ValidationFailed, "续费时长必须为正整数(月)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
verification.Status = TenantVerificationStatus.Approved;
|
verification.Status = TenantVerificationStatus.Approved;
|
||||||
tenant.Status = TenantStatus.Active;
|
tenant.Status = TenantStatus.Active;
|
||||||
|
tenant.OperatingMode = request.OperatingMode;
|
||||||
if (subscription != null)
|
if (subscription != null)
|
||||||
{
|
{
|
||||||
subscription.Status = SubscriptionStatus.Active;
|
subscription.Status = SubscriptionStatus.Active;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now)
|
if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now)
|
||||||
{
|
{
|
||||||
subscription.EffectiveFrom = now;
|
subscription.EffectiveFrom = now;
|
||||||
@@ -92,6 +102,69 @@ public sealed class ReviewTenantCommandHandler(
|
|||||||
{
|
{
|
||||||
throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法续费");
|
throw new BusinessException(ErrorCodes.BadRequest, "订阅不存在,无法续费");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingMerchant = await merchantRepository.FindByTenantIdAsync(tenant.Id, cancellationToken);
|
||||||
|
if (existingMerchant == null)
|
||||||
|
{
|
||||||
|
var merchant = new Merchant
|
||||||
|
{
|
||||||
|
TenantId = tenant.Id,
|
||||||
|
BrandName = tenant.Name,
|
||||||
|
BrandAlias = tenant.ShortName,
|
||||||
|
Category = tenant.Industry,
|
||||||
|
ContactPhone = tenant.ContactPhone ?? string.Empty,
|
||||||
|
ContactEmail = tenant.ContactEmail,
|
||||||
|
BusinessLicenseNumber = verification.BusinessLicenseNumber,
|
||||||
|
BusinessLicenseImageUrl = verification.BusinessLicenseUrl,
|
||||||
|
LegalPerson = verification.LegalPersonName,
|
||||||
|
Province = tenant.Province,
|
||||||
|
City = tenant.City,
|
||||||
|
Address = tenant.Address,
|
||||||
|
Status = MerchantStatus.Approved,
|
||||||
|
OperatingMode = request.OperatingMode,
|
||||||
|
ApprovedAt = now,
|
||||||
|
ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
JoinedAt = now,
|
||||||
|
LastReviewedAt = now,
|
||||||
|
LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
IsFrozen = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await merchantRepository.AddMerchantAsync(merchant, cancellationToken);
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
|
{
|
||||||
|
TenantId = tenant.Id,
|
||||||
|
MerchantId = merchant.Id,
|
||||||
|
Action = MerchantAuditAction.ReviewApproved,
|
||||||
|
Title = "商户审核通过",
|
||||||
|
Description = request.Reason,
|
||||||
|
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingMerchant.Status = MerchantStatus.Approved;
|
||||||
|
existingMerchant.OperatingMode = request.OperatingMode;
|
||||||
|
existingMerchant.ApprovedAt = now;
|
||||||
|
existingMerchant.ApprovedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
|
||||||
|
existingMerchant.LastReviewedAt = now;
|
||||||
|
existingMerchant.LastReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId;
|
||||||
|
existingMerchant.IsFrozen = false;
|
||||||
|
existingMerchant.FrozenReason = null;
|
||||||
|
existingMerchant.FrozenAt = null;
|
||||||
|
await merchantRepository.UpdateMerchantAsync(existingMerchant, cancellationToken);
|
||||||
|
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
|
||||||
|
{
|
||||||
|
TenantId = tenant.Id,
|
||||||
|
MerchantId = existingMerchant.Id,
|
||||||
|
Action = MerchantAuditAction.ReviewApproved,
|
||||||
|
Title = "商户审核通过",
|
||||||
|
Description = request.Reason,
|
||||||
|
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
|
||||||
|
OperatorName = actorName
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -141,6 +214,7 @@ public sealed class ReviewTenantCommandHandler(
|
|||||||
|
|
||||||
// 8. 保存并返回 DTO
|
// 8. 保存并返回 DTO
|
||||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return TenantMapping.ToDto(tenant, subscription, verification);
|
return TenantMapping.ToDto(tenant, subscription, verification);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ internal static class TenantMapping
|
|||||||
ContactEmail = tenant.ContactEmail,
|
ContactEmail = tenant.ContactEmail,
|
||||||
Status = tenant.Status,
|
Status = tenant.Status,
|
||||||
VerificationStatus = verification?.Status ?? Domain.Tenants.Enums.TenantVerificationStatus.Draft,
|
VerificationStatus = verification?.Status ?? Domain.Tenants.Enums.TenantVerificationStatus.Draft,
|
||||||
|
OperatingMode = tenant.OperatingMode,
|
||||||
CurrentPackageId = subscription?.TenantPackageId,
|
CurrentPackageId = subscription?.TenantPackageId,
|
||||||
EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom,
|
EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom,
|
||||||
EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo,
|
EffectiveTo = subscription?.EffectiveTo ?? tenant.EffectiveTo,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Tenants.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 租户审核命令验证器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReviewTenantValidator : AbstractValidator<ReviewTenantCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化验证规则。
|
||||||
|
/// </summary>
|
||||||
|
public ReviewTenantValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.TenantId).GreaterThan(0);
|
||||||
|
RuleFor(x => x.Reason)
|
||||||
|
.NotEmpty()
|
||||||
|
.When(x => !x.Approve);
|
||||||
|
RuleFor(x => x.OperatingMode)
|
||||||
|
.NotNull()
|
||||||
|
.When(x => x.Approve);
|
||||||
|
RuleFor(x => x.RenewMonths)
|
||||||
|
.NotNull()
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.Approve);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,5 +8,5 @@ public static class MenuPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否允许维护菜单(创建/更新/删除)。
|
/// 是否允许维护菜单(创建/更新/删除)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const bool CanMaintainMenus = false;
|
public static bool CanMaintainMenus { get; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Domain/TakeoutSaaS.Domain/Common/Enums/OperatingMode.cs
Normal file
17
src/Domain/TakeoutSaaS.Domain/Common/Enums/OperatingMode.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式。
|
||||||
|
/// </summary>
|
||||||
|
public enum OperatingMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 同一主体。
|
||||||
|
/// </summary>
|
||||||
|
SameEntity = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 不同主体。
|
||||||
|
/// </summary>
|
||||||
|
DifferentEntity = 2
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
@@ -103,6 +104,31 @@ public sealed class Merchant : MultiTenantEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public MerchantStatus Status { get; set; } = MerchantStatus.Pending;
|
public MerchantStatus Status { get; set; } = MerchantStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式(同一主体/不同主体)。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否冻结业务。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFrozen { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? FrozenReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 冻结时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FrozenAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最近一次审核人。
|
||||||
|
/// </summary>
|
||||||
|
public long? LastReviewedBy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 审核备注或驳回原因。
|
/// 审核备注或驳回原因。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -117,4 +143,39 @@ public sealed class Merchant : MultiTenantEntityBase
|
|||||||
/// 最近一次审核时间。
|
/// 最近一次审核时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? LastReviewedAt { get; set; }
|
public DateTime? LastReviewedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过人。
|
||||||
|
/// </summary>
|
||||||
|
public long? ApprovedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ApprovedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前领取人。
|
||||||
|
/// </summary>
|
||||||
|
public long? ClaimedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前领取人姓名。
|
||||||
|
/// </summary>
|
||||||
|
public string? ClaimedByName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取过期时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ClaimExpiresAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 并发控制版本。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,9 @@ public sealed class MerchantAuditLog : MultiTenantEntityBase
|
|||||||
/// 操作人名称。
|
/// 操作人名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? OperatorName { get; set; }
|
public string? OperatorName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作 IP。
|
||||||
|
/// </summary>
|
||||||
|
public string? IpAddress { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户变更日志。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MerchantChangeLog : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 商户标识。
|
||||||
|
/// </summary>
|
||||||
|
public long MerchantId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更字段名。
|
||||||
|
/// </summary>
|
||||||
|
public string FieldName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更前值。
|
||||||
|
/// </summary>
|
||||||
|
public string? OldValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更后值。
|
||||||
|
/// </summary>
|
||||||
|
public string? NewValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更类型。
|
||||||
|
/// </summary>
|
||||||
|
public string ChangeType { get; set; } = "Update";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更人 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long? ChangedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更人名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChangedByName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更原因。
|
||||||
|
/// </summary>
|
||||||
|
public string? ChangeReason { get; set; }
|
||||||
|
}
|
||||||
@@ -33,5 +33,40 @@ public enum MerchantAuditAction
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 商户审核结果。
|
/// 商户审核结果。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
MerchantReviewed = 5
|
MerchantReviewed = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 领取审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewClaimed = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewReleased = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核通过。
|
||||||
|
/// </summary>
|
||||||
|
ReviewApproved = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审核驳回。
|
||||||
|
/// </summary>
|
||||||
|
ReviewRejected = 9,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewRevoked = 10,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键信息变更进入待审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewPendingReApproval = 11,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 强制接管审核。
|
||||||
|
/// </summary>
|
||||||
|
ReviewForceClaimed = 12
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
|
||||||
@@ -17,6 +18,22 @@ public interface IMerchantRepository
|
|||||||
/// <returns>商户实体或 null。</returns>
|
/// <returns>商户实体或 null。</returns>
|
||||||
Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 依据标识获取商户(忽略租户过滤)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="merchantId">商户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>商户实体或 null。</returns>
|
||||||
|
Task<Merchant?> FindByIdAsync(long merchantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 依据租户标识获取商户(忽略租户过滤)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>商户实体或 null。</returns>
|
||||||
|
Task<Merchant?> FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按状态筛选商户列表。
|
/// 按状态筛选商户列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -26,6 +43,22 @@ public interface IMerchantRepository
|
|||||||
/// <returns>商户集合。</returns>
|
/// <returns>商户集合。</returns>
|
||||||
Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按条件筛选商户列表(支持跨租户)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">租户 ID,为 null 时查询全部租户。</param>
|
||||||
|
/// <param name="status">状态过滤。</param>
|
||||||
|
/// <param name="operatingMode">经营模式过滤。</param>
|
||||||
|
/// <param name="keyword">关键词过滤。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>商户集合。</returns>
|
||||||
|
Task<IReadOnlyList<Merchant>> SearchAsync(
|
||||||
|
long? tenantId,
|
||||||
|
MerchantStatus? status,
|
||||||
|
OperatingMode? operatingMode,
|
||||||
|
string? keyword,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定商户的员工列表。
|
/// 获取指定商户的员工列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -168,6 +201,14 @@ public interface IMerchantRepository
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
Task AddAuditLogAsync(MerchantAuditLog log, CancellationToken cancellationToken = default);
|
Task AddAuditLogAsync(MerchantAuditLog log, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录变更日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="log">变更日志实体。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>异步任务。</returns>
|
||||||
|
Task AddChangeLogAsync(MerchantChangeLog log, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取审核日志。
|
/// 获取审核日志。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -176,4 +217,18 @@ public interface IMerchantRepository
|
|||||||
/// <param name="cancellationToken">取消标记。</param>
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
/// <returns>审核日志列表。</returns>
|
/// <returns>审核日志列表。</returns>
|
||||||
Task<IReadOnlyList<MerchantAuditLog>> GetAuditLogsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<MerchantAuditLog>> GetAuditLogsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取变更日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="merchantId">商户 ID。</param>
|
||||||
|
/// <param name="tenantId">租户 ID。</param>
|
||||||
|
/// <param name="fieldName">字段过滤。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>变更日志列表。</returns>
|
||||||
|
Task<IReadOnlyList<MerchantChangeLog>> GetChangeLogsAsync(
|
||||||
|
long merchantId,
|
||||||
|
long tenantId,
|
||||||
|
string? fieldName = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Merchants.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户导出服务接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface IMerchantExportService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 导出为 PDF。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="merchant">商户主体。</param>
|
||||||
|
/// <param name="tenantName">租户名称。</param>
|
||||||
|
/// <param name="stores">门店列表。</param>
|
||||||
|
/// <param name="auditLogs">审核历史。</param>
|
||||||
|
/// <param name="cancellationToken">取消标记。</param>
|
||||||
|
/// <returns>PDF 字节数组。</returns>
|
||||||
|
Task<byte[]> ExportToPdfAsync(
|
||||||
|
Merchant merchant,
|
||||||
|
string? tenantName,
|
||||||
|
IReadOnlyList<Store> stores,
|
||||||
|
IReadOnlyList<MerchantAuditLog> auditLogs,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -33,6 +33,26 @@ public sealed class Store : MultiTenantEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ManagerName { get; set; }
|
public string? ManagerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店营业执照号(主体不一致模式使用)。
|
||||||
|
/// </summary>
|
||||||
|
public string? BusinessLicenseNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店法人(主体不一致模式使用)。
|
||||||
|
/// </summary>
|
||||||
|
public string? LegalRepresentative { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店注册地址(主体不一致模式使用)。
|
||||||
|
/// </summary>
|
||||||
|
public string? RegisteredAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店营业执照图片地址(主体不一致模式使用)。
|
||||||
|
/// </summary>
|
||||||
|
public string? BusinessLicenseImageUrl { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 门店当前运营状态。
|
/// 门店当前运营状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -13,11 +13,21 @@ public interface IStoreRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定商户的门店列表。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Store>> GetByMerchantIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按租户筛选门店列表。
|
/// 按租户筛选门店列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定商户集合的门店数量。
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<long, int>> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection<long> merchantIds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取门店营业时段。
|
/// 获取门店营业时段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
@@ -93,6 +94,11 @@ public sealed class Tenant : AuditableEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TenantStatus Status { get; set; } = TenantStatus.PendingReview;
|
public TenantStatus Status { get; set; } = TenantStatus.PendingReview;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 经营模式(同一主体/不同主体)。
|
||||||
|
/// </summary>
|
||||||
|
public OperatingMode? OperatingMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务生效时间(UTC)。
|
/// 服务生效时间(UTC)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||||
using TakeoutSaaS.Domain.Inventory.Repositories;
|
using TakeoutSaaS.Domain.Inventory.Repositories;
|
||||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Services;
|
||||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||||
using TakeoutSaaS.Domain.Payments.Repositories;
|
using TakeoutSaaS.Domain.Payments.Repositories;
|
||||||
using TakeoutSaaS.Domain.Products.Repositories;
|
using TakeoutSaaS.Domain.Products.Repositories;
|
||||||
@@ -63,6 +64,7 @@ public static class AppServiceCollectionExtensions
|
|||||||
// 1. 账单领域/导出服务
|
// 1. 账单领域/导出服务
|
||||||
services.AddScoped<IBillingDomainService, BillingDomainService>();
|
services.AddScoped<IBillingDomainService, BillingDomainService>();
|
||||||
services.AddScoped<IBillingExportService, BillingExportService>();
|
services.AddScoped<IBillingExportService, BillingExportService>();
|
||||||
|
services.AddScoped<IMerchantExportService, MerchantExportService>();
|
||||||
|
|
||||||
services.AddOptions<AppSeedOptions>()
|
services.AddOptions<AppSeedOptions>()
|
||||||
.Bind(configuration.GetSection(AppSeedOptions.SectionName))
|
.Bind(configuration.GetSection(AppSeedOptions.SectionName))
|
||||||
|
|||||||
@@ -469,6 +469,7 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.Industry).HasMaxLength(64);
|
builder.Property(x => x.Industry).HasMaxLength(64);
|
||||||
builder.Property(x => x.LogoUrl).HasColumnType("text");
|
builder.Property(x => x.LogoUrl).HasColumnType("text");
|
||||||
builder.Property(x => x.Remarks).HasMaxLength(512);
|
builder.Property(x => x.Remarks).HasMaxLength(512);
|
||||||
|
builder.Property(x => x.OperatingMode).HasConversion<int>();
|
||||||
builder.HasIndex(x => x.Code).IsUnique();
|
builder.HasIndex(x => x.Code).IsUnique();
|
||||||
builder.HasIndex(x => x.ContactPhone).IsUnique();
|
builder.HasIndex(x => x.ContactPhone).IsUnique();
|
||||||
}
|
}
|
||||||
@@ -533,7 +534,17 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.District).HasMaxLength(64);
|
builder.Property(x => x.District).HasMaxLength(64);
|
||||||
builder.Property(x => x.Address).HasMaxLength(256);
|
builder.Property(x => x.Address).HasMaxLength(256);
|
||||||
builder.Property(x => x.ReviewRemarks).HasMaxLength(512);
|
builder.Property(x => x.ReviewRemarks).HasMaxLength(512);
|
||||||
|
builder.Property(x => x.OperatingMode).HasConversion<int>();
|
||||||
|
builder.Property(x => x.IsFrozen).HasDefaultValue(false);
|
||||||
|
builder.Property(x => x.FrozenReason).HasMaxLength(500);
|
||||||
|
builder.Property(x => x.ClaimedByName).HasMaxLength(100);
|
||||||
|
builder.Property(x => x.RowVersion)
|
||||||
|
.IsRowVersion()
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("bytea");
|
||||||
builder.HasIndex(x => x.TenantId);
|
builder.HasIndex(x => x.TenantId);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.Status });
|
||||||
|
builder.HasIndex(x => x.ClaimedBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureStore(EntityTypeBuilder<Store> builder)
|
private static void ConfigureStore(EntityTypeBuilder<Store> builder)
|
||||||
@@ -544,6 +555,10 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
builder.Property(x => x.Phone).HasMaxLength(32);
|
builder.Property(x => x.Phone).HasMaxLength(32);
|
||||||
builder.Property(x => x.ManagerName).HasMaxLength(64);
|
builder.Property(x => x.ManagerName).HasMaxLength(64);
|
||||||
|
builder.Property(x => x.BusinessLicenseNumber).HasMaxLength(50);
|
||||||
|
builder.Property(x => x.LegalRepresentative).HasMaxLength(100);
|
||||||
|
builder.Property(x => x.RegisteredAddress).HasMaxLength(500);
|
||||||
|
builder.Property(x => x.BusinessLicenseImageUrl).HasMaxLength(500);
|
||||||
builder.Property(x => x.Province).HasMaxLength(64);
|
builder.Property(x => x.Province).HasMaxLength(64);
|
||||||
builder.Property(x => x.City).HasMaxLength(64);
|
builder.Property(x => x.City).HasMaxLength(64);
|
||||||
builder.Property(x => x.District).HasMaxLength(64);
|
builder.Property(x => x.District).HasMaxLength(64);
|
||||||
@@ -553,6 +568,9 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.DeliveryRadiusKm).HasPrecision(6, 2);
|
builder.Property(x => x.DeliveryRadiusKm).HasPrecision(6, 2);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.MerchantId });
|
builder.HasIndex(x => new { x.TenantId, x.MerchantId });
|
||||||
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.MerchantId, x.BusinessLicenseNumber })
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureProductCategory(EntityTypeBuilder<ProductCategory> builder)
|
private static void ConfigureProductCategory(EntityTypeBuilder<ProductCategory> builder)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||||
@@ -24,6 +25,26 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog
|
|||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Merchant?> FindByIdAsync(long merchantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.Merchants
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == merchantId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<Merchant?> FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.Merchants
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -208,9 +229,79 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog
|
|||||||
public async Task<IReadOnlyList<MerchantAuditLog>> GetAuditLogsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<MerchantAuditLog>> GetAuditLogsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await logsContext.MerchantAuditLogs
|
return await logsContext.MerchantAuditLogs
|
||||||
|
.IgnoreQueryFilters()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||||
.OrderByDescending(x => x.CreatedAt)
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<Merchant>> SearchAsync(
|
||||||
|
long? tenantId,
|
||||||
|
MerchantStatus? status,
|
||||||
|
OperatingMode? operatingMode,
|
||||||
|
string? keyword,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = context.Merchants
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (tenantId.HasValue && tenantId.Value > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Status == status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operatingMode.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.OperatingMode == operatingMode.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
var normalized = keyword.Trim();
|
||||||
|
query = query.Where(x =>
|
||||||
|
EF.Functions.ILike(x.BrandName, $"%{normalized}%") ||
|
||||||
|
EF.Functions.ILike(x.BusinessLicenseNumber ?? string.Empty, $"%{normalized}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddChangeLogAsync(MerchantChangeLog log, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return logsContext.MerchantChangeLogs.AddAsync(log, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MerchantChangeLog>> GetChangeLogsAsync(
|
||||||
|
long merchantId,
|
||||||
|
long tenantId,
|
||||||
|
string? fieldName = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = logsContext.MerchantChangeLogs
|
||||||
|
.IgnoreQueryFilters()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fieldName))
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.FieldName == fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderByDescending(x => x.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
|||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<Store>> GetByMerchantIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await context.Stores
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||||
|
.OrderBy(x => x.Name)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -44,6 +54,31 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
|||||||
return stores;
|
return stores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Dictionary<long, int>> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection<long> merchantIds, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (merchantIds.Count == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<long, int>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = context.Stores.AsNoTracking();
|
||||||
|
if (!tenantId.HasValue || tenantId.Value <= 0)
|
||||||
|
{
|
||||||
|
query = query.IgnoreQueryFilters();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.Where(x => merchantIds.Contains(x.MerchantId))
|
||||||
|
.GroupBy(x => x.MerchantId)
|
||||||
|
.Select(group => new { group.Key, Count = group.Count() })
|
||||||
|
.ToDictionaryAsync(x => x.Key, x => x.Count, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using QuestPDF.Fluent;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
using System.Globalization;
|
||||||
|
using TakeoutSaaS.Domain.Common.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户导出服务实现(PDF)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MerchantExportService : IMerchantExportService
|
||||||
|
{
|
||||||
|
public MerchantExportService()
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<byte[]> ExportToPdfAsync(
|
||||||
|
Merchant merchant,
|
||||||
|
string? tenantName,
|
||||||
|
IReadOnlyList<Store> stores,
|
||||||
|
IReadOnlyList<MerchantAuditLog> auditLogs,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(merchant);
|
||||||
|
|
||||||
|
var safeStores = stores ?? Array.Empty<Store>();
|
||||||
|
var safeAuditLogs = auditLogs ?? Array.Empty<MerchantAuditLog>();
|
||||||
|
|
||||||
|
var document = Document.Create(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.A4);
|
||||||
|
page.Margin(24);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(10));
|
||||||
|
|
||||||
|
page.Content().Column(column =>
|
||||||
|
{
|
||||||
|
column.Spacing(10);
|
||||||
|
column.Item().Text("Merchant Export").FontSize(16).SemiBold();
|
||||||
|
|
||||||
|
column.Item().Element(section => BuildBasicSection(section, merchant, tenantName));
|
||||||
|
column.Item().Element(section => BuildStoresSection(section, safeStores, cancellationToken));
|
||||||
|
column.Item().Element(section => BuildAuditSection(section, safeAuditLogs, cancellationToken));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(document.GeneratePdf());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildBasicSection(IContainer container, Merchant merchant, string? tenantName)
|
||||||
|
{
|
||||||
|
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
|
||||||
|
{
|
||||||
|
column.Spacing(4);
|
||||||
|
column.Item().Text("Basic Information").SemiBold();
|
||||||
|
column.Item().Text($"Merchant: {merchant.BrandName}");
|
||||||
|
column.Item().Text($"Tenant: {tenantName ?? "-"} (ID: {merchant.TenantId})");
|
||||||
|
column.Item().Text($"Operating Mode: {ResolveOperatingMode(merchant.OperatingMode)}");
|
||||||
|
column.Item().Text($"Status: {merchant.Status}");
|
||||||
|
column.Item().Text($"Frozen: {(merchant.IsFrozen ? "Yes" : "No")}");
|
||||||
|
column.Item().Text($"License Number: {merchant.BusinessLicenseNumber ?? "-"}");
|
||||||
|
column.Item().Text($"Legal Representative: {merchant.LegalPerson ?? "-"}");
|
||||||
|
column.Item().Text($"Registered Address: {merchant.Address ?? "-"}");
|
||||||
|
column.Item().Text($"Contact Phone: {merchant.ContactPhone}");
|
||||||
|
column.Item().Text($"Contact Email: {merchant.ContactEmail ?? "-"}");
|
||||||
|
column.Item().Text($"Approved At: {FormatDateTime(merchant.ApprovedAt)}");
|
||||||
|
column.Item().Text($"Approved By: {merchant.ApprovedBy?.ToString() ?? "-"}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildStoresSection(IContainer container, IReadOnlyList<Store> stores, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
|
||||||
|
{
|
||||||
|
column.Spacing(4);
|
||||||
|
column.Item().Text("Stores").SemiBold();
|
||||||
|
|
||||||
|
if (stores.Count == 0)
|
||||||
|
{
|
||||||
|
column.Item().Text("No stores.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < stores.Count; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var store = stores[i];
|
||||||
|
column.Item().Text($"{i + 1}. {store.Name} | {ResolveStoreStatus(store.Status)} | {store.Address ?? "-"} | {store.Phone ?? "-"}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildAuditSection(IContainer container, IReadOnlyList<MerchantAuditLog> auditLogs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column =>
|
||||||
|
{
|
||||||
|
column.Spacing(4);
|
||||||
|
column.Item().Text("Audit History").SemiBold();
|
||||||
|
|
||||||
|
if (auditLogs.Count == 0)
|
||||||
|
{
|
||||||
|
column.Item().Text("No audit records.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < auditLogs.Count; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var log = auditLogs[i];
|
||||||
|
var title = string.IsNullOrWhiteSpace(log.Title) ? log.Action.ToString() : log.Title;
|
||||||
|
column.Item().Text($"{i + 1}. {title} | {log.OperatorName ?? "-"} | {FormatDateTime(log.CreatedAt)}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(log.Description))
|
||||||
|
{
|
||||||
|
column.Item().Text($" {log.Description}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveOperatingMode(OperatingMode? mode)
|
||||||
|
=> mode switch
|
||||||
|
{
|
||||||
|
OperatingMode.SameEntity => "SameEntity",
|
||||||
|
OperatingMode.DifferentEntity => "DifferentEntity",
|
||||||
|
_ => "-"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ResolveStoreStatus(StoreStatus status)
|
||||||
|
=> status switch
|
||||||
|
{
|
||||||
|
StoreStatus.Closed => "Closed",
|
||||||
|
StoreStatus.Preparing => "Preparing",
|
||||||
|
StoreStatus.Operating => "Operating",
|
||||||
|
StoreStatus.Suspended => "Suspended",
|
||||||
|
_ => status.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatDateTime(DateTime? value)
|
||||||
|
=> value.HasValue ? value.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : "-";
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ public sealed class TakeoutLogsDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<MerchantAuditLog> MerchantAuditLogs => Set<MerchantAuditLog>();
|
public DbSet<MerchantAuditLog> MerchantAuditLogs => Set<MerchantAuditLog>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商户变更日志集合。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<MerchantChangeLog> MerchantChangeLogs => Set<MerchantChangeLog>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 运营操作日志集合。
|
/// 运营操作日志集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -54,6 +59,7 @@ public sealed class TakeoutLogsDbContext(
|
|||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
ConfigureTenantAuditLog(modelBuilder.Entity<TenantAuditLog>());
|
ConfigureTenantAuditLog(modelBuilder.Entity<TenantAuditLog>());
|
||||||
ConfigureMerchantAuditLog(modelBuilder.Entity<MerchantAuditLog>());
|
ConfigureMerchantAuditLog(modelBuilder.Entity<MerchantAuditLog>());
|
||||||
|
ConfigureMerchantChangeLog(modelBuilder.Entity<MerchantChangeLog>());
|
||||||
ConfigureOperationLog(modelBuilder.Entity<OperationLog>());
|
ConfigureOperationLog(modelBuilder.Entity<OperationLog>());
|
||||||
ConfigureOperationLogInboxMessage(modelBuilder.Entity<OperationLogInboxMessage>());
|
ConfigureOperationLogInboxMessage(modelBuilder.Entity<OperationLogInboxMessage>());
|
||||||
ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>());
|
ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>());
|
||||||
@@ -75,10 +81,29 @@ public sealed class TakeoutLogsDbContext(
|
|||||||
builder.ToTable("merchant_audit_logs");
|
builder.ToTable("merchant_audit_logs");
|
||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
builder.Property(x => x.MerchantId).IsRequired();
|
builder.Property(x => x.MerchantId).IsRequired();
|
||||||
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
|
builder.Property(x => x.Action).HasConversion<int>().IsRequired();
|
||||||
|
builder.Property(x => x.Title).HasMaxLength(200).IsRequired();
|
||||||
builder.Property(x => x.Description).HasMaxLength(1024);
|
builder.Property(x => x.Description).HasMaxLength(1024);
|
||||||
builder.Property(x => x.OperatorName).HasMaxLength(64);
|
builder.Property(x => x.OperatorName).HasMaxLength(100);
|
||||||
|
builder.Property(x => x.IpAddress).HasMaxLength(50);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.MerchantId });
|
builder.HasIndex(x => new { x.TenantId, x.MerchantId });
|
||||||
|
builder.HasIndex(x => new { x.MerchantId, x.CreatedAt });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.CreatedAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureMerchantChangeLog(EntityTypeBuilder<MerchantChangeLog> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("merchant_change_logs");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.MerchantId).IsRequired();
|
||||||
|
builder.Property(x => x.FieldName).HasMaxLength(100).IsRequired();
|
||||||
|
builder.Property(x => x.OldValue).HasColumnType("text");
|
||||||
|
builder.Property(x => x.NewValue).HasColumnType("text");
|
||||||
|
builder.Property(x => x.ChangeType).HasMaxLength(20).IsRequired();
|
||||||
|
builder.Property(x => x.ChangedByName).HasMaxLength(100);
|
||||||
|
builder.Property(x => x.ChangeReason).HasMaxLength(512);
|
||||||
|
builder.HasIndex(x => new { x.MerchantId, x.CreatedAt });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.CreatedAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureOperationLog(EntityTypeBuilder<OperationLog> builder)
|
private static void ConfigureOperationLog(EntityTypeBuilder<OperationLog> builder)
|
||||||
|
|||||||
7080
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251229071911_AddMerchantManagement.Designer.cs
generated
Normal file
7080
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251229071911_AddMerchantManagement.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMerchantManagement : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OperatingMode",
|
||||||
|
table: "tenants",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true,
|
||||||
|
comment: "经营模式(同一主体/不同主体)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BusinessLicenseImageUrl",
|
||||||
|
table: "stores",
|
||||||
|
type: "character varying(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true,
|
||||||
|
comment: "门店营业执照图片地址(主体不一致模式使用)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BusinessLicenseNumber",
|
||||||
|
table: "stores",
|
||||||
|
type: "character varying(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true,
|
||||||
|
comment: "门店营业执照号(主体不一致模式使用)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LegalRepresentative",
|
||||||
|
table: "stores",
|
||||||
|
type: "character varying(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true,
|
||||||
|
comment: "门店法人(主体不一致模式使用)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "RegisteredAddress",
|
||||||
|
table: "stores",
|
||||||
|
type: "character varying(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true,
|
||||||
|
comment: "门店注册地址(主体不一致模式使用)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ApprovedAt",
|
||||||
|
table: "merchants",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true,
|
||||||
|
comment: "审核通过时间。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "ApprovedBy",
|
||||||
|
table: "merchants",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: true,
|
||||||
|
comment: "审核通过人。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ClaimExpiresAt",
|
||||||
|
table: "merchants",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true,
|
||||||
|
comment: "领取过期时间。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ClaimedAt",
|
||||||
|
table: "merchants",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true,
|
||||||
|
comment: "领取时间。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "ClaimedBy",
|
||||||
|
table: "merchants",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: true,
|
||||||
|
comment: "当前领取人。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ClaimedByName",
|
||||||
|
table: "merchants",
|
||||||
|
type: "character varying(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true,
|
||||||
|
comment: "当前领取人姓名。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "FrozenAt",
|
||||||
|
table: "merchants",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true,
|
||||||
|
comment: "冻结时间。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FrozenReason",
|
||||||
|
table: "merchants",
|
||||||
|
type: "character varying(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true,
|
||||||
|
comment: "冻结原因。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsFrozen",
|
||||||
|
table: "merchants",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: "是否冻结业务。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "LastReviewedBy",
|
||||||
|
table: "merchants",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: true,
|
||||||
|
comment: "最近一次审核人。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "OperatingMode",
|
||||||
|
table: "merchants",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true,
|
||||||
|
comment: "经营模式(同一主体/不同主体)。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<byte[]>(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "merchants",
|
||||||
|
type: "bytea",
|
||||||
|
rowVersion: true,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new byte[0],
|
||||||
|
comment: "并发控制版本。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_stores_MerchantId_BusinessLicenseNumber",
|
||||||
|
table: "stores",
|
||||||
|
columns: new[] { "MerchantId", "BusinessLicenseNumber" },
|
||||||
|
unique: true,
|
||||||
|
filter: "\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchants_ClaimedBy",
|
||||||
|
table: "merchants",
|
||||||
|
column: "ClaimedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchants_TenantId_Status",
|
||||||
|
table: "merchants",
|
||||||
|
columns: new[] { "TenantId", "Status" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_stores_MerchantId_BusinessLicenseNumber",
|
||||||
|
table: "stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_merchants_ClaimedBy",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_merchants_TenantId_Status",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OperatingMode",
|
||||||
|
table: "tenants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BusinessLicenseImageUrl",
|
||||||
|
table: "stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BusinessLicenseNumber",
|
||||||
|
table: "stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LegalRepresentative",
|
||||||
|
table: "stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RegisteredAddress",
|
||||||
|
table: "stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ApprovedAt",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ApprovedBy",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClaimExpiresAt",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClaimedAt",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClaimedBy",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ClaimedByName",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FrozenAt",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FrozenReason",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsFrozen",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastReviewedBy",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OperatingMode",
|
||||||
|
table: "merchants");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RowVersion",
|
||||||
|
table: "merchants");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using TakeoutSaaS.Infrastructure.Logs.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||||
|
{
|
||||||
|
[DbContext(typeof(TakeoutLogsDbContext))]
|
||||||
|
[Migration("20251229071940_AddMerchantManagementLogs")]
|
||||||
|
partial class AddMerchantManagementLogs
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberGrowthLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("ChangeValue")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("变动数量。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("CurrentValue")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("当前成长值。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("会员标识。");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasComment("备注。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("OccurredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("发生时间。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "MemberId", "OccurredAt");
|
||||||
|
|
||||||
|
b.ToTable("member_growth_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("成长值变动日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantAuditLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Action")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("动作类型。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasComment("详情描述。");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasComment("操作 IP。");
|
||||||
|
|
||||||
|
b.Property<long>("MerchantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("商户标识。");
|
||||||
|
|
||||||
|
b.Property<long?>("OperatorId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("操作人 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("OperatorName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("操作人名称。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasComment("标题。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "MerchantId");
|
||||||
|
|
||||||
|
b.ToTable("merchant_audit_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("商户入驻审核日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantChangeLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ChangeReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasComment("变更原因。");
|
||||||
|
|
||||||
|
b.Property<string>("ChangeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasComment("变更类型。");
|
||||||
|
|
||||||
|
b.Property<long?>("ChangedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("变更人 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("ChangedByName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("变更人名称。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("FieldName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("变更字段名。");
|
||||||
|
|
||||||
|
b.Property<long>("MerchantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("商户标识。");
|
||||||
|
|
||||||
|
b.Property<string>("NewValue")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("变更后值。");
|
||||||
|
|
||||||
|
b.Property<string>("OldValue")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("变更前值。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("merchant_change_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("商户变更日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("OperationType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("操作类型:BatchExtend, BatchRemind, StatusChange 等。");
|
||||||
|
|
||||||
|
b.Property<string>("OperatorId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("操作人ID。");
|
||||||
|
|
||||||
|
b.Property<string>("OperatorName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComment("操作人名称。");
|
||||||
|
|
||||||
|
b.Property<string>("Parameters")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("操作参数(JSON)。");
|
||||||
|
|
||||||
|
b.Property<string>("Result")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("操作结果(JSON)。");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否成功。");
|
||||||
|
|
||||||
|
b.Property<string>("TargetIds")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("目标ID列表(JSON)。");
|
||||||
|
|
||||||
|
b.Property<string>("TargetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("目标类型:Subscription, Bill 等。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("OperationType", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("operation_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("运营操作日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAuditLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("Action")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("操作类型。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int?>("CurrentStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("新状态。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasComment("详细描述。");
|
||||||
|
|
||||||
|
b.Property<long?>("OperatorId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("操作人 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("OperatorName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("操作人名称。");
|
||||||
|
|
||||||
|
b.Property<int?>("PreviousStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("原状态。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("关联的租户标识。");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComment("日志标题。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("tenant_audit_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("租户运营审核日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Infrastructure.Logs.Persistence.OperationLogInboxMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("ConsumedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("MessageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MessageId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("operation_log_inbox_messages", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMerchantManagementLogs : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Title",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
type: "character varying(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: false,
|
||||||
|
comment: "标题。",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(128)",
|
||||||
|
oldMaxLength: 128,
|
||||||
|
oldComment: "标题。");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "OperatorName",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
type: "character varying(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true,
|
||||||
|
comment: "操作人名称。",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(64)",
|
||||||
|
oldMaxLength: 64,
|
||||||
|
oldNullable: true,
|
||||||
|
oldComment: "操作人名称。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "IpAddress",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
type: "character varying(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true,
|
||||||
|
comment: "操作 IP。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "merchant_change_logs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
MerchantId = table.Column<long>(type: "bigint", nullable: false, comment: "商户标识。"),
|
||||||
|
FieldName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "变更字段名。"),
|
||||||
|
OldValue = table.Column<string>(type: "text", nullable: true, comment: "变更前值。"),
|
||||||
|
NewValue = table.Column<string>(type: "text", nullable: true, comment: "变更后值。"),
|
||||||
|
ChangeType = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, comment: "变更类型。"),
|
||||||
|
ChangedBy = table.Column<long>(type: "bigint", nullable: true, comment: "变更人 ID。"),
|
||||||
|
ChangedByName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true, comment: "变更人名称。"),
|
||||||
|
ChangeReason = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "变更原因。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_merchant_change_logs", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "商户变更日志。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchant_audit_logs_MerchantId_CreatedAt",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
columns: new[] { "MerchantId", "CreatedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchant_audit_logs_TenantId_CreatedAt",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
columns: new[] { "TenantId", "CreatedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchant_change_logs_MerchantId_CreatedAt",
|
||||||
|
table: "merchant_change_logs",
|
||||||
|
columns: new[] { "MerchantId", "CreatedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_merchant_change_logs_TenantId_CreatedAt",
|
||||||
|
table: "merchant_change_logs",
|
||||||
|
columns: new[] { "TenantId", "CreatedAt" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "merchant_change_logs");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_merchant_audit_logs_MerchantId_CreatedAt",
|
||||||
|
table: "merchant_audit_logs");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_merchant_audit_logs_TenantId_CreatedAt",
|
||||||
|
table: "merchant_audit_logs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IpAddress",
|
||||||
|
table: "merchant_audit_logs");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Title",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: false,
|
||||||
|
comment: "标题。",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(200)",
|
||||||
|
oldMaxLength: 200,
|
||||||
|
oldComment: "标题。");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "OperatorName",
|
||||||
|
table: "merchant_audit_logs",
|
||||||
|
type: "character varying(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true,
|
||||||
|
comment: "操作人名称。",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(100)",
|
||||||
|
oldMaxLength: 100,
|
||||||
|
oldNullable: true,
|
||||||
|
oldComment: "操作人名称。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.0")
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -124,6 +124,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
.HasColumnType("character varying(1024)")
|
.HasColumnType("character varying(1024)")
|
||||||
.HasComment("详情描述。");
|
.HasComment("详情描述。");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasComment("操作 IP。");
|
||||||
|
|
||||||
b.Property<long>("MerchantId")
|
b.Property<long>("MerchantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("商户标识。");
|
.HasComment("商户标识。");
|
||||||
@@ -133,8 +138,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
.HasComment("操作人 ID。");
|
.HasComment("操作人 ID。");
|
||||||
|
|
||||||
b.Property<string>("OperatorName")
|
b.Property<string>("OperatorName")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(100)")
|
||||||
.HasComment("操作人名称。");
|
.HasComment("操作人名称。");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
@@ -143,8 +148,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(200)")
|
||||||
.HasComment("标题。");
|
.HasComment("标题。");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
@@ -157,6 +162,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "CreatedAt");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "MerchantId");
|
b.HasIndex("TenantId", "MerchantId");
|
||||||
|
|
||||||
b.ToTable("merchant_audit_logs", null, t =>
|
b.ToTable("merchant_audit_logs", null, t =>
|
||||||
@@ -165,6 +174,93 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantChangeLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ChangeReason")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasComment("变更原因。");
|
||||||
|
|
||||||
|
b.Property<string>("ChangeType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasComment("变更类型。");
|
||||||
|
|
||||||
|
b.Property<long?>("ChangedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("变更人 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("ChangedByName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("变更人名称。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("FieldName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("变更字段名。");
|
||||||
|
|
||||||
|
b.Property<long>("MerchantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("商户标识。");
|
||||||
|
|
||||||
|
b.Property<string>("NewValue")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("变更后值。");
|
||||||
|
|
||||||
|
b.Property<string>("OldValue")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("变更前值。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("merchant_change_logs", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("商户变更日志。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "10.0.0")
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -2373,6 +2373,14 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasComment("详细地址。");
|
.HasComment("详细地址。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ApprovedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("审核通过时间。");
|
||||||
|
|
||||||
|
b.Property<long?>("ApprovedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("审核通过人。");
|
||||||
|
|
||||||
b.Property<string>("BrandAlias")
|
b.Property<string>("BrandAlias")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
@@ -2402,6 +2410,23 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
.HasComment("所在城市。");
|
.HasComment("所在城市。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClaimExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("领取过期时间。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ClaimedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("领取时间。");
|
||||||
|
|
||||||
|
b.Property<long?>("ClaimedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("当前领取人。");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimedByName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("当前领取人姓名。");
|
||||||
|
|
||||||
b.Property<string>("ContactEmail")
|
b.Property<string>("ContactEmail")
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
@@ -2434,6 +2459,21 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
.HasComment("所在区县。");
|
.HasComment("所在区县。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FrozenAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("冻结时间。");
|
||||||
|
|
||||||
|
b.Property<string>("FrozenReason")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasComment("冻结原因。");
|
||||||
|
|
||||||
|
b.Property<bool>("IsFrozen")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasComment("是否冻结业务。");
|
||||||
|
|
||||||
b.Property<DateTime?>("JoinedAt")
|
b.Property<DateTime?>("JoinedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("入驻时间。");
|
.HasComment("入驻时间。");
|
||||||
@@ -2442,6 +2482,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("最近一次审核时间。");
|
.HasComment("最近一次审核时间。");
|
||||||
|
|
||||||
|
b.Property<long?>("LastReviewedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最近一次审核人。");
|
||||||
|
|
||||||
b.Property<double?>("Latitude")
|
b.Property<double?>("Latitude")
|
||||||
.HasColumnType("double precision")
|
.HasColumnType("double precision")
|
||||||
.HasComment("纬度信息。");
|
.HasComment("纬度信息。");
|
||||||
@@ -2459,6 +2503,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("double precision")
|
.HasColumnType("double precision")
|
||||||
.HasComment("经度信息。");
|
.HasComment("经度信息。");
|
||||||
|
|
||||||
|
b.Property<int?>("OperatingMode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("经营模式(同一主体/不同主体)。");
|
||||||
|
|
||||||
b.Property<string>("Province")
|
b.Property<string>("Province")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
@@ -2469,6 +2517,13 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
.HasComment("审核备注或驳回原因。");
|
.HasComment("审核备注或驳回原因。");
|
||||||
|
|
||||||
|
b.Property<byte[]>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("bytea")
|
||||||
|
.HasComment("并发控制版本。");
|
||||||
|
|
||||||
b.Property<string>("ServicePhone")
|
b.Property<string>("ServicePhone")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("客服电话。");
|
.HasComment("客服电话。");
|
||||||
@@ -2499,8 +2554,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ClaimedBy");
|
||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "Status");
|
||||||
|
|
||||||
b.ToTable("merchants", null, t =>
|
b.ToTable("merchants", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("商户主体信息,承载入驻和资质审核结果。");
|
t.HasComment("商户主体信息,承载入驻和资质审核结果。");
|
||||||
@@ -4784,6 +4843,16 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasComment("门店营业时段描述(备用字符串)。");
|
.HasComment("门店营业时段描述(备用字符串)。");
|
||||||
|
|
||||||
|
b.Property<string>("BusinessLicenseImageUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasComment("门店营业执照图片地址(主体不一致模式使用)。");
|
||||||
|
|
||||||
|
b.Property<string>("BusinessLicenseNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasComment("门店营业执照号(主体不一致模式使用)。");
|
||||||
|
|
||||||
b.Property<string>("City")
|
b.Property<string>("City")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
@@ -4837,6 +4906,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("double precision")
|
.HasColumnType("double precision")
|
||||||
.HasComment("纬度。");
|
.HasComment("纬度。");
|
||||||
|
|
||||||
|
b.Property<string>("LegalRepresentative")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasComment("门店法人(主体不一致模式使用)。");
|
||||||
|
|
||||||
b.Property<double?>("Longitude")
|
b.Property<double?>("Longitude")
|
||||||
.HasColumnType("double precision")
|
.HasColumnType("double precision")
|
||||||
.HasComment("高德/腾讯地图经度。");
|
.HasComment("高德/腾讯地图经度。");
|
||||||
@@ -4866,6 +4940,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
.HasComment("所在省份。");
|
.HasComment("所在省份。");
|
||||||
|
|
||||||
|
b.Property<string>("RegisteredAddress")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)")
|
||||||
|
.HasComment("门店注册地址(主体不一致模式使用)。");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("门店当前运营状态。");
|
.HasComment("门店当前运营状态。");
|
||||||
@@ -4908,6 +4987,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId", "BusinessLicenseNumber")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "Code")
|
b.HasIndex("TenantId", "Code")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
@@ -5704,6 +5787,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
.HasComment("租户全称或品牌名称。");
|
.HasComment("租户全称或品牌名称。");
|
||||||
|
|
||||||
|
b.Property<int?>("OperatingMode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("经营模式(同一主体/不同主体)。");
|
||||||
|
|
||||||
b.Property<long?>("PrimaryOwnerUserId")
|
b.Property<long?>("PrimaryOwnerUserId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("系统内对应的租户所有者账号 ID。");
|
.HasComment("系统内对应的租户所有者账号 ID。");
|
||||||
@@ -5806,6 +5893,18 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("失效时间(UTC),为空表示长期有效。");
|
.HasComment("失效时间(UTC),为空表示长期有效。");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否启用(已弃用,迁移期保留)。");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("展示优先级,数值越大越靠前。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PublishedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("实际发布时间(UTC)。");
|
||||||
|
|
||||||
b.Property<int>("PublisherScope")
|
b.Property<int>("PublisherScope")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("发布者范围。");
|
.HasComment("发布者范围。");
|
||||||
@@ -5814,32 +5913,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("发布者用户 ID(平台或租户后台账号)。");
|
.HasComment("发布者用户 ID(平台或租户后台账号)。");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasComment("公告状态。");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("PublishedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("实际发布时间(UTC)。");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("RevokedAt")
|
b.Property<DateTime?>("RevokedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("撤销时间(UTC)。");
|
.HasComment("撤销时间(UTC)。");
|
||||||
|
|
||||||
b.Property<DateTime?>("ScheduledPublishAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("预定发布时间(UTC)。");
|
|
||||||
|
|
||||||
b.Property<string>("TargetType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)")
|
|
||||||
.HasComment("目标受众类型。");
|
|
||||||
|
|
||||||
b.Property<string>("TargetParameters")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasComment("目标受众参数(JSON)。");
|
|
||||||
|
|
||||||
b.Property<byte[]>("RowVersion")
|
b.Property<byte[]>("RowVersion")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -5847,13 +5924,23 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bytea")
|
.HasColumnType("bytea")
|
||||||
.HasComment("并发控制字段。");
|
.HasComment("并发控制字段。");
|
||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<DateTime?>("ScheduledPublishAt")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("是否启用(已弃用,迁移期保留)。");
|
.HasComment("预定发布时间(UTC)。");
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("展示优先级,数值越大越靠前。");
|
.HasComment("公告状态。");
|
||||||
|
|
||||||
|
b.Property<string>("TargetParameters")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("目标受众参数(JSON)。");
|
||||||
|
|
||||||
|
b.Property<string>("TargetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("目标受众类型。");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
@@ -5875,15 +5962,15 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status", "EffectiveFrom")
|
||||||
|
.HasFilter("\"TenantId\" = 0");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "AnnouncementType", "IsActive");
|
b.HasIndex("TenantId", "AnnouncementType", "IsActive");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo");
|
b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "Status", "EffectiveFrom");
|
b.HasIndex("TenantId", "Status", "EffectiveFrom");
|
||||||
|
|
||||||
b.HasIndex("Status", "EffectiveFrom")
|
|
||||||
.HasFilter("\"TenantId\" = 0");
|
|
||||||
|
|
||||||
b.ToTable("tenant_announcements", null, t =>
|
b.ToTable("tenant_announcements", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("租户公告。");
|
t.HasComment("租户公告。");
|
||||||
|
|||||||
Reference in New Issue
Block a user