feat:商户管理
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
using TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Application.App.Merchants.Queries;
|
||||
@@ -41,36 +42,32 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
/// <summary>
|
||||
/// 查询商户列表。
|
||||
/// </summary>
|
||||
/// <param name="status">状态筛选。</param>
|
||||
/// <param name="page">页码。</param>
|
||||
/// <param name="pageSize">每页大小。</param>
|
||||
/// <param name="sortBy">排序字段。</param>
|
||||
/// <param name="sortDesc">是否倒序。</param>
|
||||
/// <param name="query">查询参数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>商户分页结果。</returns>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantDto>>> List(
|
||||
[FromQuery] MerchantStatus? status,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? sortBy = null,
|
||||
[FromQuery] bool sortDesc = true,
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantListItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantListItemDto>>> List(
|
||||
[FromQuery] GetMerchantListQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 组装查询参数并执行查询
|
||||
var result = await mediator.Send(new SearchMerchantsQuery
|
||||
{
|
||||
Status = status,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
SortBy = sortBy,
|
||||
SortDescending = sortDesc
|
||||
}, cancellationToken);
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<MerchantListItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
// 2. 返回分页结果
|
||||
return ApiResponse<PagedResult<MerchantDto>>.Ok(result);
|
||||
/// <summary>
|
||||
/// 待审核商户列表。
|
||||
/// </summary>
|
||||
[HttpGet("pending-review")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<MerchantReviewListItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<MerchantReviewListItemDto>>> PendingReviewList(
|
||||
[FromQuery] GetPendingReviewListQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<MerchantReviewListItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,23 +79,36 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
/// <returns>更新后的商户或未找到。</returns>
|
||||
[HttpPut("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<UpdateMerchantResultDto>), StatusCodes.Status422UnprocessableEntity)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MerchantDto>> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<UpdateMerchantResultDto>> Update(long merchantId, [FromBody] UpdateMerchantCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 绑定商户标识
|
||||
if (command.MerchantId == 0)
|
||||
if (command.MerchantId != 0 && command.MerchantId != merchantId)
|
||||
{
|
||||
command = command with { MerchantId = merchantId };
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
|
||||
}
|
||||
|
||||
command = command with { MerchantId = merchantId };
|
||||
|
||||
// 2. 执行更新
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回更新结果或 404
|
||||
return result == null
|
||||
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
||||
: ApiResponse<MerchantDto>.Ok(result);
|
||||
if (result == null)
|
||||
{
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(ErrorCodes.NotFound, "商户不存在");
|
||||
}
|
||||
|
||||
if (result.RequiresReview)
|
||||
{
|
||||
return ApiResponse<UpdateMerchantResultDto>.Error(
|
||||
ErrorCodes.ValidationFailed,
|
||||
"关键信息修改,商户已进入待审核状态,业务已冻结")
|
||||
with { Data = result };
|
||||
}
|
||||
|
||||
return ApiResponse<UpdateMerchantResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -130,17 +140,51 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
/// <returns>商户概览或未找到。</returns>
|
||||
[HttpGet("{merchantId:long}")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<MerchantDetailDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<MerchantDto>> Detail(long merchantId, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<MerchantDetailDto>> Detail(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询商户概览
|
||||
var result = await mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken);
|
||||
var result = await mediator.Send(new GetMerchantDetailQuery(merchantId), cancellationToken);
|
||||
|
||||
// 2. 返回结果或 404
|
||||
return result == null
|
||||
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
|
||||
: ApiResponse<MerchantDto>.Ok(result);
|
||||
// 2. 返回结果
|
||||
return ApiResponse<MerchantDetailDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取审核领取信息。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto?>> GetReviewClaim(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantReviewClaimQuery(merchantId), cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取审核。
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto>> ClaimReview(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ClaimMerchantReviewCommand { MerchantId = merchantId }, cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放审核领取。
|
||||
/// </summary>
|
||||
[HttpDelete("{merchantId:long}/review/claim")]
|
||||
[PermissionAuthorize("merchant:review")]
|
||||
[ProducesResponseType(typeof(ApiResponse<ClaimInfoDto?>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<ClaimInfoDto?>> ReleaseReviewClaim(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new ReleaseClaimCommand { MerchantId = merchantId }, cancellationToken);
|
||||
return ApiResponse<ClaimInfoDto?>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,6 +334,60 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
return ApiResponse<MerchantDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 撤销审核。
|
||||
/// </summary>
|
||||
[HttpPost("{merchantId:long}/review/revoke")]
|
||||
[PermissionAuthorize("merchant:review:revoke")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> RevokeReview(
|
||||
long merchantId,
|
||||
[FromBody] RevokeMerchantReviewCommand body,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (body.MerchantId != 0 && body.MerchantId != merchantId)
|
||||
{
|
||||
return ApiResponse<object>.Error(StatusCodes.Status400BadRequest, "路由 merchantId 与请求体 merchantId 不一致");
|
||||
}
|
||||
|
||||
var command = new RevokeMerchantReviewCommand
|
||||
{
|
||||
MerchantId = merchantId,
|
||||
Reason = body.Reason
|
||||
};
|
||||
await mediator.Send(command, cancellationToken);
|
||||
return ApiResponse<object>.Ok(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核历史。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/audit-history")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantAuditLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantAuditLogDto>>> AuditHistory(
|
||||
long merchantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantAuditHistoryQuery(merchantId), cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantAuditLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更历史。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/change-history")]
|
||||
[PermissionAuthorize("merchant:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantChangeLogDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<MerchantChangeLogDto>>> ChangeHistory(
|
||||
long merchantId,
|
||||
[FromQuery] string? fieldName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetMerchantChangeHistoryQuery(merchantId, fieldName), cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<MerchantChangeLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核日志。
|
||||
/// </summary>
|
||||
@@ -310,6 +408,27 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
|
||||
return ApiResponse<PagedResult<MerchantAuditLogDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出商户 PDF。
|
||||
/// </summary>
|
||||
[HttpGet("{merchantId:long}/export-pdf")]
|
||||
[PermissionAuthorize("merchant:export")]
|
||||
[Produces("application/pdf")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ExportPdf(long merchantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = await mediator.Send(new ExportMerchantPdfQuery(merchantId), cancellationToken);
|
||||
var fileName = $"merchant_{merchantId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.pdf";
|
||||
|
||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileName,
|
||||
FileNameStar = fileName
|
||||
}.ToString();
|
||||
|
||||
return File(bytes, "application/pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可选商户类目列表。
|
||||
/// </summary>
|
||||
|
||||
@@ -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 TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 更新商户命令。
|
||||
/// </summary>
|
||||
public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
||||
public sealed record UpdateMerchantCommand : IRequest<UpdateMerchantResultDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
@@ -15,29 +14,29 @@ public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
||||
public long MerchantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品牌名称。
|
||||
/// 商户名称。
|
||||
/// </summary>
|
||||
public string BrandName { get; init; } = string.Empty;
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品牌简称。
|
||||
/// 营业执照号。
|
||||
/// </summary>
|
||||
public string? BrandAlias { get; init; }
|
||||
public string? LicenseNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Logo 地址。
|
||||
/// 法人或负责人。
|
||||
/// </summary>
|
||||
public string? LogoUrl { get; init; }
|
||||
public string? LegalRepresentative { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 品类。
|
||||
/// 注册地址。
|
||||
/// </summary>
|
||||
public string? Category { get; init; }
|
||||
public string? RegisteredAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系电话。
|
||||
/// </summary>
|
||||
public string ContactPhone { get; init; } = string.Empty;
|
||||
public string? ContactPhone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系邮箱。
|
||||
@@ -45,7 +44,7 @@ public sealed record UpdateMerchantCommand : IRequest<MerchantDto?>
|
||||
public string? ContactEmail { get; init; }
|
||||
|
||||
/// <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>
|
||||
public MerchantAuditAction Action { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人 ID。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? OperatorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
@@ -41,6 +47,11 @@ public sealed class MerchantAuditLogDto
|
||||
/// </summary>
|
||||
public string? OperatorName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作 IP。
|
||||
/// </summary>
|
||||
public string? IpAddress { get; init; }
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
@@ -6,17 +11,117 @@ namespace TakeoutSaaS.Application.App.Merchants.Dto;
|
||||
public sealed class MerchantDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 基础信息。
|
||||
/// 商户 ID。
|
||||
/// </summary>
|
||||
public MerchantDto Merchant { get; init; } = new();
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 证照列表。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
public IReadOnlyList<MerchantDocumentDto> Documents { get; init; } = [];
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <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 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.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
@@ -13,7 +18,11 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
/// </summary>
|
||||
public sealed class GetMerchantDetailQueryHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IStoreRepository storeRepository,
|
||||
ITenantRepository tenantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService)
|
||||
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -24,21 +33,31 @@ public sealed class GetMerchantDetailQueryHandler(
|
||||
/// <returns>商户详情 DTO。</returns>
|
||||
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 获取租户上下文并查询商户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||
// 1. 获取权限与商户
|
||||
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
||||
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
|
||||
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
|
||||
|
||||
// 2. 查询证照与合同
|
||||
var documents = await merchantRepository.GetDocumentsAsync(request.MerchantId, tenantId, cancellationToken);
|
||||
var contracts = await merchantRepository.GetContractsAsync(request.MerchantId, tenantId, cancellationToken);
|
||||
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, "禁止访问其他租户的商户");
|
||||
}
|
||||
|
||||
// 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
|
||||
return new MerchantDetailDto
|
||||
{
|
||||
Merchant = MerchantMapping.ToDto(merchant),
|
||||
Documents = MerchantMapping.ToDocumentDtos(documents),
|
||||
Contracts = MerchantMapping.ToContractDtos(contracts)
|
||||
};
|
||||
return MerchantMapping.ToDetailDto(merchant, tenant?.Name, storeDtos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
|
||||
@@ -16,7 +15,6 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
/// </summary>
|
||||
public sealed class ReviewMerchantCommandHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<ReviewMerchantCommand, MerchantDto>
|
||||
{
|
||||
@@ -29,33 +27,62 @@ public sealed class ReviewMerchantCommandHandler(
|
||||
public async Task<MerchantDto> Handle(ReviewMerchantCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取商户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken)
|
||||
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
|
||||
|
||||
// 2. 已审核通过则直接返回
|
||||
if (request.Approve && merchant.Status == MerchantStatus.Approved)
|
||||
var now = DateTime.UtcNow;
|
||||
if (!merchant.ClaimedBy.HasValue || !merchant.ClaimExpiresAt.HasValue || merchant.ClaimExpiresAt <= now)
|
||||
{
|
||||
return MerchantMapping.ToDto(merchant);
|
||||
throw new BusinessException(ErrorCodes.Conflict, "请先领取审核");
|
||||
}
|
||||
|
||||
// 3. 更新审核状态
|
||||
var previousStatus = merchant.Status;
|
||||
if (merchant.ClaimedBy != currentUserAccessor.UserId)
|
||||
{
|
||||
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.ReviewRemarks = request.Remarks;
|
||||
merchant.LastReviewedAt = DateTime.UtcNow;
|
||||
merchant.LastReviewedAt = now;
|
||||
merchant.LastReviewedBy = ResolveOperatorId();
|
||||
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.AddAuditLogAsync(new MerchantAuditLog
|
||||
{
|
||||
TenantId = tenantId,
|
||||
TenantId = merchant.TenantId,
|
||||
MerchantId = merchant.Id,
|
||||
Action = MerchantAuditAction.MerchantReviewed,
|
||||
Action = request.Approve ? MerchantAuditAction.ReviewApproved : MerchantAuditAction.ReviewRejected,
|
||||
Title = request.Approve ? "商户审核通过" : "商户审核驳回",
|
||||
Description = request.Remarks,
|
||||
OperatorId = ResolveOperatorId(),
|
||||
@@ -63,7 +90,7 @@ public sealed class ReviewMerchantCommandHandler(
|
||||
}, cancellationToken);
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 5. 返回 DTO
|
||||
// 4. 返回 DTO
|
||||
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 TakeoutSaaS.Application.App.Merchants.Commands;
|
||||
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.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;
|
||||
@@ -12,51 +21,175 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
|
||||
/// </summary>
|
||||
public sealed class UpdateMerchantCommandHandler(
|
||||
IMerchantRepository merchantRepository,
|
||||
IStoreRepository storeRepository,
|
||||
ITenantRepository tenantRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IAdminAuthService adminAuthService,
|
||||
ILogger<UpdateMerchantCommandHandler> logger)
|
||||
: IRequestHandler<UpdateMerchantCommand, MerchantDto?>
|
||||
: IRequestHandler<UpdateMerchantCommand, UpdateMerchantResultDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MerchantDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
|
||||
public async Task<UpdateMerchantResultDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取现有商户
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var existing = await merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
|
||||
if (existing == null)
|
||||
if (request.RowVersion == null || request.RowVersion.Length == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 2. 更新字段
|
||||
existing.BrandName = request.BrandName.Trim();
|
||||
existing.BrandAlias = request.BrandAlias?.Trim();
|
||||
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()
|
||||
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
|
||||
{
|
||||
Id = merchant.Id,
|
||||
TenantId = merchant.TenantId,
|
||||
BrandName = merchant.BrandName,
|
||||
BrandAlias = merchant.BrandAlias,
|
||||
LogoUrl = merchant.LogoUrl,
|
||||
Category = merchant.Category,
|
||||
ContactPhone = merchant.ContactPhone,
|
||||
ContactEmail = merchant.ContactEmail,
|
||||
Status = merchant.Status,
|
||||
JoinedAt = merchant.JoinedAt,
|
||||
CreatedAt = merchant.CreatedAt
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
TenantId = merchant.TenantId,
|
||||
MerchantId = merchant.Id,
|
||||
Action = MerchantAuditAction.ReviewPendingReApproval,
|
||||
Title = "关键信息变更待审核",
|
||||
Description = "关键信息修改后已进入待审核状态",
|
||||
OperatorId = actorId,
|
||||
OperatorName = actorName
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
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.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Merchants;
|
||||
|
||||
@@ -28,6 +29,53 @@ internal static class MerchantMapping
|
||||
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>
|
||||
/// 将商户证照实体映射为 DTO。
|
||||
/// </summary>
|
||||
@@ -76,12 +124,42 @@ internal static class MerchantMapping
|
||||
Id = log.Id,
|
||||
MerchantId = log.MerchantId,
|
||||
Action = log.Action,
|
||||
OperatorId = log.OperatorId,
|
||||
Title = log.Title,
|
||||
Description = log.Description,
|
||||
OperatorName = log.OperatorName,
|
||||
IpAddress = log.IpAddress,
|
||||
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>
|
||||
/// 将商户分类实体映射为 DTO。
|
||||
/// </summary>
|
||||
@@ -119,4 +197,10 @@ internal static class MerchantMapping
|
||||
/// <returns>分类 DTO 列表。</returns>
|
||||
public static IReadOnlyList<MerchantCategoryDto> ToCategoryDtos(IEnumerable<MerchantCategory> categories)
|
||||
=> 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()
|
||||
{
|
||||
RuleFor(x => x.MerchantId).GreaterThan(0);
|
||||
RuleFor(x => x.BrandName).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.BrandAlias).MaximumLength(64);
|
||||
RuleFor(x => x.LogoUrl).MaximumLength(256);
|
||||
RuleFor(x => x.Category).MaximumLength(64);
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.LicenseNumber).MaximumLength(64);
|
||||
RuleFor(x => x.LegalRepresentative).MaximumLength(64);
|
||||
RuleFor(x => x.RegisteredAddress).MaximumLength(256);
|
||||
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 System.ComponentModel.DataAnnotations;
|
||||
using TakeoutSaaS.Application.App.Tenants.Dto;
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
|
||||
@@ -29,4 +30,9 @@ public sealed record ReviewTenantCommand : IRequest<TenantDto>
|
||||
/// 审核通过后续费时长(月)。
|
||||
/// </summary>
|
||||
public int? RenewMonths { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经营模式(审核通过时必填)。
|
||||
/// </summary>
|
||||
public OperatingMode? OperatingMode { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||
|
||||
@@ -55,6 +56,11 @@ public sealed class TenantDto
|
||||
/// </summary>
|
||||
public TenantVerificationStatus VerificationStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 经营模式。
|
||||
/// </summary>
|
||||
public OperatingMode? OperatingMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前套餐 ID。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Tenants.Commands;
|
||||
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.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
@@ -14,6 +17,7 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
/// </summary>
|
||||
public sealed class ReviewTenantCommandHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
IMerchantRepository merchantRepository,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<ReviewTenantCommand, TenantDto>
|
||||
{
|
||||
@@ -53,19 +57,25 @@ public sealed class ReviewTenantCommandHandler(
|
||||
// 4. 更新租户与订阅状态
|
||||
if (request.Approve)
|
||||
{
|
||||
if (!request.OperatingMode.HasValue)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "审核通过时必须选择经营模式");
|
||||
}
|
||||
|
||||
var renewMonths = request.RenewMonths ?? 0;
|
||||
if (renewMonths <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "续费时长必须为正整数(月)");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
verification.Status = TenantVerificationStatus.Approved;
|
||||
tenant.Status = TenantStatus.Active;
|
||||
tenant.OperatingMode = request.OperatingMode;
|
||||
if (subscription != null)
|
||||
{
|
||||
subscription.Status = SubscriptionStatus.Active;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (subscription.EffectiveFrom == default || subscription.EffectiveFrom > now)
|
||||
{
|
||||
subscription.EffectiveFrom = now;
|
||||
@@ -92,6 +102,69 @@ public sealed class ReviewTenantCommandHandler(
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -141,6 +214,7 @@ public sealed class ReviewTenantCommandHandler(
|
||||
|
||||
// 8. 保存并返回 DTO
|
||||
await tenantRepository.SaveChangesAsync(cancellationToken);
|
||||
await merchantRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return TenantMapping.ToDto(tenant, subscription, verification);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ internal static class TenantMapping
|
||||
ContactEmail = tenant.ContactEmail,
|
||||
Status = tenant.Status,
|
||||
VerificationStatus = verification?.Status ?? Domain.Tenants.Enums.TenantVerificationStatus.Draft,
|
||||
OperatingMode = tenant.OperatingMode,
|
||||
CurrentPackageId = subscription?.TenantPackageId,
|
||||
EffectiveFrom = subscription?.EffectiveFrom ?? tenant.EffectiveFrom,
|
||||
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>
|
||||
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.Shared.Abstractions.Entities;
|
||||
|
||||
@@ -103,6 +104,31 @@ public sealed class Merchant : MultiTenantEntityBase
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -117,4 +143,39 @@ public sealed class Merchant : MultiTenantEntityBase
|
||||
/// 最近一次审核时间。
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
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.Enums;
|
||||
|
||||
@@ -17,6 +18,22 @@ public interface IMerchantRepository
|
||||
/// <returns>商户实体或 null。</returns>
|
||||
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>
|
||||
@@ -26,6 +43,22 @@ public interface IMerchantRepository
|
||||
/// <returns>商户集合。</returns>
|
||||
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>
|
||||
@@ -168,6 +201,14 @@ public interface IMerchantRepository
|
||||
/// <returns>异步任务。</returns>
|
||||
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>
|
||||
@@ -176,4 +217,18 @@ public interface IMerchantRepository
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>审核日志列表。</returns>
|
||||
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>
|
||||
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>
|
||||
|
||||
@@ -13,11 +13,21 @@ public interface IStoreRepository
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
@@ -93,6 +94,11 @@ public sealed class Tenant : AuditableEntityBase
|
||||
/// </summary>
|
||||
public TenantStatus Status { get; set; } = TenantStatus.PendingReview;
|
||||
|
||||
/// <summary>
|
||||
/// 经营模式(同一主体/不同主体)。
|
||||
/// </summary>
|
||||
public OperatingMode? OperatingMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务生效时间(UTC)。
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||
using TakeoutSaaS.Domain.Inventory.Repositories;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
using TakeoutSaaS.Domain.Merchants.Services;
|
||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||
using TakeoutSaaS.Domain.Payments.Repositories;
|
||||
using TakeoutSaaS.Domain.Products.Repositories;
|
||||
@@ -63,6 +64,7 @@ public static class AppServiceCollectionExtensions
|
||||
// 1. 账单领域/导出服务
|
||||
services.AddScoped<IBillingDomainService, BillingDomainService>();
|
||||
services.AddScoped<IBillingExportService, BillingExportService>();
|
||||
services.AddScoped<IMerchantExportService, MerchantExportService>();
|
||||
|
||||
services.AddOptions<AppSeedOptions>()
|
||||
.Bind(configuration.GetSection(AppSeedOptions.SectionName))
|
||||
|
||||
@@ -469,6 +469,7 @@ public sealed class TakeoutAppDbContext(
|
||||
builder.Property(x => x.Industry).HasMaxLength(64);
|
||||
builder.Property(x => x.LogoUrl).HasColumnType("text");
|
||||
builder.Property(x => x.Remarks).HasMaxLength(512);
|
||||
builder.Property(x => x.OperatingMode).HasConversion<int>();
|
||||
builder.HasIndex(x => x.Code).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.Address).HasMaxLength(256);
|
||||
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 => new { x.TenantId, x.Status });
|
||||
builder.HasIndex(x => x.ClaimedBy);
|
||||
}
|
||||
|
||||
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.Phone).HasMaxLength(32);
|
||||
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.City).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.HasIndex(x => new { x.TenantId, x.MerchantId });
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Common.Enums;
|
||||
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
@@ -24,6 +25,26 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog
|
||||
.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 />
|
||||
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)
|
||||
{
|
||||
return await logsContext.MerchantAuditLogs
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.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);
|
||||
}
|
||||
|
||||
/// <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 />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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 />
|
||||
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>
|
||||
public DbSet<MerchantAuditLog> MerchantAuditLogs => Set<MerchantAuditLog>();
|
||||
|
||||
/// <summary>
|
||||
/// 商户变更日志集合。
|
||||
/// </summary>
|
||||
public DbSet<MerchantChangeLog> MerchantChangeLogs => Set<MerchantChangeLog>();
|
||||
|
||||
/// <summary>
|
||||
/// 运营操作日志集合。
|
||||
/// </summary>
|
||||
@@ -54,6 +59,7 @@ public sealed class TakeoutLogsDbContext(
|
||||
base.OnModelCreating(modelBuilder);
|
||||
ConfigureTenantAuditLog(modelBuilder.Entity<TenantAuditLog>());
|
||||
ConfigureMerchantAuditLog(modelBuilder.Entity<MerchantAuditLog>());
|
||||
ConfigureMerchantChangeLog(modelBuilder.Entity<MerchantChangeLog>());
|
||||
ConfigureOperationLog(modelBuilder.Entity<OperationLog>());
|
||||
ConfigureOperationLogInboxMessage(modelBuilder.Entity<OperationLogInboxMessage>());
|
||||
ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>());
|
||||
@@ -75,10 +81,29 @@ public sealed class TakeoutLogsDbContext(
|
||||
builder.ToTable("merchant_audit_logs");
|
||||
builder.HasKey(x => x.Id);
|
||||
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.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.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)
|
||||
|
||||
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
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -124,6 +124,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasComment("详情描述。");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasComment("操作 IP。");
|
||||
|
||||
b.Property<long>("MerchantId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("商户标识。");
|
||||
@@ -133,8 +138,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||
.HasComment("操作人 ID。");
|
||||
|
||||
b.Property<string>("OperatorName")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasComment("操作人名称。");
|
||||
|
||||
b.Property<long>("TenantId")
|
||||
@@ -143,8 +148,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasComment("标题。");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
@@ -157,6 +162,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MerchantId", "CreatedAt");
|
||||
|
||||
b.HasIndex("TenantId", "CreatedAt");
|
||||
|
||||
b.HasIndex("TenantId", "MerchantId");
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -2373,6 +2373,14 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasComment("详细地址。");
|
||||
|
||||
b.Property<DateTime?>("ApprovedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("审核通过时间。");
|
||||
|
||||
b.Property<long?>("ApprovedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("审核通过人。");
|
||||
|
||||
b.Property<string>("BrandAlias")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
@@ -2402,6 +2410,23 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(64)")
|
||||
.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")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
@@ -2434,6 +2459,21 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(64)")
|
||||
.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")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("入驻时间。");
|
||||
@@ -2442,6 +2482,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("最近一次审核时间。");
|
||||
|
||||
b.Property<long?>("LastReviewedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("最近一次审核人。");
|
||||
|
||||
b.Property<double?>("Latitude")
|
||||
.HasColumnType("double precision")
|
||||
.HasComment("纬度信息。");
|
||||
@@ -2459,6 +2503,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("double precision")
|
||||
.HasComment("经度信息。");
|
||||
|
||||
b.Property<int?>("OperatingMode")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("经营模式(同一主体/不同主体)。");
|
||||
|
||||
b.Property<string>("Province")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
@@ -2469,6 +2517,13 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasComment("审核备注或驳回原因。");
|
||||
|
||||
b.Property<byte[]>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("bytea")
|
||||
.HasComment("并发控制版本。");
|
||||
|
||||
b.Property<string>("ServicePhone")
|
||||
.HasColumnType("text")
|
||||
.HasComment("客服电话。");
|
||||
@@ -2499,8 +2554,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClaimedBy");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("TenantId", "Status");
|
||||
|
||||
b.ToTable("merchants", null, t =>
|
||||
{
|
||||
t.HasComment("商户主体信息,承载入驻和资质审核结果。");
|
||||
@@ -4784,6 +4843,16 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(256)")
|
||||
.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")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
@@ -4837,6 +4906,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("double precision")
|
||||
.HasComment("纬度。");
|
||||
|
||||
b.Property<string>("LegalRepresentative")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasComment("门店法人(主体不一致模式使用)。");
|
||||
|
||||
b.Property<double?>("Longitude")
|
||||
.HasColumnType("double precision")
|
||||
.HasComment("高德/腾讯地图经度。");
|
||||
@@ -4866,6 +4940,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("所在省份。");
|
||||
|
||||
b.Property<string>("RegisteredAddress")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("门店注册地址(主体不一致模式使用)。");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("门店当前运营状态。");
|
||||
@@ -4908,6 +4987,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MerchantId", "BusinessLicenseNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||
|
||||
b.HasIndex("TenantId", "Code")
|
||||
.IsUnique();
|
||||
|
||||
@@ -5704,6 +5787,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasComment("租户全称或品牌名称。");
|
||||
|
||||
b.Property<int?>("OperatingMode")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("经营模式(同一主体/不同主体)。");
|
||||
|
||||
b.Property<long?>("PrimaryOwnerUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("系统内对应的租户所有者账号 ID。");
|
||||
@@ -5806,6 +5893,18 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.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")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("发布者范围。");
|
||||
@@ -5814,32 +5913,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("发布者用户 ID(平台或租户后台账号)。");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("公告状态。");
|
||||
|
||||
b.Property<DateTime?>("PublishedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("实际发布时间(UTC)。");
|
||||
|
||||
b.Property<DateTime?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.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")
|
||||
.IsConcurrencyToken()
|
||||
.IsRequired()
|
||||
@@ -5847,13 +5924,23 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("bytea")
|
||||
.HasComment("并发控制字段。");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasComment("是否启用(已弃用,迁移期保留)。");
|
||||
b.Property<DateTime?>("ScheduledPublishAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("预定发布时间(UTC)。");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
b.Property<int>("Status")
|
||||
.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")
|
||||
.HasColumnType("bigint")
|
||||
@@ -5875,15 +5962,15 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status", "EffectiveFrom")
|
||||
.HasFilter("\"TenantId\" = 0");
|
||||
|
||||
b.HasIndex("TenantId", "AnnouncementType", "IsActive");
|
||||
|
||||
b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo");
|
||||
|
||||
b.HasIndex("TenantId", "Status", "EffectiveFrom");
|
||||
|
||||
b.HasIndex("Status", "EffectiveFrom")
|
||||
.HasFilter("\"TenantId\" = 0");
|
||||
|
||||
b.ToTable("tenant_announcements", null, t =>
|
||||
{
|
||||
t.HasComment("租户公告。");
|
||||
|
||||
Reference in New Issue
Block a user