diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs
index c5b52d0..79b4753 100644
--- a/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/MerchantsController.cs
@@ -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
///
/// 查询商户列表。
///
- /// 状态筛选。
- /// 页码。
- /// 每页大小。
- /// 排序字段。
- /// 是否倒序。
+ /// 查询参数。
/// 取消标记。
/// 商户分页结果。
[HttpGet]
[PermissionAuthorize("merchant:read")]
- [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
- public async Task>> List(
- [FromQuery] MerchantStatus? status,
- [FromQuery] int page = 1,
- [FromQuery] int pageSize = 20,
- [FromQuery] string? sortBy = null,
- [FromQuery] bool sortDesc = true,
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> 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>.Ok(result);
+ }
- // 2. 返回分页结果
- return ApiResponse>.Ok(result);
+ ///
+ /// 待审核商户列表。
+ ///
+ [HttpGet("pending-review")]
+ [PermissionAuthorize("merchant:review")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> PendingReviewList(
+ [FromQuery] GetPendingReviewListQuery query,
+ CancellationToken cancellationToken)
+ {
+ var result = await mediator.Send(query, cancellationToken);
+ return ApiResponse>.Ok(result);
}
///
@@ -82,23 +79,36 @@ public sealed class MerchantsController(IMediator mediator) : BaseApiController
/// 更新后的商户或未找到。
[HttpPut("{merchantId:long}")]
[PermissionAuthorize("merchant:update")]
- [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status422UnprocessableEntity)]
[ProducesResponseType(typeof(ApiResponse
public TenantVerificationStatus VerificationStatus { get; init; }
+ ///
+ /// 经营模式。
+ ///
+ public OperatingMode? OperatingMode { get; init; }
+
///
/// 当前套餐 ID。
///
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs
index d45b6e9..8e4e477 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/ReviewTenantCommandHandler.cs
@@ -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;
///
public sealed class ReviewTenantCommandHandler(
ITenantRepository tenantRepository,
+ IMerchantRepository merchantRepository,
ICurrentUserAccessor currentUserAccessor)
: IRequestHandler
{
@@ -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);
}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
index 125f991..0616ea9 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
@@ -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,
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs
new file mode 100644
index 0000000..84bd0a2
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Validators/ReviewTenantValidator.cs
@@ -0,0 +1,28 @@
+using FluentValidation;
+using TakeoutSaaS.Application.App.Tenants.Commands;
+
+namespace TakeoutSaaS.Application.App.Tenants.Validators;
+
+///
+/// 租户审核命令验证器。
+///
+public sealed class ReviewTenantValidator : AbstractValidator
+{
+ ///
+ /// 初始化验证规则。
+ ///
+ 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);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/MenuPolicy.cs b/src/Application/TakeoutSaaS.Application/Identity/MenuPolicy.cs
index 0e0b08d..4b8022b 100644
--- a/src/Application/TakeoutSaaS.Application/Identity/MenuPolicy.cs
+++ b/src/Application/TakeoutSaaS.Application/Identity/MenuPolicy.cs
@@ -8,5 +8,5 @@ public static class MenuPolicy
///
/// 是否允许维护菜单(创建/更新/删除)。
///
- public const bool CanMaintainMenus = false;
+ public static bool CanMaintainMenus { get; } = false;
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Common/Enums/OperatingMode.cs b/src/Domain/TakeoutSaaS.Domain/Common/Enums/OperatingMode.cs
new file mode 100644
index 0000000..b8961cb
--- /dev/null
+++ b/src/Domain/TakeoutSaaS.Domain/Common/Enums/OperatingMode.cs
@@ -0,0 +1,17 @@
+namespace TakeoutSaaS.Domain.Common.Enums;
+
+///
+/// 经营模式。
+///
+public enum OperatingMode
+{
+ ///
+ /// 同一主体。
+ ///
+ SameEntity = 1,
+
+ ///
+ /// 不同主体。
+ ///
+ DifferentEntity = 2
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs
index d05ae41..e6b0f73 100644
--- a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/Merchant.cs
@@ -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
///
public MerchantStatus Status { get; set; } = MerchantStatus.Pending;
+ ///
+ /// 经营模式(同一主体/不同主体)。
+ ///
+ public OperatingMode? OperatingMode { get; set; }
+
+ ///
+ /// 是否冻结业务。
+ ///
+ public bool IsFrozen { get; set; }
+
+ ///
+ /// 冻结原因。
+ ///
+ public string? FrozenReason { get; set; }
+
+ ///
+ /// 冻结时间。
+ ///
+ public DateTime? FrozenAt { get; set; }
+
+ ///
+ /// 最近一次审核人。
+ ///
+ public long? LastReviewedBy { get; set; }
+
///
/// 审核备注或驳回原因。
///
@@ -117,4 +143,39 @@ public sealed class Merchant : MultiTenantEntityBase
/// 最近一次审核时间。
///
public DateTime? LastReviewedAt { get; set; }
+
+ ///
+ /// 审核通过人。
+ ///
+ public long? ApprovedBy { get; set; }
+
+ ///
+ /// 审核通过时间。
+ ///
+ public DateTime? ApprovedAt { get; set; }
+
+ ///
+ /// 当前领取人。
+ ///
+ public long? ClaimedBy { get; set; }
+
+ ///
+ /// 当前领取人姓名。
+ ///
+ public string? ClaimedByName { get; set; }
+
+ ///
+ /// 领取时间。
+ ///
+ public DateTime? ClaimedAt { get; set; }
+
+ ///
+ /// 领取过期时间。
+ ///
+ public DateTime? ClaimExpiresAt { get; set; }
+
+ ///
+ /// 并发控制版本。
+ ///
+ public byte[] RowVersion { get; set; } = Array.Empty();
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantAuditLog.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantAuditLog.cs
index e41d3e6..2f0a492 100644
--- a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantAuditLog.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantAuditLog.cs
@@ -37,4 +37,9 @@ public sealed class MerchantAuditLog : MultiTenantEntityBase
/// 操作人名称。
///
public string? OperatorName { get; set; }
+
+ ///
+ /// 操作 IP。
+ ///
+ public string? IpAddress { get; set; }
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantChangeLog.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantChangeLog.cs
new file mode 100644
index 0000000..a67e98f
--- /dev/null
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Entities/MerchantChangeLog.cs
@@ -0,0 +1,49 @@
+using TakeoutSaaS.Shared.Abstractions.Entities;
+
+namespace TakeoutSaaS.Domain.Merchants.Entities;
+
+///
+/// 商户变更日志。
+///
+public sealed class MerchantChangeLog : MultiTenantEntityBase
+{
+ ///
+ /// 商户标识。
+ ///
+ public long MerchantId { get; set; }
+
+ ///
+ /// 变更字段名。
+ ///
+ public string FieldName { get; set; } = string.Empty;
+
+ ///
+ /// 变更前值。
+ ///
+ public string? OldValue { get; set; }
+
+ ///
+ /// 变更后值。
+ ///
+ public string? NewValue { get; set; }
+
+ ///
+ /// 变更类型。
+ ///
+ public string ChangeType { get; set; } = "Update";
+
+ ///
+ /// 变更人 ID。
+ ///
+ public long? ChangedBy { get; set; }
+
+ ///
+ /// 变更人名称。
+ ///
+ public string? ChangedByName { get; set; }
+
+ ///
+ /// 变更原因。
+ ///
+ public string? ChangeReason { get; set; }
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantAuditAction.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantAuditAction.cs
index 330f6c4..865b1ec 100644
--- a/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantAuditAction.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Enums/MerchantAuditAction.cs
@@ -33,5 +33,40 @@ public enum MerchantAuditAction
///
/// 商户审核结果。
///
- MerchantReviewed = 5
+ MerchantReviewed = 5,
+
+ ///
+ /// 领取审核。
+ ///
+ ReviewClaimed = 6,
+
+ ///
+ /// 释放审核。
+ ///
+ ReviewReleased = 7,
+
+ ///
+ /// 审核通过。
+ ///
+ ReviewApproved = 8,
+
+ ///
+ /// 审核驳回。
+ ///
+ ReviewRejected = 9,
+
+ ///
+ /// 撤销审核。
+ ///
+ ReviewRevoked = 10,
+
+ ///
+ /// 关键信息变更进入待审核。
+ ///
+ ReviewPendingReApproval = 11,
+
+ ///
+ /// 强制接管审核。
+ ///
+ ReviewForceClaimed = 12
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs
index 1983bed..1dd5a0f 100644
--- a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs
@@ -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
/// 商户实体或 null。
Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
+ ///
+ /// 依据标识获取商户(忽略租户过滤)。
+ ///
+ /// 商户 ID。
+ /// 取消标记。
+ /// 商户实体或 null。
+ Task FindByIdAsync(long merchantId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 依据租户标识获取商户(忽略租户过滤)。
+ ///
+ /// 租户 ID。
+ /// 取消标记。
+ /// 商户实体或 null。
+ Task FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default);
+
///
/// 按状态筛选商户列表。
///
@@ -26,6 +43,22 @@ public interface IMerchantRepository
/// 商户集合。
Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default);
+ ///
+ /// 按条件筛选商户列表(支持跨租户)。
+ ///
+ /// 租户 ID,为 null 时查询全部租户。
+ /// 状态过滤。
+ /// 经营模式过滤。
+ /// 关键词过滤。
+ /// 取消标记。
+ /// 商户集合。
+ Task> SearchAsync(
+ long? tenantId,
+ MerchantStatus? status,
+ OperatingMode? operatingMode,
+ string? keyword,
+ CancellationToken cancellationToken = default);
+
///
/// 获取指定商户的员工列表。
///
@@ -168,6 +201,14 @@ public interface IMerchantRepository
/// 异步任务。
Task AddAuditLogAsync(MerchantAuditLog log, CancellationToken cancellationToken = default);
+ ///
+ /// 记录变更日志。
+ ///
+ /// 变更日志实体。
+ /// 取消标记。
+ /// 异步任务。
+ Task AddChangeLogAsync(MerchantChangeLog log, CancellationToken cancellationToken = default);
+
///
/// 获取审核日志。
///
@@ -176,4 +217,18 @@ public interface IMerchantRepository
/// 取消标记。
/// 审核日志列表。
Task> GetAuditLogsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
+
+ ///
+ /// 获取变更日志。
+ ///
+ /// 商户 ID。
+ /// 租户 ID。
+ /// 字段过滤。
+ /// 取消标记。
+ /// 变更日志列表。
+ Task> GetChangeLogsAsync(
+ long merchantId,
+ long tenantId,
+ string? fieldName = null,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Services/IMerchantExportService.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Services/IMerchantExportService.cs
new file mode 100644
index 0000000..19912e5
--- /dev/null
+++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Services/IMerchantExportService.cs
@@ -0,0 +1,26 @@
+using TakeoutSaaS.Domain.Merchants.Entities;
+using TakeoutSaaS.Domain.Stores.Entities;
+
+namespace TakeoutSaaS.Domain.Merchants.Services;
+
+///
+/// 商户导出服务接口。
+///
+public interface IMerchantExportService
+{
+ ///
+ /// 导出为 PDF。
+ ///
+ /// 商户主体。
+ /// 租户名称。
+ /// 门店列表。
+ /// 审核历史。
+ /// 取消标记。
+ /// PDF 字节数组。
+ Task ExportToPdfAsync(
+ Merchant merchant,
+ string? tenantName,
+ IReadOnlyList stores,
+ IReadOnlyList auditLogs,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs
index f3c8e44..b4364e2 100644
--- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/Store.cs
@@ -33,6 +33,26 @@ public sealed class Store : MultiTenantEntityBase
///
public string? ManagerName { get; set; }
+ ///
+ /// 门店营业执照号(主体不一致模式使用)。
+ ///
+ public string? BusinessLicenseNumber { get; set; }
+
+ ///
+ /// 门店法人(主体不一致模式使用)。
+ ///
+ public string? LegalRepresentative { get; set; }
+
+ ///
+ /// 门店注册地址(主体不一致模式使用)。
+ ///
+ public string? RegisteredAddress { get; set; }
+
+ ///
+ /// 门店营业执照图片地址(主体不一致模式使用)。
+ ///
+ public string? BusinessLicenseImageUrl { get; set; }
+
///
/// 门店当前运营状态。
///
diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs
index fd941bc..9a1b65c 100644
--- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs
@@ -13,11 +13,21 @@ public interface IStoreRepository
///
Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
+ ///
+ /// 获取指定商户的门店列表。
+ ///
+ Task> GetByMerchantIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
+
///
/// 按租户筛选门店列表。
///
Task> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default);
+ ///
+ /// 获取指定商户集合的门店数量。
+ ///
+ Task> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection merchantIds, CancellationToken cancellationToken = default);
+
///
/// 获取门店营业时段。
///
diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs
index b2823e3..d689c2b 100644
--- a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/Tenant.cs
@@ -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
///
public TenantStatus Status { get; set; } = TenantStatus.PendingReview;
+ ///
+ /// 经营模式(同一主体/不同主体)。
+ ///
+ public OperatingMode? OperatingMode { get; set; }
+
///
/// 服务生效时间(UTC)。
///
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs
index 4265143..08e10db 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs
@@ -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();
services.AddScoped();
+ services.AddScoped();
services.AddOptions()
.Bind(configuration.GetSection(AppSeedOptions.SectionName))
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs
index 40a3e0d..173cccb 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs
@@ -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();
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();
+ 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 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 builder)
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs
index db41cdd..3c50cd5 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs
@@ -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);
}
+ ///
+ public Task FindByIdAsync(long merchantId, CancellationToken cancellationToken = default)
+ {
+ return context.Merchants
+ .IgnoreQueryFilters()
+ .AsNoTracking()
+ .Where(x => x.Id == merchantId)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
+ ///
+ public Task FindByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default)
+ {
+ return context.Merchants
+ .IgnoreQueryFilters()
+ .AsNoTracking()
+ .Where(x => x.TenantId == tenantId)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
///
public async Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default)
{
@@ -208,9 +229,79 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog
public async Task> 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);
}
+
+ ///
+ public async Task> 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);
+ }
+
+ ///
+ public Task AddChangeLogAsync(MerchantChangeLog log, CancellationToken cancellationToken = default)
+ {
+ return logsContext.MerchantChangeLogs.AddAsync(log, cancellationToken).AsTask();
+ }
+
+ ///
+ public async Task> 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);
+ }
}
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs
index 23c67ce..665e212 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs
@@ -25,6 +25,16 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
.FirstOrDefaultAsync(cancellationToken);
}
+ ///
+ public async Task> 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);
+ }
+
///
public async Task> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
{
@@ -44,6 +54,31 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
return stores;
}
+ ///
+ public async Task> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection merchantIds, CancellationToken cancellationToken = default)
+ {
+ if (merchantIds.Count == 0)
+ {
+ return new Dictionary();
+ }
+
+ 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);
+ }
+
///
public async Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/MerchantExportService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/MerchantExportService.cs
new file mode 100644
index 0000000..dd56293
--- /dev/null
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/MerchantExportService.cs
@@ -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;
+
+///
+/// 商户导出服务实现(PDF)。
+///
+public sealed class MerchantExportService : IMerchantExportService
+{
+ public MerchantExportService()
+ {
+ QuestPDF.Settings.License = LicenseType.Community;
+ }
+
+ ///
+ public Task ExportToPdfAsync(
+ Merchant merchant,
+ string? tenantName,
+ IReadOnlyList stores,
+ IReadOnlyList auditLogs,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(merchant);
+
+ var safeStores = stores ?? Array.Empty();
+ var safeAuditLogs = auditLogs ?? Array.Empty();
+
+ 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 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 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) : "-";
+}
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs
index 655e566..a34444d 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs
@@ -30,6 +30,11 @@ public sealed class TakeoutLogsDbContext(
///
public DbSet MerchantAuditLogs => Set();
+ ///
+ /// 商户变更日志集合。
+ ///
+ public DbSet MerchantChangeLogs => Set();
+
///
/// 运营操作日志集合。
///
@@ -54,6 +59,7 @@ public sealed class TakeoutLogsDbContext(
base.OnModelCreating(modelBuilder);
ConfigureTenantAuditLog(modelBuilder.Entity());
ConfigureMerchantAuditLog(modelBuilder.Entity());
+ ConfigureMerchantChangeLog(modelBuilder.Entity());
ConfigureOperationLog(modelBuilder.Entity());
ConfigureOperationLogInboxMessage(modelBuilder.Entity());
ConfigureMemberGrowthLog(modelBuilder.Entity());
@@ -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().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 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 builder)
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251229071911_AddMerchantManagement.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251229071911_AddMerchantManagement.Designer.cs
new file mode 100644
index 0000000..36c3016
--- /dev/null
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20251229071911_AddMerchantManagement.Designer.cs
@@ -0,0 +1,7080 @@
+//
+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.App.Persistence;
+
+#nullable disable
+
+namespace TakeoutSaaS.Infrastructure.Migrations
+{
+ [DbContext(typeof(TakeoutAppDbContext))]
+ [Migration("20251229071911_AddMerchantManagement")]
+ partial class AddMerchantManagement
+ {
+ ///
+ 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.Analytics.Entities.MetricAlertRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ConditionJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("触发条件 JSON。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean")
+ .HasComment("是否启用。");
+
+ b.Property("MetricDefinitionId")
+ .HasColumnType("bigint")
+ .HasComment("关联指标。");
+
+ b.Property("NotificationChannels")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasComment("通知渠道。");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasComment("告警级别。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "MetricDefinitionId", "Severity");
+
+ b.ToTable("metric_alert_rules", null, t =>
+ {
+ t.HasComment("指标告警规则。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("指标编码。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DefaultAggregation")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("默认聚合方式。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Description")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("说明。");
+
+ b.Property("DimensionsJson")
+ .HasColumnType("text")
+ .HasComment("维度描述 JSON。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("指标名称。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "Code")
+ .IsUnique();
+
+ b.ToTable("metric_definitions", null, t =>
+ {
+ t.HasComment("指标定义,描述可观测的数据点。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("DimensionKey")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasComment("维度键(JSON)。");
+
+ b.Property("MetricDefinitionId")
+ .HasColumnType("bigint")
+ .HasComment("指标定义 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("Value")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)")
+ .HasComment("数值。");
+
+ b.Property("WindowEnd")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("统计时间窗口结束。");
+
+ b.Property("WindowStart")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("统计时间窗口开始。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd")
+ .IsUnique();
+
+ b.ToTable("metric_snapshots", null, t =>
+ {
+ t.HasComment("指标快照,用于大盘展示。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("券码或序列号。");
+
+ b.Property("CouponTemplateId")
+ .HasColumnType("bigint")
+ .HasComment("模板标识。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("ExpireAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("到期时间。");
+
+ b.Property("IssuedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("发放时间。");
+
+ b.Property("OrderId")
+ .HasColumnType("bigint")
+ .HasComment("订单 ID(已使用时记录)。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("状态。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("使用时间。");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasComment("归属用户。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "Code")
+ .IsUnique();
+
+ b.ToTable("coupons", null, t =>
+ {
+ t.HasComment("用户领取的券。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AllowStack")
+ .HasColumnType("boolean")
+ .HasComment("是否允许叠加其他优惠。");
+
+ b.Property("ChannelsJson")
+ .HasColumnType("text")
+ .HasComment("发放渠道(JSON)。");
+
+ b.Property("ClaimedQuantity")
+ .HasColumnType("integer")
+ .HasComment("已领取数量。");
+
+ b.Property("CouponType")
+ .HasColumnType("integer")
+ .HasComment("券类型。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Description")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("备注。");
+
+ b.Property("DiscountCap")
+ .HasColumnType("numeric")
+ .HasComment("折扣上限(针对折扣券)。");
+
+ b.Property("MinimumSpend")
+ .HasColumnType("numeric")
+ .HasComment("最低消费门槛。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("模板名称。");
+
+ b.Property("ProductScopeJson")
+ .HasColumnType("text")
+ .HasComment("适用品类或商品范围(JSON)。");
+
+ b.Property("RelativeValidDays")
+ .HasColumnType("integer")
+ .HasComment("有效天数(相对发放时间)。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("状态。");
+
+ b.Property("StoreScopeJson")
+ .HasColumnType("text")
+ .HasComment("适用门店 ID 集合(JSON)。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("TotalQuantity")
+ .HasColumnType("integer")
+ .HasComment("总发放数量上限。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("ValidFrom")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("可用开始时间。");
+
+ b.Property("ValidTo")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("可用结束时间。");
+
+ b.Property("Value")
+ .HasColumnType("numeric")
+ .HasComment("面值或折扣额度。");
+
+ b.HasKey("Id");
+
+ b.ToTable("coupon_templates", null, t =>
+ {
+ t.HasComment("优惠券模板。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AudienceDescription")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("目标人群描述。");
+
+ b.Property("BannerUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("营销素材(如 banner)。");
+
+ b.Property("Budget")
+ .HasColumnType("numeric")
+ .HasComment("预算金额。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("EndAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("结束时间。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("活动名称。");
+
+ b.Property("PromotionType")
+ .HasColumnType("integer")
+ .HasComment("活动类型。");
+
+ b.Property("RulesJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("活动规则 JSON。");
+
+ b.Property("StartAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("开始时间。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("活动状态。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.ToTable("promotion_campaigns", null, t =>
+ {
+ t.HasComment("营销活动配置。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChatSessionId")
+ .HasColumnType("bigint")
+ .HasComment("会话标识。");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasComment("消息内容。");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("消息类型(文字/图片/语音等)。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("IsRead")
+ .HasColumnType("boolean")
+ .HasComment("是否已读。");
+
+ b.Property("ReadAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("读取时间。");
+
+ b.Property("SenderType")
+ .HasColumnType("integer")
+ .HasComment("发送方类型。");
+
+ b.Property("SenderUserId")
+ .HasColumnType("bigint")
+ .HasComment("发送方用户 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "ChatSessionId", "CreatedAt");
+
+ b.ToTable("chat_messages", null, t =>
+ {
+ t.HasComment("会话消息。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AgentUserId")
+ .HasColumnType("bigint")
+ .HasComment("当前客服员工 ID。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("CustomerUserId")
+ .HasColumnType("bigint")
+ .HasComment("顾客用户 ID。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("EndedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("结束时间。");
+
+ b.Property("IsBotActive")
+ .HasColumnType("boolean")
+ .HasComment("是否机器人接待中。");
+
+ b.Property("SessionCode")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("会话编号。");
+
+ b.Property("StartedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("开始时间。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("会话状态。");
+
+ b.Property("StoreId")
+ .HasColumnType("bigint")
+ .HasComment("所属门店(可空为平台)。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "SessionCode")
+ .IsUnique();
+
+ b.ToTable("chat_sessions", null, t =>
+ {
+ t.HasComment("客服会话。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AssignedAgentId")
+ .HasColumnType("bigint")
+ .HasComment("指派的客服。");
+
+ b.Property("ClosedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("关闭时间。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("CustomerUserId")
+ .HasColumnType("bigint")
+ .HasComment("客户用户 ID。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("工单详情。");
+
+ b.Property("OrderId")
+ .HasColumnType("bigint")
+ .HasComment("关联订单(如有)。");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasComment("优先级。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("状态。");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("工单主题。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("TicketNo")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("工单编号。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "TicketNo")
+ .IsUnique();
+
+ b.ToTable("support_tickets", null, t =>
+ {
+ t.HasComment("客服工单。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AttachmentsJson")
+ .HasColumnType("text")
+ .HasComment("附件 JSON。");
+
+ b.Property("AuthorUserId")
+ .HasColumnType("bigint")
+ .HasComment("评论人 ID。");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasComment("评论内容。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("IsInternal")
+ .HasColumnType("boolean")
+ .HasComment("是否内部备注。");
+
+ b.Property("SupportTicketId")
+ .HasColumnType("bigint")
+ .HasComment("工单标识。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "SupportTicketId");
+
+ b.ToTable("ticket_comments", null, t =>
+ {
+ t.HasComment("工单评论/流转记录。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("DeliveryOrderId")
+ .HasColumnType("bigint")
+ .HasComment("配送单标识。");
+
+ b.Property("EventType")
+ .HasColumnType("integer")
+ .HasComment("事件类型。");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasComment("事件描述。");
+
+ b.Property("OccurredAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("发生时间。");
+
+ b.Property("Payload")
+ .HasColumnType("text")
+ .HasComment("原始数据 JSON。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "DeliveryOrderId", "EventType");
+
+ b.ToTable("delivery_events", null, t =>
+ {
+ t.HasComment("配送状态事件流水。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CourierName")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("骑手姓名。");
+
+ b.Property("CourierPhone")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("骑手电话。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property