feat(member): implement points mall backend module

This commit is contained in:
2026-03-04 12:15:18 +08:00
parent 2970134200
commit bd418c5927
53 changed files with 5193 additions and 0 deletions

View File

@@ -402,6 +402,18 @@ public sealed class TakeoutAppDbContext(
/// </summary>
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
/// <summary>
/// 积分商城规则。
/// </summary>
public DbSet<MemberPointMallRule> MemberPointMallRules => Set<MemberPointMallRule>();
/// <summary>
/// 积分商城兑换商品。
/// </summary>
public DbSet<MemberPointMallProduct> MemberPointMallProducts => Set<MemberPointMallProduct>();
/// <summary>
/// 积分商城兑换记录。
/// </summary>
public DbSet<MemberPointMallRecord> MemberPointMallRecords => Set<MemberPointMallRecord>();
/// <summary>
/// 会员储值方案。
/// </summary>
public DbSet<MemberStoredCardPlan> MemberStoredCardPlans => Set<MemberStoredCardPlan>();
@@ -576,6 +588,9 @@ public sealed class TakeoutAppDbContext(
ConfigureMemberProfileTag(modelBuilder.Entity<MemberProfileTag>());
ConfigureMemberDaySetting(modelBuilder.Entity<MemberDaySetting>());
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
ConfigureMemberPointMallRule(modelBuilder.Entity<MemberPointMallRule>());
ConfigureMemberPointMallProduct(modelBuilder.Entity<MemberPointMallProduct>());
ConfigureMemberPointMallRecord(modelBuilder.Entity<MemberPointMallRecord>());
ConfigureMemberStoredCardPlan(modelBuilder.Entity<MemberStoredCardPlan>());
ConfigureMemberStoredCardRechargeRecord(modelBuilder.Entity<MemberStoredCardRechargeRecord>());
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
@@ -1856,6 +1871,80 @@ public sealed class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt });
}
private static void ConfigureMemberPointMallRule(EntityTypeBuilder<MemberPointMallRule> builder)
{
builder.ToTable("member_point_mall_rules");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.IsConsumeRewardEnabled).IsRequired();
builder.Property(x => x.ConsumeAmountPerStep).IsRequired();
builder.Property(x => x.ConsumeRewardPointsPerStep).IsRequired();
builder.Property(x => x.IsReviewRewardEnabled).IsRequired();
builder.Property(x => x.ReviewRewardPoints).IsRequired();
builder.Property(x => x.IsRegisterRewardEnabled).IsRequired();
builder.Property(x => x.RegisterRewardPoints).IsRequired();
builder.Property(x => x.IsSigninRewardEnabled).IsRequired();
builder.Property(x => x.SigninRewardPoints).IsRequired();
builder.Property(x => x.ExpiryMode).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
}
private static void ConfigureMemberPointMallProduct(EntityTypeBuilder<MemberPointMallProduct> builder)
{
builder.ToTable("member_point_mall_products");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.ImageUrl).HasMaxLength(512);
builder.Property(x => x.RedeemType).HasConversion<int>();
builder.Property(x => x.ProductId);
builder.Property(x => x.CouponTemplateId);
builder.Property(x => x.PhysicalName).HasMaxLength(64);
builder.Property(x => x.PickupMethod).HasConversion<int?>();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.ExchangeType).HasConversion<int>();
builder.Property(x => x.RequiredPoints).IsRequired();
builder.Property(x => x.CashAmount).HasPrecision(18, 2);
builder.Property(x => x.StockTotal).IsRequired();
builder.Property(x => x.StockAvailable).IsRequired();
builder.Property(x => x.PerMemberLimit);
builder.Property(x => x.NotifyChannelsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ProductId });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.CouponTemplateId });
}
private static void ConfigureMemberPointMallRecord(EntityTypeBuilder<MemberPointMallRecord> builder)
{
builder.ToTable("member_point_mall_records");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.RecordNo).HasMaxLength(32).IsRequired();
builder.Property(x => x.PointMallProductId).IsRequired();
builder.Property(x => x.MemberId).IsRequired();
builder.Property(x => x.MemberName).HasMaxLength(64).IsRequired();
builder.Property(x => x.MemberMobileMasked).HasMaxLength(32).IsRequired();
builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired();
builder.Property(x => x.RedeemType).HasConversion<int>();
builder.Property(x => x.ExchangeType).HasConversion<int>();
builder.Property(x => x.UsedPoints).IsRequired();
builder.Property(x => x.CashAmount).HasPrecision(18, 2);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.RedeemedAt).IsRequired();
builder.Property(x => x.IssuedAt);
builder.Property(x => x.VerifiedAt);
builder.Property(x => x.VerifyMethod).HasConversion<int?>();
builder.Property(x => x.VerifyRemark).HasMaxLength(256);
builder.Property(x => x.VerifiedBy);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RecordNo }).IsUnique();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PointMallProductId, x.RedeemedAt });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.MemberId, x.RedeemedAt });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status, x.RedeemedAt });
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RedeemedAt });
}
private static void ConfigureMemberStoredCardPlan(EntityTypeBuilder<MemberStoredCardPlan> builder)
{
builder.ToTable("member_stored_card_plans");
@@ -2102,3 +2191,4 @@ public sealed class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.QuotaPackageId, x.PurchasedAt });
}
}

