feat: 商户冻结/解冻功能及字典缓存重构

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MSuMshk
2026-02-04 10:46:32 +08:00
parent 754dd788ea
commit f69904e195
54 changed files with 753 additions and 1385 deletions

View File

@@ -0,0 +1,24 @@
using MediatR;
namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 冻结商户命令。
/// </summary>
public sealed record FreezeMerchantCommand : IRequest<bool>
{
/// <summary>
/// 商户 ID。
/// </summary>
public long MerchantId { get; init; }
/// <summary>
/// 冻结原因。
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// 并发控制版本PostgreSQL xmin
/// </summary>
public uint RowVersion { get; init; }
}

View File

@@ -0,0 +1,19 @@
using MediatR;
namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 解冻商户命令。
/// </summary>
public sealed record UnfreezeMerchantCommand : IRequest<bool>
{
/// <summary>
/// 商户 ID。
/// </summary>
public long MerchantId { get; init; }
/// <summary>
/// 并发控制版本PostgreSQL xmin
/// </summary>
public uint RowVersion { get; init; }
}

View File

@@ -18,21 +18,36 @@ public sealed record UpdateMerchantCommand : IRequest<UpdateMerchantResultDto?>
/// </summary>
public string? Name { get; init; }
/// <summary>
/// 品牌简称。
/// </summary>
public string? BrandAlias { get; init; }
/// <summary>
/// Logo URL。
/// </summary>
public string? LogoUrl { get; init; }
/// <summary>
/// 品类。
/// </summary>
public string? Category { get; init; }
/// <summary>
/// 营业执照号。
/// </summary>
public string? LicenseNumber { get; init; }
/// <summary>
/// 税号。
/// </summary>
public string? TaxNumber { get; init; }
/// <summary>
/// 法人或负责人。
/// </summary>
public string? LegalRepresentative { get; init; }
/// <summary>
/// 注册地址。
/// </summary>
public string? RegisteredAddress { get; init; }
/// <summary>
/// 联系电话。
/// </summary>
@@ -44,7 +59,42 @@ public sealed record UpdateMerchantCommand : IRequest<UpdateMerchantResultDto?>
public string? ContactEmail { get; init; }
/// <summary>
/// 并发控制版本
/// 客服电话
/// </summary>
public byte[] RowVersion { get; init; } = Array.Empty<byte>();
public string? ServicePhone { get; init; }
/// <summary>
/// 客服邮箱。
/// </summary>
public string? SupportEmail { get; init; }
/// <summary>
/// 省份。
/// </summary>
public string? Province { get; init; }
/// <summary>
/// 城市。
/// </summary>
public string? City { get; init; }
/// <summary>
/// 区县。
/// </summary>
public string? District { get; init; }
/// <summary>
/// 详细地址。
/// </summary>
public string? Address { get; init; }
/// <summary>
/// 并发控制版本PostgreSQL xmin
/// </summary>
public uint RowVersion { get; init; }
/// <summary>
/// 是否跳过敏感字段审核Admin 端设置为 true
/// </summary>
public bool SkipReview { get; init; }
}

View File

@@ -0,0 +1,25 @@
using MediatR;
using TakeoutSaaS.Domain.Common.Enums;
namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 更新商户经营模式命令。
/// </summary>
public sealed record UpdateMerchantOperatingModeCommand : IRequest<bool>
{
/// <summary>
/// 商户 ID。
/// </summary>
public long MerchantId { get; init; }
/// <summary>
/// 经营模式。
/// </summary>
public OperatingMode OperatingMode { get; init; }
/// <summary>
/// 并发控制版本PostgreSQL xmin
/// </summary>
public uint RowVersion { get; init; }
}

View File

