完成门店管理后端接口与任务
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Deliveries.Repositories;
|
||||
using TakeoutSaaS.Domain.Inventory.Repositories;
|
||||
using TakeoutSaaS.Domain.Merchants.Repositories;
|
||||
@@ -66,6 +67,13 @@ public static class AppServiceCollectionExtensions
|
||||
services.AddScoped<IBillingExportService, BillingExportService>();
|
||||
services.AddScoped<IMerchantExportService, MerchantExportService>();
|
||||
|
||||
// 2. (空行后) 门店配置服务
|
||||
services.AddScoped<IGeoJsonValidationService, GeoJsonValidationService>();
|
||||
services.AddScoped<IDeliveryZoneService, DeliveryZoneService>();
|
||||
services.AddScoped<IStoreFeeCalculationService, StoreFeeCalculationService>();
|
||||
services.AddScoped<IStoreSchedulerService, StoreSchedulerService>();
|
||||
|
||||
// 3. (空行后) 初始化配置与种子
|
||||
services.AddOptions<AppSeedOptions>()
|
||||
.Bind(configuration.GetSection(AppSeedOptions.SectionName))
|
||||
.ValidateDataAnnotations();
|
||||
|
||||
@@ -25,6 +25,7 @@ using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence.Configurations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
@@ -35,8 +36,9 @@ public sealed class TakeoutAppDbContext(
|
||||
DbContextOptions<TakeoutAppDbContext> options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户聚合根。
|
||||
@@ -123,6 +125,18 @@ public sealed class TakeoutAppDbContext(
|
||||
/// </summary>
|
||||
public DbSet<Store> Stores => Set<Store>();
|
||||
/// <summary>
|
||||
/// 门店费用配置。
|
||||
/// </summary>
|
||||
public DbSet<StoreFee> StoreFees => Set<StoreFee>();
|
||||
/// <summary>
|
||||
/// 门店资质证照。
|
||||
/// </summary>
|
||||
public DbSet<StoreQualification> StoreQualifications => Set<StoreQualification>();
|
||||
/// <summary>
|
||||
/// 门店审核记录。
|
||||
/// </summary>
|
||||
public DbSet<StoreAuditRecord> StoreAuditRecords => Set<StoreAuditRecord>();
|
||||
/// <summary>
|
||||
/// 门店营业时间。
|
||||
/// </summary>
|
||||
public DbSet<StoreBusinessHour> StoreBusinessHours => Set<StoreBusinessHour>();
|
||||
@@ -392,6 +406,9 @@ public sealed class TakeoutAppDbContext(
|
||||
ConfigureMerchantContract(modelBuilder.Entity<MerchantContract>());
|
||||
ConfigureMerchantStaff(modelBuilder.Entity<MerchantStaff>());
|
||||
ConfigureMerchantCategory(modelBuilder.Entity<MerchantCategory>());
|
||||
ConfigureStoreFee(modelBuilder.Entity<StoreFee>());
|
||||
ConfigureStoreQualification(modelBuilder.Entity<StoreQualification>());
|
||||
ConfigureStoreAuditRecord(modelBuilder.Entity<StoreAuditRecord>());
|
||||
ConfigureStoreBusinessHour(modelBuilder.Entity<StoreBusinessHour>());
|
||||
ConfigureStoreHoliday(modelBuilder.Entity<StoreHoliday>());
|
||||
ConfigureStoreDeliveryZone(modelBuilder.Entity<StoreDeliveryZone>());
|
||||
@@ -559,6 +576,14 @@ public sealed class TakeoutAppDbContext(
|
||||
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.SignboardImageUrl).HasMaxLength(500);
|
||||
builder.Property(x => x.OwnershipType).HasConversion<int>();
|
||||
builder.Property(x => x.AuditStatus).HasConversion<int>();
|
||||
builder.Property(x => x.BusinessStatus).HasConversion<int>();
|
||||
builder.Property(x => x.ClosureReason).HasConversion<int?>();
|
||||
builder.Property(x => x.ClosureReasonText).HasMaxLength(500);
|
||||
builder.Property(x => x.RejectionReason).HasMaxLength(500);
|
||||
builder.Property(x => x.ForceCloseReason).HasMaxLength(500);
|
||||
builder.Property(x => x.Province).HasMaxLength(64);
|
||||
builder.Property(x => x.City).HasMaxLength(64);
|
||||
builder.Property(x => x.District).HasMaxLength(64);
|
||||
@@ -568,11 +593,61 @@ 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.TenantId, x.AuditStatus });
|
||||
builder.HasIndex(x => new { x.TenantId, x.BusinessStatus });
|
||||
builder.HasIndex(x => new { x.TenantId, x.OwnershipType });
|
||||
builder.HasIndex(x => new { x.Longitude, x.Latitude })
|
||||
.HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL");
|
||||
builder.HasIndex(x => new { x.MerchantId, x.BusinessLicenseNumber })
|
||||
.IsUnique()
|
||||
.HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||
}
|
||||
|
||||
private static void ConfigureStoreFee(EntityTypeBuilder<StoreFee> builder)
|
||||
{
|
||||
builder.ToTable("store_fees");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.StoreId).IsRequired();
|
||||
builder.Property(x => x.MinimumOrderAmount).HasPrecision(10, 2);
|
||||
builder.Property(x => x.BaseDeliveryFee).HasPrecision(10, 2);
|
||||
builder.Property(x => x.PackagingFeeMode).HasConversion<int>();
|
||||
builder.Property(x => x.FixedPackagingFee).HasPrecision(10, 2);
|
||||
builder.Property(x => x.FreeDeliveryThreshold).HasPrecision(10, 2);
|
||||
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
}
|
||||
|
||||
private static void ConfigureStoreQualification(EntityTypeBuilder<StoreQualification> builder)
|
||||
{
|
||||
builder.ToTable("store_qualifications");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.StoreId).IsRequired();
|
||||
builder.Property(x => x.QualificationType).HasConversion<int>();
|
||||
builder.Property(x => x.FileUrl).HasMaxLength(500).IsRequired();
|
||||
builder.Property(x => x.DocumentNumber).HasMaxLength(100);
|
||||
builder.Property(x => x.SortOrder).HasDefaultValue(100);
|
||||
builder.HasIndex(x => new { x.TenantId, x.StoreId });
|
||||
builder.HasIndex(x => x.ExpiresAt)
|
||||
.HasFilter("\"ExpiresAt\" IS NOT NULL");
|
||||
builder.Ignore(x => x.IsExpired);
|
||||
builder.Ignore(x => x.IsExpiringSoon);
|
||||
}
|
||||
|
||||
private static void ConfigureStoreAuditRecord(EntityTypeBuilder<StoreAuditRecord> builder)
|
||||
{
|
||||
builder.ToTable("store_audit_records");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.StoreId).IsRequired();
|
||||
builder.Property(x => x.Action).HasConversion<int>();
|
||||
builder.Property(x => x.PreviousStatus).HasConversion<int?>();
|
||||
builder.Property(x => x.NewStatus).HasConversion<int>();
|
||||
builder.Property(x => x.OperatorName).HasMaxLength(100).IsRequired();
|
||||
builder.Property(x => x.RejectionReason).HasMaxLength(500);
|
||||
builder.Property(x => x.Remarks).HasMaxLength(1000);
|
||||
builder.HasIndex(x => new { x.TenantId, x.StoreId });
|
||||
builder.HasIndex(x => x.CreatedAt);
|
||||
}
|
||||
|
||||
private static void ConfigureProductCategory(EntityTypeBuilder<ProductCategory> builder)
|
||||
{
|
||||
builder.ToTable("product_categories");
|
||||
|
||||
@@ -36,17 +36,51 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<Store>> SearchAsync(
|
||||
long tenantId,
|
||||
long? merchantId,
|
||||
StoreStatus? status,
|
||||
StoreAuditStatus? auditStatus,
|
||||
StoreBusinessStatus? businessStatus,
|
||||
StoreOwnershipType? ownershipType,
|
||||
string? keyword,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.Stores
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (merchantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.MerchantId == merchantId.Value);
|
||||
}
|
||||
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == status.Value);
|
||||
}
|
||||
|
||||
if (auditStatus.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AuditStatus == auditStatus.Value);
|
||||
}
|
||||
|
||||
if (businessStatus.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.BusinessStatus == businessStatus.Value);
|
||||
}
|
||||
|
||||
if (ownershipType.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.OwnershipType == ownershipType.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var trimmed = keyword.Trim();
|
||||
query = query.Where(x => x.Name.Contains(trimmed) || x.Code.Contains(trimmed));
|
||||
}
|
||||
|
||||
var stores = await query
|
||||
.OrderBy(x => x.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -54,6 +88,43 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
||||
return stores;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ExistsStoreWithinDistanceAsync(
|
||||
long merchantId,
|
||||
long tenantId,
|
||||
double longitude,
|
||||
double latitude,
|
||||
double distanceMeters,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 校验距离阈值
|
||||
if (distanceMeters <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. (空行后) 拉取候选坐标
|
||||
var coordinates = await context.Stores
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||
.Where(x => x.Longitude.HasValue && x.Latitude.HasValue)
|
||||
.Select(x => new { Longitude = x.Longitude!.Value, Latitude = x.Latitude!.Value })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// 3. (空行后) 计算距离并判断是否命中
|
||||
foreach (var coordinate in coordinates)
|
||||
{
|
||||
var distance = CalculateDistanceMeters(latitude, longitude, coordinate.Latitude, coordinate.Longitude);
|
||||
if (distance <= distanceMeters)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. (空行后) 返回未命中结果
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<long, int>> GetStoreCountsAsync(long? tenantId, IReadOnlyCollection<long> merchantIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -92,6 +163,91 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
||||
return hours;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreFee?> GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.StoreFees
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddStoreFeeAsync(StoreFee storeFee, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.StoreFees.AddAsync(storeFee, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateStoreFeeAsync(StoreFee storeFee, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.StoreFees.Update(storeFee);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreQualification>> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var qualifications = await context.StoreQualifications
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.QualificationType)
|
||||
.ToListAsync(cancellationToken);
|
||||
return qualifications;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreQualification?> FindQualificationByIdAsync(long qualificationId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.StoreQualifications
|
||||
.Where(x => x.TenantId == tenantId && x.Id == qualificationId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddQualificationAsync(StoreQualification qualification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.StoreQualifications.AddAsync(qualification, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateQualificationAsync(StoreQualification qualification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.StoreQualifications.Update(qualification);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteQualificationAsync(long qualificationId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await context.StoreQualifications
|
||||
.Where(x => x.TenantId == tenantId && x.Id == qualificationId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
context.StoreQualifications.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddAuditRecordAsync(StoreAuditRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.StoreAuditRecords.AddAsync(record, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreAuditRecord>> GetAuditRecordsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var records = await context.StoreAuditRecords
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreBusinessHour?> FindBusinessHourByIdAsync(long businessHourId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -476,4 +632,20 @@ public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepos
|
||||
|
||||
context.Stores.Remove(existing);
|
||||
}
|
||||
|
||||
private static double CalculateDistanceMeters(double latitude1, double longitude1, double latitude2, double longitude2)
|
||||
{
|
||||
const double earthRadius = 6371000d;
|
||||
var latRad1 = DegreesToRadians(latitude1);
|
||||
var latRad2 = DegreesToRadians(latitude2);
|
||||
var deltaLat = DegreesToRadians(latitude2 - latitude1);
|
||||
var deltaLon = DegreesToRadians(longitude2 - longitude1);
|
||||
var sinLat = Math.Sin(deltaLat / 2);
|
||||
var sinLon = Math.Sin(deltaLon / 2);
|
||||
var a = sinLat * sinLat + Math.Cos(latRad1) * Math.Cos(latRad2) * sinLon * sinLon;
|
||||
var c = 2 * Math.Asin(Math.Min(1, Math.Sqrt(a)));
|
||||
return earthRadius * c;
|
||||
}
|
||||
|
||||
private static double DegreesToRadians(double degrees) => degrees * (Math.PI / 180d);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 配送范围检测服务实现。
|
||||
/// </summary>
|
||||
public sealed class DeliveryZoneService : IDeliveryZoneService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public StoreDeliveryCheckResultDto CheckPointInZones(
|
||||
IReadOnlyList<StoreDeliveryZone> zones,
|
||||
double longitude,
|
||||
double latitude)
|
||||
{
|
||||
// 1. 无配送区域直接返回
|
||||
if (zones is null || zones.Count == 0)
|
||||
{
|
||||
return new StoreDeliveryCheckResultDto { InRange = false };
|
||||
}
|
||||
// 2. (空行后) 逐个检测多边形命中
|
||||
foreach (var zone in zones)
|
||||
{
|
||||
if (!TryReadPolygon(zone.PolygonGeoJson, out var polygon))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (IsPointInPolygon(polygon, longitude, latitude))
|
||||
{
|
||||
return new StoreDeliveryCheckResultDto
|
||||
{
|
||||
InRange = true,
|
||||
DeliveryZoneId = zone.Id,
|
||||
DeliveryZoneName = zone.ZoneName
|
||||
};
|
||||
}
|
||||
}
|
||||
// 3. (空行后) 未命中任何区域
|
||||
return new StoreDeliveryCheckResultDto { InRange = false };
|
||||
}
|
||||
|
||||
private static bool TryReadPolygon(string geoJson, out List<Point> polygon)
|
||||
{
|
||||
polygon = [];
|
||||
if (string.IsNullOrWhiteSpace(geoJson))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(geoJson);
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!root.TryGetProperty("coordinates", out var coordinatesElement) || coordinatesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (coordinatesElement.GetArrayLength() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var ringElement = coordinatesElement[0];
|
||||
if (ringElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (var pointElement in ringElement.EnumerateArray())
|
||||
{
|
||||
if (pointElement.ValueKind != JsonValueKind.Array || pointElement.GetArrayLength() < 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!pointElement[0].TryGetDouble(out var x) || !pointElement[1].TryGetDouble(out var y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
polygon.Add(new Point(x, y));
|
||||
}
|
||||
if (polygon.Count >= 2 && AreSamePoint(polygon[0], polygon[^1]))
|
||||
{
|
||||
polygon.RemoveAt(polygon.Count - 1);
|
||||
}
|
||||
return polygon.Count >= 3;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPointInPolygon(IReadOnlyList<Point> polygon, double x, double y)
|
||||
{
|
||||
var inside = false;
|
||||
for (var i = 0; i < polygon.Count; i++)
|
||||
{
|
||||
var j = i == 0 ? polygon.Count - 1 : i - 1;
|
||||
var xi = polygon[i].Longitude;
|
||||
var yi = polygon[i].Latitude;
|
||||
var xj = polygon[j].Longitude;
|
||||
var yj = polygon[j].Latitude;
|
||||
var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi + double.Epsilon) + xi);
|
||||
if (intersect)
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
private static bool AreSamePoint(Point first, Point second)
|
||||
=> Math.Abs(first.Longitude - second.Longitude) <= 1e-6
|
||||
&& Math.Abs(first.Latitude - second.Latitude) <= 1e-6;
|
||||
|
||||
private readonly record struct Point(double Longitude, double Latitude);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// GeoJSON 校验服务实现。
|
||||
/// </summary>
|
||||
public sealed class GeoJsonValidationService : IGeoJsonValidationService
|
||||
{
|
||||
private const double CoordinateTolerance = 1e-6;
|
||||
|
||||
/// <inheritdoc />
|
||||
public GeoJsonValidationResult ValidatePolygon(string geoJson)
|
||||
{
|
||||
// 1. 基础校验
|
||||
if (string.IsNullOrWhiteSpace(geoJson))
|
||||
{
|
||||
return BuildInvalid("GeoJSON 不能为空");
|
||||
}
|
||||
// 2. (空行后) 解析与验证结构
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(geoJson);
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return BuildInvalid("GeoJSON 格式错误");
|
||||
}
|
||||
if (!root.TryGetProperty("type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
return BuildInvalid("GeoJSON 缺少 type");
|
||||
}
|
||||
var type = typeElement.GetString();
|
||||
if (!string.Equals(type, "Polygon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BuildInvalid("仅支持 Polygon 类型");
|
||||
}
|
||||
if (!root.TryGetProperty("coordinates", out var coordinatesElement) || coordinatesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return BuildInvalid("GeoJSON 缺少 coordinates");
|
||||
}
|
||||
if (coordinatesElement.GetArrayLength() == 0)
|
||||
{
|
||||
return BuildInvalid("GeoJSON coordinates 为空");
|
||||
}
|
||||
var ringElement = coordinatesElement[0];
|
||||
if (ringElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return BuildInvalid("GeoJSON 坐标格式错误");
|
||||
}
|
||||
var points = new List<Point>();
|
||||
foreach (var pointElement in ringElement.EnumerateArray())
|
||||
{
|
||||
if (pointElement.ValueKind != JsonValueKind.Array || pointElement.GetArrayLength() < 2)
|
||||
{
|
||||
return BuildInvalid("坐标点格式错误");
|
||||
}
|
||||
if (!pointElement[0].TryGetDouble(out var longitude) || !pointElement[1].TryGetDouble(out var latitude))
|
||||
{
|
||||
return BuildInvalid("坐标点必须为数值");
|
||||
}
|
||||
points.Add(new Point(longitude, latitude));
|
||||
}
|
||||
if (points.Count < 3)
|
||||
{
|
||||
return BuildInvalid("多边形至少需要 3 个点");
|
||||
}
|
||||
var distinctCount = CountDistinct(points);
|
||||
if (distinctCount < 3)
|
||||
{
|
||||
return BuildInvalid("多边形坐标点不足");
|
||||
}
|
||||
var normalized = Normalize(points, out var normalizedJson);
|
||||
if (normalized.Count < 4)
|
||||
{
|
||||
return BuildInvalid("多边形至少需要 4 个点(含闭合点)");
|
||||
}
|
||||
if (HasSelfIntersection(normalized))
|
||||
{
|
||||
return BuildInvalid("多边形存在自相交");
|
||||
}
|
||||
return new GeoJsonValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
NormalizedGeoJson = normalizedJson
|
||||
};
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return BuildInvalid("GeoJSON 解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static GeoJsonValidationResult BuildInvalid(string message) => new()
|
||||
{
|
||||
IsValid = false,
|
||||
ErrorMessage = message
|
||||
};
|
||||
|
||||
private static int CountDistinct(IReadOnlyList<Point> points)
|
||||
{
|
||||
var distinct = new List<Point>();
|
||||
foreach (var point in points)
|
||||
{
|
||||
if (distinct.Any(existing => AreSamePoint(existing, point)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
distinct.Add(point);
|
||||
}
|
||||
return distinct.Count;
|
||||
}
|
||||
|
||||
private static List<Point> Normalize(IReadOnlyList<Point> points, out string? normalizedJson)
|
||||
{
|
||||
var normalized = new List<Point>(points);
|
||||
if (!AreSamePoint(normalized[0], normalized[^1]))
|
||||
{
|
||||
normalized.Add(normalized[0]);
|
||||
normalizedJson = BuildGeoJson(normalized);
|
||||
return normalized;
|
||||
}
|
||||
normalizedJson = null;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string BuildGeoJson(IReadOnlyList<Point> points)
|
||||
{
|
||||
var coordinates = points
|
||||
.Select(point => new[] { point.Longitude, point.Latitude })
|
||||
.ToArray();
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
{ "type", "Polygon" },
|
||||
{ "coordinates", new[] { coordinates } }
|
||||
};
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
|
||||
private static bool HasSelfIntersection(IReadOnlyList<Point> points)
|
||||
{
|
||||
var segmentCount = points.Count - 1;
|
||||
for (var i = 0; i < segmentCount; i++)
|
||||
{
|
||||
var a1 = points[i];
|
||||
var a2 = points[i + 1];
|
||||
for (var j = i + 1; j < segmentCount; j++)
|
||||
{
|
||||
if (Math.Abs(i - j) <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (i == 0 && j == segmentCount - 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var b1 = points[j];
|
||||
var b2 = points[j + 1];
|
||||
if (SegmentsIntersect(a1, a2, b1, b2))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SegmentsIntersect(Point p1, Point q1, Point p2, Point q2)
|
||||
{
|
||||
var o1 = Orientation(p1, q1, p2);
|
||||
var o2 = Orientation(p1, q1, q2);
|
||||
var o3 = Orientation(p2, q2, p1);
|
||||
var o4 = Orientation(p2, q2, q1);
|
||||
|
||||
if (o1 != o2 && o3 != o4)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (o1 == 0 && OnSegment(p1, p2, q1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (o2 == 0 && OnSegment(p1, q2, q1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (o3 == 0 && OnSegment(p2, p1, q2))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (o4 == 0 && OnSegment(p2, q1, q2))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int Orientation(Point p, Point q, Point r)
|
||||
{
|
||||
var value = (q.Latitude - p.Latitude) * (r.Longitude - q.Longitude)
|
||||
- (q.Longitude - p.Longitude) * (r.Latitude - q.Latitude);
|
||||
if (Math.Abs(value) <= CoordinateTolerance)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return value > 0 ? 1 : 2;
|
||||
}
|
||||
|
||||
private static bool OnSegment(Point p, Point q, Point r)
|
||||
=> q.Longitude <= Math.Max(p.Longitude, r.Longitude) + CoordinateTolerance
|
||||
&& q.Longitude >= Math.Min(p.Longitude, r.Longitude) - CoordinateTolerance
|
||||
&& q.Latitude <= Math.Max(p.Latitude, r.Latitude) + CoordinateTolerance
|
||||
&& q.Latitude >= Math.Min(p.Latitude, r.Latitude) - CoordinateTolerance;
|
||||
|
||||
private static bool AreSamePoint(Point first, Point second)
|
||||
=> Math.Abs(first.Longitude - second.Longitude) <= CoordinateTolerance
|
||||
&& Math.Abs(first.Latitude - second.Latitude) <= CoordinateTolerance;
|
||||
|
||||
private readonly record struct Point(double Longitude, double Latitude);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Globalization;
|
||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 门店费用计算服务实现。
|
||||
/// </summary>
|
||||
public sealed class StoreFeeCalculationService : IStoreFeeCalculationService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public StoreFeeCalculationResultDto Calculate(StoreFee fee, StoreFeeCalculationRequestDto request)
|
||||
{
|
||||
// 1. 计算起送费满足情况
|
||||
var minimum = fee.MinimumOrderAmount;
|
||||
if (request.OrderAmount < minimum)
|
||||
{
|
||||
var shortfall = minimum - request.OrderAmount;
|
||||
var message = $"还差{shortfall.ToString("0.##", CultureInfo.InvariantCulture)}元起送";
|
||||
return new StoreFeeCalculationResultDto
|
||||
{
|
||||
OrderAmount = request.OrderAmount,
|
||||
MinimumOrderAmount = minimum,
|
||||
MeetsMinimum = false,
|
||||
Shortfall = shortfall,
|
||||
DeliveryFee = 0m,
|
||||
PackagingFee = 0m,
|
||||
PackagingFeeMode = fee.PackagingFeeMode,
|
||||
TotalFee = 0m,
|
||||
TotalAmount = request.OrderAmount,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
// 2. (空行后) 计算配送费
|
||||
var deliveryFee = fee.BaseDeliveryFee;
|
||||
if (fee.FreeDeliveryThreshold.HasValue && request.OrderAmount >= fee.FreeDeliveryThreshold.Value)
|
||||
{
|
||||
deliveryFee = 0m;
|
||||
}
|
||||
|
||||
// 3. (空行后) 计算打包费
|
||||
var packagingFee = 0m;
|
||||
IReadOnlyList<StoreFeeCalculationBreakdownDto>? breakdown = null;
|
||||
if (fee.PackagingFeeMode == PackagingFeeMode.Fixed)
|
||||
{
|
||||
packagingFee = fee.FixedPackagingFee;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (request.Items.Count == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "商品计费模式必须提供商品列表");
|
||||
}
|
||||
var list = new List<StoreFeeCalculationBreakdownDto>(request.Items.Count);
|
||||
foreach (var item in request.Items)
|
||||
{
|
||||
var subtotal = item.PackagingFee * item.Quantity;
|
||||
packagingFee += subtotal;
|
||||
list.Add(new StoreFeeCalculationBreakdownDto
|
||||
{
|
||||
SkuId = item.SkuId,
|
||||
Quantity = item.Quantity,
|
||||
UnitFee = item.PackagingFee,
|
||||
Subtotal = subtotal
|
||||
});
|
||||
}
|
||||
breakdown = list;
|
||||
}
|
||||
|
||||
// 4. (空行后) 汇总结果
|
||||
var totalFee = deliveryFee + packagingFee;
|
||||
var totalAmount = request.OrderAmount + totalFee;
|
||||
return new StoreFeeCalculationResultDto
|
||||
{
|
||||
OrderAmount = request.OrderAmount,
|
||||
MinimumOrderAmount = minimum,
|
||||
MeetsMinimum = true,
|
||||
DeliveryFee = deliveryFee,
|
||||
PackagingFee = packagingFee,
|
||||
PackagingFeeMode = fee.PackagingFeeMode,
|
||||
PackagingFeeBreakdown = breakdown,
|
||||
TotalFee = totalFee,
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TakeoutSaaS.Application.App.Stores.Services;
|
||||
using TakeoutSaaS.Domain.Stores.Entities;
|
||||
using TakeoutSaaS.Domain.Stores.Enums;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 门店定时任务服务实现。
|
||||
/// </summary>
|
||||
public sealed class StoreSchedulerService(
|
||||
TakeoutAppDbContext context,
|
||||
ILogger<StoreSchedulerService> logger)
|
||||
: IStoreSchedulerService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<int> AutoSwitchBusinessStatusAsync(DateTime now, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取候选门店
|
||||
var stores = await context.Stores
|
||||
.Where(store => store.DeletedAt == null
|
||||
&& store.AuditStatus == StoreAuditStatus.Activated
|
||||
&& store.BusinessStatus != StoreBusinessStatus.ForceClosed)
|
||||
.ToListAsync(cancellationToken);
|
||||
if (stores.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. (空行后) 读取营业时段与休息日
|
||||
var storeIds = stores.Select(store => store.Id).ToArray();
|
||||
var hours = await context.StoreBusinessHours
|
||||
.AsNoTracking()
|
||||
.Where(hour => storeIds.Contains(hour.StoreId))
|
||||
.ToListAsync(cancellationToken);
|
||||
var today = now.Date;
|
||||
var holidays = await context.StoreHolidays
|
||||
.AsNoTracking()
|
||||
.Where(holiday => storeIds.Contains(holiday.StoreId)
|
||||
&& holiday.IsClosed
|
||||
&& holiday.Date.Date == today)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// 3. (空行后) 构造查找表
|
||||
var hoursLookup = hours
|
||||
.GroupBy(hour => hour.StoreId)
|
||||
.ToDictionary(group => group.Key, group => (IReadOnlyList<StoreBusinessHour>)group.ToList());
|
||||
var holidaySet = holidays.Select(holiday => holiday.StoreId).ToHashSet();
|
||||
|
||||
// 4. (空行后) 判定状态并更新
|
||||
var updated = 0;
|
||||
foreach (var store in stores)
|
||||
{
|
||||
// 4.1 跳过强制关闭门店
|
||||
if (store.BusinessStatus == StoreBusinessStatus.ForceClosed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.2 (空行后) 尊重手动歇业原因
|
||||
if (store.ClosureReason.HasValue && store.ClosureReason != StoreClosureReason.OutOfBusinessHours)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.3 (空行后) 计算营业状态
|
||||
var isHolidayClosed = holidaySet.Contains(store.Id);
|
||||
var hasHours = hoursLookup.TryGetValue(store.Id, out var storeHours) && storeHours.Count > 0;
|
||||
var isOpen = !isHolidayClosed && hasHours && IsWithinBusinessHours(storeHours ?? [], now);
|
||||
if (isOpen)
|
||||
{
|
||||
if (store.BusinessStatus != StoreBusinessStatus.Open)
|
||||
{
|
||||
store.BusinessStatus = StoreBusinessStatus.Open;
|
||||
store.ClosureReason = null;
|
||||
store.ClosureReasonText = null;
|
||||
updated++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.4 (空行后) 非营业时段切换为休息
|
||||
if (store.BusinessStatus != StoreBusinessStatus.Resting || store.ClosureReason != StoreClosureReason.OutOfBusinessHours)
|
||||
{
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.ClosureReason = StoreClosureReason.OutOfBusinessHours;
|
||||
store.ClosureReasonText = "非营业时间自动休息";
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. (空行后) 保存变更并记录日志
|
||||
if (updated > 0)
|
||||
{
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("定时任务:营业状态自动切换完成,更新 {UpdatedCount} 家门店", updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CheckQualificationExpiryAsync(DateTime now, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 查询过期门店
|
||||
var expiredStoreIds = await context.StoreQualifications
|
||||
.AsNoTracking()
|
||||
.Where(qualification => qualification.DeletedAt == null
|
||||
&& qualification.ExpiresAt.HasValue
|
||||
&& qualification.ExpiresAt.Value < now)
|
||||
.Select(qualification => qualification.StoreId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
if (expiredStoreIds.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. (空行后) 加载门店并更新状态
|
||||
var stores = await context.Stores
|
||||
.Where(store => expiredStoreIds.Contains(store.Id)
|
||||
&& store.DeletedAt == null
|
||||
&& store.AuditStatus == StoreAuditStatus.Activated
|
||||
&& store.BusinessStatus != StoreBusinessStatus.ForceClosed)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var updated = 0;
|
||||
foreach (var store in stores)
|
||||
{
|
||||
// 2.1 跳过已标记过期门店
|
||||
if (store.BusinessStatus == StoreBusinessStatus.Resting && store.ClosureReason == StoreClosureReason.LicenseExpired)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2.2 (空行后) 设置资质过期状态
|
||||
store.BusinessStatus = StoreBusinessStatus.Resting;
|
||||
store.ClosureReason = StoreClosureReason.LicenseExpired;
|
||||
store.ClosureReasonText = "证照过期自动休息";
|
||||
updated++;
|
||||
}
|
||||
|
||||
// 3. (空行后) 保存变更并记录日志
|
||||
if (updated > 0)
|
||||
{
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("定时任务:资质过期检查完成,更新 {UpdatedCount} 家门店", updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private static bool IsWithinBusinessHours(IReadOnlyList<StoreBusinessHour> hours, DateTime now)
|
||||
{
|
||||
// 1. 提取当前时间
|
||||
var day = now.DayOfWeek;
|
||||
var time = now.TimeOfDay;
|
||||
|
||||
foreach (var hour in hours)
|
||||
{
|
||||
if (hour.HourType == BusinessHourType.Closed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (hour.StartTime == hour.EndTime)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (hour.StartTime < hour.EndTime)
|
||||
{
|
||||
if (hour.DayOfWeek == day && time >= hour.StartTime && time < hour.EndTime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var nextDay = NextDay(hour.DayOfWeek);
|
||||
if (hour.DayOfWeek == day && time >= hour.StartTime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (nextDay == day && time < hour.EndTime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static DayOfWeek NextDay(DayOfWeek day)
|
||||
{
|
||||
var next = (int)day + 1;
|
||||
return next > 6 ? DayOfWeek.Sunday : (DayOfWeek)next;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddStoreManagementEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ActivatedAt",
|
||||
table: "stores",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
comment: "审核通过时间。");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AuditStatus",
|
||||
table: "stores",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
comment: "审核状态。");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "BusinessStatus",
|
||||
table: "stores",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
comment: "经营状态。");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "CategoryId",
|
||||
table: "stores",
|
||||
type: "bigint",
|
||||
nullable: true,
|
||||
comment: "行业类目 ID。");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ClosureReason",
|
||||
table: "stores",
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
comment: "歇业原因。");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClosureReasonText",
|
||||
table: "stores",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
comment: "歇业原因补充说明。");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ForceCloseReason",
|
||||
table: "stores",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
comment: "强制关闭原因。");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ForceClosedAt",
|
||||
table: "stores",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
comment: "强制关闭时间。");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "OwnershipType",
|
||||
table: "stores",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
comment: "主体类型。");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "RejectionReason",
|
||||
table: "stores",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
comment: "审核驳回原因。");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignboardImageUrl",
|
||||
table: "stores",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true,
|
||||
comment: "门头招牌图 URL。");
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "SubmittedAt",
|
||||
table: "stores",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true,
|
||||
comment: "提交审核时间。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "store_audit_records",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店标识。"),
|
||||
Action = table.Column<int>(type: "integer", nullable: false, comment: "操作类型。"),
|
||||
PreviousStatus = table.Column<int>(type: "integer", nullable: true, comment: "操作前状态。"),
|
||||
NewStatus = table.Column<int>(type: "integer", nullable: false, comment: "操作后状态。"),
|
||||
OperatorId = table.Column<long>(type: "bigint", nullable: true, comment: "操作人 ID。"),
|
||||
OperatorName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false, comment: "操作人名称。"),
|
||||
RejectionReasonId = table.Column<long>(type: "bigint", nullable: true, comment: "驳回理由 ID。"),
|
||||
RejectionReason = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true, comment: "驳回理由文本。"),
|
||||
Remarks = table.Column<string>(type: "character varying(1000)", maxLength: 1000, 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_store_audit_records", x => x.Id);
|
||||
},
|
||||
comment: "门店审核记录。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "store_fees",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店标识。"),
|
||||
MinimumOrderAmount = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false, comment: "起送费(元)。"),
|
||||
BaseDeliveryFee = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false, comment: "基础配送费(元)。"),
|
||||
PackagingFeeMode = table.Column<int>(type: "integer", nullable: false, comment: "打包费模式。"),
|
||||
FixedPackagingFee = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false, comment: "固定打包费(总计模式有效)。"),
|
||||
FreeDeliveryThreshold = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, 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_store_fees", x => x.Id);
|
||||
},
|
||||
comment: "门店费用配置。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "store_qualifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店标识。"),
|
||||
QualificationType = table.Column<int>(type: "integer", nullable: false, comment: "资质类型。"),
|
||||
FileUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "证照文件 URL。"),
|
||||
DocumentNumber = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true, comment: "证照编号。"),
|
||||
IssuedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "签发日期。"),
|
||||
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "到期日期。"),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false, defaultValue: 100, 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_store_qualifications", x => x.Id);
|
||||
},
|
||||
comment: "门店资质证照。");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stores_Longitude_Latitude",
|
||||
table: "stores",
|
||||
columns: new[] { "Longitude", "Latitude" },
|
||||
filter: "\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stores_TenantId_AuditStatus",
|
||||
table: "stores",
|
||||
columns: new[] { "TenantId", "AuditStatus" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stores_TenantId_BusinessStatus",
|
||||
table: "stores",
|
||||
columns: new[] { "TenantId", "BusinessStatus" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_stores_TenantId_OwnershipType",
|
||||
table: "stores",
|
||||
columns: new[] { "TenantId", "OwnershipType" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_audit_records_CreatedAt",
|
||||
table: "store_audit_records",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_audit_records_TenantId_StoreId",
|
||||
table: "store_audit_records",
|
||||
columns: new[] { "TenantId", "StoreId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_fees_TenantId",
|
||||
table: "store_fees",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_fees_TenantId_StoreId",
|
||||
table: "store_fees",
|
||||
columns: new[] { "TenantId", "StoreId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_qualifications_ExpiresAt",
|
||||
table: "store_qualifications",
|
||||
column: "ExpiresAt",
|
||||
filter: "\"ExpiresAt\" IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_store_qualifications_TenantId_StoreId",
|
||||
table: "store_qualifications",
|
||||
columns: new[] { "TenantId", "StoreId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "store_audit_records");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "store_fees");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "store_qualifications");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_stores_Longitude_Latitude",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_stores_TenantId_AuditStatus",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_stores_TenantId_BusinessStatus",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_stores_TenantId_OwnershipType",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ActivatedAt",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AuditStatus",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BusinessStatus",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CategoryId",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClosureReason",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClosureReasonText",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ForceCloseReason",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ForceClosedAt",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OwnershipType",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RejectionReason",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignboardImageUrl",
|
||||
table: "stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SubmittedAt",
|
||||
table: "stores");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4828,6 +4828,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime?>("ActivatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("审核通过时间。");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
@@ -4838,6 +4842,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasComment("门店公告。");
|
||||
|
||||
b.Property<int>("AuditStatus")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("审核状态。");
|
||||
|
||||
b.Property<string>("BusinessHours")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
@@ -4853,11 +4861,28 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasComment("门店营业执照号(主体不一致模式使用)。");
|
||||
|
||||
b.Property<int>("BusinessStatus")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("经营状态。");
|
||||
|
||||
b.Property<long?>("CategoryId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("行业类目 ID。");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("所在城市。");
|
||||
|
||||
b.Property<int?>("ClosureReason")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("歇业原因。");
|
||||
|
||||
b.Property<string>("ClosureReasonText")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("歇业原因补充说明。");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
@@ -4902,6 +4927,15 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("区县信息。");
|
||||
|
||||
b.Property<string>("ForceCloseReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("强制关闭原因。");
|
||||
|
||||
b.Property<DateTime?>("ForceClosedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("强制关闭时间。");
|
||||
|
||||
b.Property<double?>("Latitude")
|
||||
.HasColumnType("double precision")
|
||||
.HasComment("纬度。");
|
||||
@@ -4930,6 +4964,10 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasComment("门店名称。");
|
||||
|
||||
b.Property<int>("OwnershipType")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("主体类型。");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
@@ -4945,10 +4983,24 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("门店注册地址(主体不一致模式使用)。");
|
||||
|
||||
b.Property<string>("RejectionReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("审核驳回原因。");
|
||||
|
||||
b.Property<string>("SignboardImageUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("门头招牌图 URL。");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("门店当前运营状态。");
|
||||
|
||||
b.Property<DateTime?>("SubmittedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("提交审核时间。");
|
||||
|
||||
b.Property<bool>("SupportsDelivery")
|
||||
.HasColumnType("boolean")
|
||||
.HasComment("是否支持配送。");
|
||||
@@ -4987,21 +5039,119 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Longitude", "Latitude")
|
||||
.HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL");
|
||||
|
||||
b.HasIndex("MerchantId", "BusinessLicenseNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3");
|
||||
|
||||
b.HasIndex("TenantId", "AuditStatus");
|
||||
|
||||
b.HasIndex("TenantId", "BusinessStatus");
|
||||
|
||||
b.HasIndex("TenantId", "Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("TenantId", "MerchantId");
|
||||
|
||||
b.HasIndex("TenantId", "OwnershipType");
|
||||
|
||||
b.ToTable("stores", null, t =>
|
||||
{
|
||||
t.HasComment("门店信息,承载营业配置与能力。");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreAuditRecord", 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<int>("NewStatus")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("操作后状态。");
|
||||
|
||||
b.Property<long?>("OperatorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("操作人 ID。");
|
||||
|
||||
b.Property<string>("OperatorName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasComment("操作人名称。");
|
||||
|
||||
b.Property<int?>("PreviousStatus")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("操作前状态。");
|
||||
|
||||
b.Property<string>("RejectionReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("驳回理由文本。");
|
||||
|
||||
b.Property<long?>("RejectionReasonId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("驳回理由 ID。");
|
||||
|
||||
b.Property<string>("Remarks")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasComment("备注。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.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("CreatedAt");
|
||||
|
||||
b.HasIndex("TenantId", "StoreId");
|
||||
|
||||
b.ToTable("store_audit_records", null, t =>
|
||||
{
|
||||
t.HasComment("门店审核记录。");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -5235,6 +5385,84 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreFee", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("实体唯一标识。");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("BaseDeliveryFee")
|
||||
.HasPrecision(10, 2)
|
||||
.HasColumnType("numeric(10,2)")
|
||||
.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<decimal>("FixedPackagingFee")
|
||||
.HasPrecision(10, 2)
|
||||
.HasColumnType("numeric(10,2)")
|
||||
.HasComment("固定打包费(总计模式有效)。");
|
||||
|
||||
b.Property<decimal?>("FreeDeliveryThreshold")
|
||||
.HasPrecision(10, 2)
|
||||
.HasColumnType("numeric(10,2)")
|
||||
.HasComment("免配送费门槛。");
|
||||
|
||||
b.Property<decimal>("MinimumOrderAmount")
|
||||
.HasPrecision(10, 2)
|
||||
.HasColumnType("numeric(10,2)")
|
||||
.HasComment("起送费(元)。");
|
||||
|
||||
b.Property<int>("PackagingFeeMode")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("打包费模式。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.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");
|
||||
|
||||
b.HasIndex("TenantId", "StoreId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("store_fees", null, t =>
|
||||
{
|
||||
t.HasComment("门店费用配置。");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -5473,6 +5701,89 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreQualification", 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>("DocumentNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasComment("证照编号。");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("到期日期。");
|
||||
|
||||
b.Property<string>("FileUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasComment("证照文件 URL。");
|
||||
|
||||
b.Property<DateTime?>("IssuedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("签发日期。");
|
||||
|
||||
b.Property<int>("QualificationType")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("资质类型。");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(100)
|
||||
.HasComment("排序值。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.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("ExpiresAt")
|
||||
.HasFilter("\"ExpiresAt\" IS NOT NULL");
|
||||
|
||||
b.HasIndex("TenantId", "StoreId");
|
||||
|
||||
b.ToTable("store_qualifications", null, t =>
|
||||
{
|
||||
t.HasComment("门店资质证照。");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
Reference in New Issue
Block a user