View File

@@ -0,0 +1,479 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary>
/// 会员积分商城 EF Core 仓储实现。
/// </summary>
public sealed class EfPointMallRepository(TakeoutAppDbContext context) : IPointMallRepository
{
/// <inheritdoc />
public Task<MemberPointMallRule?> GetRuleByStoreAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRules
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallRules.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallRules.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberPointMallProduct>> SearchProductsAsync(
long tenantId,
long storeId,
MemberPointMallProductStatus? status,
string? keyword,
CancellationToken cancellationToken = default)
{
var query = context.MemberPointMallProducts
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
var normalizedKeyword = (keyword ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
{
var like = $"%{normalizedKeyword}%";
query = query.Where(item =>
EF.Functions.ILike(item.Name, like) ||
(item.PhysicalName != null && EF.Functions.ILike(item.PhysicalName, like)));
}
return await query
.OrderByDescending(item => item.Status)
.ThenByDescending(item => item.UpdatedAt ?? item.CreatedAt)
.ThenByDescending(item => item.Id)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallProduct?> FindProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == productId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallProduct?> GetProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == productId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallProducts.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallProducts.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallProducts.Remove(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<bool> HasRecordsByProductIdAsync(
long tenantId,
long storeId,
long pointMallProductId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.AnyAsync(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.PointMallProductId == pointMallProductId,
cancellationToken);
}
/// <inheritdoc />
public Task<int> CountMemberRedeemsByProductAsync(
long tenantId,
long storeId,
long pointMallProductId,
long memberId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.PointMallProductId == pointMallProductId &&
item.MemberId == memberId &&
item.Status != MemberPointMallRecordStatus.Canceled)
.CountAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<(IReadOnlyList<MemberPointMallRecord> Items, int TotalCount)> SearchRecordsAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var normalizedPage = Math.Max(1, page);
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
var query = BuildRecordQuery(
tenantId,
storeId,
redeemType,
status,
startUtc,
endUtc,
keyword);
var totalCount = await query.CountAsync(cancellationToken);
if (totalCount == 0)
{
return ([], 0);
}
var items = await query
.OrderByDescending(item => item.RedeemedAt)
.ThenByDescending(item => item.Id)
.Skip((normalizedPage - 1) * normalizedPageSize)
.Take(normalizedPageSize)
.ToListAsync(cancellationToken);
return (items, totalCount);
}
/// <inheritdoc />
public Task<MemberPointMallRecord?> GetRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == recordId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<MemberPointMallRecord?> FindRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.Id == recordId)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberPointMallRecord>> ListRecordsForExportAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
CancellationToken cancellationToken = default)
{
return await BuildRecordQuery(
tenantId,
storeId,
redeemType,
status,
startUtc,
endUtc,
keyword)
.OrderByDescending(item => item.RedeemedAt)
.ThenByDescending(item => item.Id)
.Take(20_000)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
{
return context.MemberPointMallRecords.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default)
{
context.MemberPointMallRecords.Update(entity);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task AddPointLedgerAsync(MemberPointLedger entity, CancellationToken cancellationToken = default)
{
return context.MemberPointLedgers.AddAsync(entity, cancellationToken).AsTask();
}
/// <inheritdoc />
public async Task<MemberPointMallRuleStatsSnapshot> GetRuleStatsAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default)
{
var redeemedPoints = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.Select(item => (int?)item.UsedPoints)
.SumAsync(cancellationToken) ?? 0;
var memberIds = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
.Select(item => item.MemberId)
.Distinct()
.ToListAsync(cancellationToken);
var pointMembers = memberIds.Count;
if (pointMembers == 0)
{
return new MemberPointMallRuleStatsSnapshot
{
TotalIssuedPoints = 0,
RedeemedPoints = 0,
PointMembers = 0,
RedeemRate = 0m
};
}
var currentPoints = await context.MemberProfiles
.AsNoTracking()
.Where(item => item.TenantId == tenantId && memberIds.Contains(item.Id))
.Select(item => (int?)item.PointsBalance)
.SumAsync(cancellationToken) ?? 0;
var totalIssuedPoints = Math.Max(redeemedPoints + Math.Max(0, currentPoints), 0);
var redeemRate = totalIssuedPoints <= 0
? 0m
: decimal.Round((decimal)redeemedPoints * 100m / totalIssuedPoints, 1, MidpointRounding.AwayFromZero);
return new MemberPointMallRuleStatsSnapshot
{
TotalIssuedPoints = totalIssuedPoints,
RedeemedPoints = redeemedPoints,
PointMembers = pointMembers,
RedeemRate = redeemRate
};
}
/// <inheritdoc />
public async Task<MemberPointMallRecordStatsSnapshot> GetRecordStatsAsync(
long tenantId,
long storeId,
DateTime nowUtc,
CancellationToken cancellationToken = default)
{
var normalizedNow = NormalizeUtc(nowUtc);
var dayStart = new DateTime(
normalizedNow.Year,
normalizedNow.Month,
normalizedNow.Day,
0,
0,
0,
DateTimeKind.Utc);
var monthStart = new DateTime(
normalizedNow.Year,
normalizedNow.Month,
1,
0,
0,
0,
DateTimeKind.Utc);
var todayRedeemCount = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemedAt >= dayStart &&
item.RedeemedAt <= normalizedNow)
.CountAsync(cancellationToken);
var pendingPhysicalCount = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemType == MemberPointMallRedeemType.Physical &&
item.Status == MemberPointMallRecordStatus.PendingPickup)
.CountAsync(cancellationToken);
var currentMonthUsedPoints = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
item.RedeemedAt >= monthStart &&
item.RedeemedAt <= normalizedNow)
.Select(item => (int?)item.UsedPoints)
.SumAsync(cancellationToken) ?? 0;
return new MemberPointMallRecordStatsSnapshot
{
TodayRedeemCount = todayRedeemCount,
PendingPhysicalCount = pendingPhysicalCount,
CurrentMonthUsedPoints = currentMonthUsedPoints
};
}
/// <inheritdoc />
public async Task<Dictionary<long, MemberPointMallProductAggregateSnapshot>> GetProductAggregatesAsync(
long tenantId,
long storeId,
IReadOnlyCollection<long> pointMallProductIds,
CancellationToken cancellationToken = default)
{
if (pointMallProductIds.Count == 0)
{
return [];
}
var aggregates = await context.MemberPointMallRecords
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.StoreId == storeId &&
pointMallProductIds.Contains(item.PointMallProductId))
.GroupBy(item => item.PointMallProductId)
.Select(group => new MemberPointMallProductAggregateSnapshot
{
PointMallProductId = group.Key,
RedeemedCount = group.Count()
})
.ToListAsync(cancellationToken);
return aggregates.ToDictionary(item => item.PointMallProductId, item => item);
}
/// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
private IQueryable<MemberPointMallRecord> BuildRecordQuery(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword)
{
var query = context.MemberPointMallRecords
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
if (redeemType.HasValue)
{
query = query.Where(item => item.RedeemType == redeemType.Value);
}
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
if (startUtc.HasValue)
{
var normalizedStart = NormalizeUtc(startUtc.Value);
query = query.Where(item => item.RedeemedAt >= normalizedStart);
}
if (endUtc.HasValue)
{
var normalizedEnd = NormalizeUtc(endUtc.Value);
query = query.Where(item => item.RedeemedAt <= normalizedEnd);
}
var normalizedKeyword = (keyword ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
{
var like = $"%{normalizedKeyword}%";
query = query.Where(item =>
EF.Functions.ILike(item.RecordNo, like) ||
EF.Functions.ILike(item.MemberName, like) ||
EF.Functions.ILike(item.MemberMobileMasked, like) ||
EF.Functions.ILike(item.ProductName, like));
}
return query;
}
private static DateTime NormalizeUtc(DateTime value)
{
return value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
};
}
}