@@ -28,10 +28,25 @@ public sealed class MerchantDetailDto
public string? TenantName { get; init; }
/// <summary>
/// 商户名称。
/// 商户名称(品牌名)
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 品牌简称。
/// </summary>
public string? BrandAlias { get; init; }
/// <summary>
/// Logo 地址。
/// </summary>
public string? LogoUrl { get; init; }
/// <summary>
/// 品类。
/// </summary>
public string? Category { get; init; }
/// <summary>
/// 经营模式。
/// </summary>
@@ -48,9 +63,29 @@ public sealed class MerchantDetailDto
public string? LegalRepresentative { get; init; }
/// <summary>
/// 注册地址
/// 税号
/// </summary>
public string? RegisteredAddress { get; init; }
public string? TaxNumber { get; init; }
/// <summary>
/// 省份。
/// </summary>
public string? Province { get; init; }
/// <summary>
/// 城市。
/// </summary>
public string? City { get; init; }
/// <summary>
/// 区县。
/// </summary>
public string? District { get; init; }
/// <summary>
/// 详细地址。
/// </summary>
public string? Address { get; init; }
/// <summary>
/// 联系电话。
@@ -62,6 +97,16 @@ public sealed class MerchantDetailDto
/// </summary>
public string? ContactEmail { get; init; }
/// <summary>
/// 客服电话。
/// </summary>
public string? ServicePhone { get; init; }
/// <summary>
/// 客服邮箱。
/// </summary>
public string? SupportEmail { get; init; }
/// <summary>
/// 审核状态。
/// </summary>
@@ -99,9 +144,9 @@ public sealed class MerchantDetailDto
public IReadOnlyList<MerchantStoreDto> Stores { get; init; } = [];
/// <summary>
/// 并发控制版本。
/// 并发控制版本PostgreSQL xmin
/// </summary>
public byte[] RowVersion { get; init; } = Array.Empty<byte>();
public uint RowVersion { get; init; }
/// <summary>
/// 创建时间。

View File

@@ -1,23 +1,27 @@
using MediatR;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
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.Security;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// <summary>
/// 创建商户命令处理器。
/// </summary>
public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRepository, ILogger<CreateMerchantCommandHandler> logger)
public sealed class CreateMerchantCommandHandler(
IMerchantRepository merchantRepository,
ICurrentUserAccessor currentUserAccessor,
ILogger<CreateMerchantCommandHandler> logger)
: IRequestHandler<CreateMerchantCommand, MerchantDto>
{
/// <inheritdoc />
public async Task<MerchantDto> Handle(CreateMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 构建商户实体
// 1. 构建商户实体RowVersion 由 PostgreSQL xmin 自动管理)
var merchant = new Merchant
{
TenantId = request.TenantId,
@@ -28,16 +32,39 @@ public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRep
ContactPhone = request.ContactPhone.Trim(),
ContactEmail = request.ContactEmail?.Trim(),
Status = request.Status,
RowVersion = RandomNumberGenerator.GetBytes(16),
JoinedAt = DateTime.UtcNow
};
// 2. 持久化
// 2. 如果状态为已通过,设置审核通过时间和审核人
if (request.Status == MerchantStatus.Approved)
{
merchant.ApprovedAt = DateTime.UtcNow;
merchant.ApprovedBy = currentUserAccessor.UserId;
}
// 3. 持久化商户
await merchantRepository.AddMerchantAsync(merchant, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
// 3. 记录日志
logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName);
// 4. 如果状态为已通过,添加默认审核通过记录
if (request.Status == MerchantStatus.Approved)
{
var auditLog = new MerchantAuditLog
{
TenantId = merchant.TenantId,
MerchantId = merchant.Id,
Action = MerchantAuditAction.ReviewApproved,
Title = "商户创建时直接通过审核",
Description = "平台管理员创建商户时选择无需审核,系统自动通过",
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
OperatorName = currentUserAccessor.UserId == 0 ? "system" : $"user:{currentUserAccessor.UserId}"
};
await merchantRepository.AddAuditLogAsync(auditLog, cancellationToken);
await merchantRepository.SaveChangesAsync(cancellationToken);
}
// 5. 记录日志
logger.LogInformation("创建商户 {MerchantId} - {BrandName},状态:{Status}", merchant.Id, merchant.BrandName, merchant.Status);
return MapToDto(merchant);
}

View File

@@ -0,0 +1,84 @@
using MediatR;
using Microsoft.Extensions.Logging;
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 FreezeMerchantCommandHandler(
IMerchantRepository merchantRepository,
ICurrentUserAccessor currentUserAccessor,
ILogger<FreezeMerchantCommandHandler> logger)
: IRequestHandler<FreezeMerchantCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(FreezeMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 验证 RowVersion
if (request.RowVersion == 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
// 2. 读取商户信息
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
return false;
}
// 3. 检查是否已冻结
if (merchant.IsFrozen)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "商户已处于冻结状态");
}
// 4. 执行冻结
var now = DateTime.UtcNow;
var actorId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
var actorName = currentUserAccessor.IsAuthenticated ? $"user:{currentUserAccessor.UserId}" : "system";
merchant.IsFrozen = true;
merchant.FrozenReason = request.Reason?.Trim();
merchant.FrozenAt = now;
merchant.RowVersion = request.RowVersion;
// 5. 记录审核日志
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = merchant.TenantId,
MerchantId = merchant.Id,
Action = MerchantAuditAction.Frozen,
Title = "商户冻结",
Description = string.IsNullOrWhiteSpace(request.Reason) ? "管理员冻结商户" : $"冻结原因:{request.Reason.Trim()}",
OperatorId = actorId,
OperatorName = actorName
}, cancellationToken);
// 6. 持久化
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
try
{
await merchantRepository.SaveChangesAsync(cancellationToken);
}
catch (Exception exception) when (IsConcurrencyException(exception))
{
throw new BusinessException(ErrorCodes.Conflict, "商户信息已被修改,请刷新后重试");
}
logger.LogInformation("冻结商户 {MerchantId},原因:{Reason}", merchant.Id, request.Reason);
return true;
}
private static bool IsConcurrencyException(Exception exception)
=> string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal);
}