View File

@@ -0,0 +1,187 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMemberPointsMallModule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "member_point_mall_products",
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: "门店标识。"),
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "展示名称。"),
ImageUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "展示图片地址。"),
RedeemType = table.Column<int>(type: "integer", nullable: false, comment: "兑换类型。"),
ProductId = table.Column<long>(type: "bigint", nullable: true, comment: "关联商品 ID兑换商品时必填。"),
CouponTemplateId = table.Column<long>(type: "bigint", nullable: true, comment: "关联优惠券模板 ID兑换优惠券时必填。"),
PhysicalName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true, comment: "实物名称(兑换实物时必填)。"),
PickupMethod = table.Column<int>(type: "integer", nullable: true, comment: "实物领取方式。"),
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "商品描述。"),
ExchangeType = table.Column<int>(type: "integer", nullable: false, comment: "兑换方式(纯积分/积分+现金)。"),
RequiredPoints = table.Column<int>(type: "integer", nullable: false, comment: "所需积分。"),
CashAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "现金部分(积分+现金时使用)。"),
StockTotal = table.Column<int>(type: "integer", nullable: false, comment: "初始库存数量。"),
StockAvailable = table.Column<int>(type: "integer", nullable: false, comment: "剩余库存数量。"),
PerMemberLimit = table.Column<int>(type: "integer", nullable: true, comment: "每人限兑次数null 表示不限)。"),
NotifyChannelsJson = table.Column<string>(type: "text", nullable: false, comment: "到账通知渠道JSON 数组)。"),
Status = table.Column<int>(type: "integer", nullable: false, 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_member_point_mall_products", x => x.Id);
},
comment: "会员积分商城兑换商品。");
migrationBuilder.CreateTable(
name: "member_point_mall_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: "门店标识。"),
RecordNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "兑换记录单号。"),
PointMallProductId = table.Column<long>(type: "bigint", nullable: false, comment: "关联积分商品 ID。"),
MemberId = table.Column<long>(type: "bigint", nullable: false, comment: "会员标识。"),
MemberName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会员名称快照。"),
MemberMobileMasked = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "会员手机号快照(脱敏)。"),
ProductName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "商品名称快照。"),
RedeemType = table.Column<int>(type: "integer", nullable: false, comment: "兑换类型快照。"),
ExchangeType = table.Column<int>(type: "integer", nullable: false, comment: "兑换方式快照。"),
UsedPoints = table.Column<int>(type: "integer", nullable: false, comment: "消耗积分。"),
CashAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "现金部分。"),
Status = table.Column<int>(type: "integer", nullable: false, comment: "记录状态。"),
RedeemedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "兑换时间UTC。"),
IssuedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "发放时间UTC。"),
VerifiedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "核销时间UTC。"),
VerifyMethod = table.Column<int>(type: "integer", nullable: true, comment: "核销方式。"),
VerifyRemark = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "核销备注。"),
VerifiedBy = table.Column<long>(type: "bigint", 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_member_point_mall_records", x => x.Id);
},
comment: "会员积分商城兑换记录。");
migrationBuilder.CreateTable(
name: "member_point_mall_rules",
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: "门店标识。"),
IsConsumeRewardEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用消费获取。"),
ConsumeAmountPerStep = table.Column<int>(type: "integer", nullable: false, comment: "每消费多少元触发一次积分计算。"),
ConsumeRewardPointsPerStep = table.Column<int>(type: "integer", nullable: false, comment: "每步获得积分。"),
IsReviewRewardEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用评价奖励。"),
ReviewRewardPoints = table.Column<int>(type: "integer", nullable: false, comment: "评价奖励积分。"),
IsRegisterRewardEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用注册奖励。"),
RegisterRewardPoints = table.Column<int>(type: "integer", nullable: false, comment: "注册奖励积分。"),
IsSigninRewardEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否启用签到奖励。"),
SigninRewardPoints = table.Column<int>(type: "integer", nullable: false, comment: "签到奖励积分。"),
ExpiryMode = table.Column<int>(type: "integer", nullable: false, 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_member_point_mall_rules", x => x.Id);
},
comment: "会员积分商城规则配置。");
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_products_TenantId_StoreId_CouponTemplateId",
table: "member_point_mall_products",
columns: new[] { "TenantId", "StoreId", "CouponTemplateId" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_products_TenantId_StoreId_Name",
table: "member_point_mall_products",
columns: new[] { "TenantId", "StoreId", "Name" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_products_TenantId_StoreId_ProductId",
table: "member_point_mall_products",
columns: new[] { "TenantId", "StoreId", "ProductId" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_products_TenantId_StoreId_Status",
table: "member_point_mall_products",
columns: new[] { "TenantId", "StoreId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_records_TenantId_StoreId_MemberId_RedeemedAt",
table: "member_point_mall_records",
columns: new[] { "TenantId", "StoreId", "MemberId", "RedeemedAt" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_records_TenantId_StoreId_PointMallProductId_RedeemedAt",
table: "member_point_mall_records",
columns: new[] { "TenantId", "StoreId", "PointMallProductId", "RedeemedAt" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_records_TenantId_StoreId_RecordNo",
table: "member_point_mall_records",
columns: new[] { "TenantId", "StoreId", "RecordNo" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_records_TenantId_StoreId_RedeemedAt",
table: "member_point_mall_records",
columns: new[] { "TenantId", "StoreId", "RedeemedAt" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_records_TenantId_StoreId_Status_RedeemedAt",
table: "member_point_mall_records",
columns: new[] { "TenantId", "StoreId", "Status", "RedeemedAt" });
migrationBuilder.CreateIndex(
name: "IX_member_point_mall_rules_TenantId_StoreId",
table: "member_point_mall_rules",
columns: new[] { "TenantId", "StoreId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "member_point_mall_products");
migrationBuilder.DropTable(
name: "member_point_mall_records");
migrationBuilder.DropTable(
name: "member_point_mall_rules");
}
}
}