View File

@@ -0,0 +1,83 @@
using MediatR;
using Microsoft.Extensions.Logging;
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 UnfreezeMerchantCommandHandler(
IMerchantRepository merchantRepository,
ICurrentUserAccessor currentUserAccessor,
ILogger<UnfreezeMerchantCommandHandler> logger)
: IRequestHandler<UnfreezeMerchantCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(UnfreezeMerchantCommand request, CancellationToken cancellationToken)
{
// 1. 验证 RowVersion
if (request.RowVersion == 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
// 2. 读取商户信息
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
return false;
}
// 3. 检查是否已解冻
if (!merchant.IsFrozen)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "商户未处于冻结状态");
}
// 4. 执行解冻
var actorId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
var actorName = currentUserAccessor.IsAuthenticated ? $"user:{currentUserAccessor.UserId}" : "system";
merchant.IsFrozen = false;
merchant.FrozenReason = null;
merchant.FrozenAt = null;
merchant.RowVersion = request.RowVersion;
// 5. 记录审核日志
await merchantRepository.AddAuditLogAsync(new MerchantAuditLog
{
TenantId = merchant.TenantId,
MerchantId = merchant.Id,
Action = MerchantAuditAction.Unfrozen,
Title = "商户解冻",
Description = "管理员解冻商户",
OperatorId = actorId,
OperatorName = actorName
}, cancellationToken);
// 6. 持久化
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
try
{
await merchantRepository.SaveChangesAsync(cancellationToken);
}
catch (Exception exception) when (IsConcurrencyException(exception))
{
throw new BusinessException(ErrorCodes.Conflict, "商户信息已被修改,请刷新后重试");
}
logger.LogInformation("解冻商户 {MerchantId}", merchant.Id);
return true;
}
private static bool IsConcurrencyException(Exception exception)
=> string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal);
}

View File

@@ -27,7 +27,7 @@ public sealed class UpdateMerchantCommandHandler(
/// <inheritdoc />
public async Task<UpdateMerchantResultDto?> Handle(UpdateMerchantCommand request, CancellationToken cancellationToken)
{
if (request.RowVersion == null || request.RowVersion.Length == 0)
if (request.RowVersion == 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
@@ -43,10 +43,19 @@ public sealed class UpdateMerchantCommandHandler(
// 2. (空行后) 规范化输入
var name = NormalizeRequired(request.Name, "商户名称");
var contactPhone = NormalizeRequired(request.ContactPhone, "联系电话");
var brandAlias = NormalizeOptional(request.BrandAlias);
var logoUrl = NormalizeOptional(request.LogoUrl);
var category = NormalizeOptional(request.Category);
var licenseNumber = NormalizeOptional(request.LicenseNumber);
var taxNumber = NormalizeOptional(request.TaxNumber);
var legalRepresentative = NormalizeOptional(request.LegalRepresentative);
var registeredAddress = NormalizeOptional(request.RegisteredAddress);
var contactEmail = NormalizeOptional(request.ContactEmail);
var servicePhone = NormalizeOptional(request.ServicePhone);
var supportEmail = NormalizeOptional(request.SupportEmail);
var province = NormalizeOptional(request.Province);
var city = NormalizeOptional(request.City);
var district = NormalizeOptional(request.District);
var address = NormalizeOptional(request.Address);
var now = DateTime.UtcNow;
var actorId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
@@ -54,23 +63,44 @@ public sealed class UpdateMerchantCommandHandler(
var changes = new List<MerchantChangeLog>();
var criticalChanged = false;
// 判断是否为管理员操作Admin 端修改不触发审核)
var isAdminOperation = request.SkipReview;
TrackChange("name", merchant.BrandName, name, isCritical: true);
TrackChange("brandAlias", merchant.BrandAlias, brandAlias, isCritical: false);
TrackChange("logoUrl", merchant.LogoUrl, logoUrl, isCritical: false);
TrackChange("category", merchant.Category, category, isCritical: false);
TrackChange("licenseNumber", merchant.BusinessLicenseNumber, licenseNumber, isCritical: true);
TrackChange("taxNumber", merchant.TaxNumber, taxNumber, isCritical: false);
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);
TrackChange("servicePhone", merchant.ServicePhone, servicePhone, isCritical: false);
TrackChange("supportEmail", merchant.SupportEmail, supportEmail, isCritical: false);
TrackChange("province", merchant.Province, province, isCritical: false);
TrackChange("city", merchant.City, city, isCritical: false);
TrackChange("district", merchant.District, district, isCritical: false);
TrackChange("address", merchant.Address, address, isCritical: true);
// 3. (空行后) 写入字段
merchant.BrandName = name;
merchant.BrandAlias = brandAlias;
merchant.LogoUrl = logoUrl;
merchant.Category = category;
merchant.BusinessLicenseNumber = licenseNumber;
merchant.TaxNumber = taxNumber;
merchant.LegalPerson = legalRepresentative;
merchant.Address = registeredAddress;
merchant.ContactPhone = contactPhone;
merchant.ContactEmail = contactEmail;
merchant.ServicePhone = servicePhone;
merchant.SupportEmail = supportEmail;
merchant.Province = province;
merchant.City = city;
merchant.District = district;
merchant.Address = address;
merchant.RowVersion = request.RowVersion;
var requiresReview = merchant.Status == MerchantStatus.Approved && criticalChanged;
var requiresReview = !isAdminOperation && merchant.Status == MerchantStatus.Approved && criticalChanged;
if (requiresReview)
{
merchant.Status = MerchantStatus.Pending;

View File

@@ -0,0 +1,80 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Domain.Merchants.Entities;
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 UpdateMerchantOperatingModeCommandHandler(
IMerchantRepository merchantRepository,
ICurrentUserAccessor currentUserAccessor,
ILogger<UpdateMerchantOperatingModeCommandHandler> logger)
: IRequestHandler<UpdateMerchantOperatingModeCommand, bool>
{
/// <inheritdoc />
public async Task<bool> Handle(UpdateMerchantOperatingModeCommand request, CancellationToken cancellationToken)
{
// 1. 验证 RowVersion
if (request.RowVersion == 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
}
// 2. 读取商户信息
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken);
if (merchant == null)
{
return false;
}
// 3. 记录变更日志
var actorId = currentUserAccessor.UserId == 0 ? (long?)null : currentUserAccessor.UserId;
var actorName = currentUserAccessor.IsAuthenticated ? $"user:{currentUserAccessor.UserId}" : "system";
var oldValue = merchant.OperatingMode?.ToString();
var newValue = request.OperatingMode.ToString();
if (!string.Equals(oldValue, newValue, StringComparison.Ordinal))
{
await merchantRepository.AddChangeLogAsync(new MerchantChangeLog
{
TenantId = merchant.TenantId,
MerchantId = merchant.Id,
FieldName = "operatingMode",
OldValue = oldValue,
NewValue = newValue,
ChangedBy = actorId,
ChangedByName = actorName,
ChangeType = "Update"
}, cancellationToken);
}
// 4. 更新经营模式
merchant.OperatingMode = request.OperatingMode;
merchant.RowVersion = request.RowVersion;
// 5. 持久化
await merchantRepository.UpdateMerchantAsync(merchant, cancellationToken);
try
{
await merchantRepository.SaveChangesAsync(cancellationToken);
}
catch (Exception exception) when (IsConcurrencyException(exception))
{
throw new BusinessException(ErrorCodes.Conflict, "商户信息已被修改,请刷新后重试");
}
logger.LogInformation("更新商户 {MerchantId} 经营模式为 {OperatingMode}", merchant.Id, request.OperatingMode);
return true;
}
private static bool IsConcurrencyException(Exception exception)
=> string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal);
}

View File

@@ -56,12 +56,21 @@ internal static class MerchantMapping
TenantId = merchant.TenantId,
TenantName = tenantName,
Name = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
OperatingMode = merchant.OperatingMode,
LicenseNumber = merchant.BusinessLicenseNumber,
LegalRepresentative = merchant.LegalPerson,
RegisteredAddress = merchant.Address,
TaxNumber = merchant.TaxNumber,
Province = merchant.Province,
City = merchant.City,
District = merchant.District,
Address = merchant.Address,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
ServicePhone = merchant.ServicePhone,
SupportEmail = merchant.SupportEmail,
Status = merchant.Status,
IsFrozen = merchant.IsFrozen,
FrozenReason = merchant.FrozenReason,

View File

@@ -15,12 +15,22 @@ public sealed class UpdateMerchantCommandValidator : AbstractValidator<UpdateMer
{
RuleFor(x => x.MerchantId).GreaterThan(0);
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
RuleFor(x => x.BrandAlias).MaximumLength(64);
RuleFor(x => x.LogoUrl).MaximumLength(512);
RuleFor(x => x.Category).MaximumLength(64);
RuleFor(x => x.LicenseNumber).MaximumLength(64);
RuleFor(x => x.TaxNumber).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().MaximumLength(128)
.When(x => !string.IsNullOrWhiteSpace(x.ContactEmail));
RuleFor(x => x.ServicePhone).MaximumLength(32);
RuleFor(x => x.SupportEmail).EmailAddress().MaximumLength(128)
.When(x => !string.IsNullOrWhiteSpace(x.SupportEmail));
RuleFor(x => x.Province).MaximumLength(64);
RuleFor(x => x.City).MaximumLength(64);
RuleFor(x => x.District).MaximumLength(64);
RuleFor(x => x.Address).MaximumLength(256);
RuleFor(x => x.RowVersion).NotEmpty();
}
}