feat: 实现完整的前后端功能

- 后端新增管理员、商品、分类聚合模型
- 实现积分规则、礼品、订单、会员等完整功能
- 添加管理员认证和权限管理
- 完善数据库迁移和实体配置
- 前端管理后台实现登录、仪表盘、积分规则、礼品、订单、会员等页面
- 集成shadcn-vue UI组件库
- 添加前后端功能文档和截图
This commit is contained in:
sam 2026-02-11 21:36:37 +08:00
parent e24925e1ed
commit 056eb9b6f9
265 changed files with 14607 additions and 250 deletions

View File

@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- Third-party package versions --> <!-- Third-party package versions -->
<NetCorePalVersion>3.2.1</NetCorePalVersion> <NetCorePalVersion>3.2.1</NetCorePalVersion>
@ -13,14 +12,12 @@
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion> <NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion> <NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" /> <PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" /> <PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
<!-- Database providers - framework specific versions --> <!-- Database providers - framework specific versions -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
@ -39,7 +36,6 @@
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" /> <PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
<!-- CAP packages for .NET 9.0+ --> <!-- CAP packages for .NET 9.0+ -->
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
@ -50,14 +46,12 @@
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
<!-- FastEndpoints --> <!-- FastEndpoints -->
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" /> <PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
<!-- Other packages --> <!-- Other packages -->
<PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" /> <PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" /> <PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
@ -70,7 +64,6 @@
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" /> <PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<!-- Aspire packages --> <!-- Aspire packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" /> <PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" /> <PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
@ -98,7 +91,6 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" /> <PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" /> <PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
<!-- NetCorePal packages --> <!-- NetCorePal packages -->
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
@ -126,7 +118,6 @@
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
<!-- Testing packages --> <!-- Testing packages -->
<PackageVersion Include="Moq" Version="4.20.72" /> <PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" /> <PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
@ -145,7 +136,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" /> <PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
<!-- Code analysis --> <!-- Code analysis -->
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" /> <PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup> </ItemGroup>

13
Backend/dotnet-tools.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.3",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@ -0,0 +1,117 @@
using Fengling.Backend.Domain.DomainEvents;
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
/// <summary>
/// 管理员ID
/// </summary>
public partial record AdminId : IGuidStronglyTypedId;
/// <summary>
/// 管理员聚合根
/// </summary>
public class Admin : Entity<AdminId>, IAggregateRoot
{
protected Admin() { }
private Admin(string username, string passwordHash)
{
Username = username;
PasswordHash = passwordHash;
Status = AdminStatus.Active;
CreatedAt = DateTime.UtcNow;
this.AddDomainEvent(new AdminCreatedDomainEvent(this));
}
/// <summary>
/// 用户名
/// </summary>
public string Username { get; private set; } = string.Empty;
/// <summary>
/// 密码哈希
/// </summary>
public string PasswordHash { get; private set; } = string.Empty;
/// <summary>
/// 管理员状态
/// </summary>
public AdminStatus Status { get; private set; }
/// <summary>
/// 最后登录时间
/// </summary>
public DateTime? LastLoginAt { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 软删除标记
/// </summary>
public Deleted Deleted { get; private set; } = new();
/// <summary>
/// 行版本
/// </summary>
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 创建管理员
/// </summary>
public static Admin Create(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
throw new KnownException("用户名不能为空");
if (string.IsNullOrWhiteSpace(password))
throw new KnownException("密码不能为空");
var passwordHash = PasswordHelper.HashPassword(password);
return new Admin(username, passwordHash);
}
/// <summary>
/// 验证密码
/// </summary>
public bool VerifyPassword(string password)
{
return PasswordHelper.VerifyPassword(password, PasswordHash);
}
/// <summary>
/// 记录登录
/// </summary>
public void RecordLogin()
{
LastLoginAt = DateTime.UtcNow;
this.AddDomainEvent(new AdminLoggedInDomainEvent(this));
}
/// <summary>
/// 禁用管理员
/// </summary>
public void Disable()
{
if (Status == AdminStatus.Disabled)
return;
Status = AdminStatus.Disabled;
this.AddDomainEvent(new AdminDisabledDomainEvent(this));
}
/// <summary>
/// 启用管理员
/// </summary>
public void Enable()
{
if (Status == AdminStatus.Active)
return;
Status = AdminStatus.Active;
this.AddDomainEvent(new AdminEnabledDomainEvent(this));
}
}

View File

@ -0,0 +1,17 @@
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
/// <summary>
/// 管理员状态
/// </summary>
public enum AdminStatus
{
/// <summary>
/// 正常
/// </summary>
Active = 1,
/// <summary>
/// 禁用
/// </summary>
Disabled = 2
}

View File

@ -0,0 +1,36 @@
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
/// <summary>
/// 密码加密工具类
/// </summary>
public static class PasswordHelper
{
/// <summary>
/// 加密密码
/// </summary>
public static string HashPassword(string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("密码不能为空", nameof(password));
return BCrypt.Net.BCrypt.HashPassword(password);
}
/// <summary>
/// 验证密码
/// </summary>
public static bool VerifyPassword(string password, string hash)
{
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
return false;
try
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
catch
{
return false;
}
}
}

View File

@ -0,0 +1,111 @@
namespace Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
/// <summary>
/// 品类ID
/// </summary>
public partial record CategoryId : IGuidStronglyTypedId;
/// <summary>
/// 品类聚合根
/// </summary>
public class Category : Entity<CategoryId>, IAggregateRoot
{
protected Category() { }
public Category(
string name,
string code,
string? description = null,
int sortOrder = 0)
{
Name = name;
Code = code;
Description = description ?? string.Empty;
SortOrder = sortOrder;
IsActive = true;
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 品类名称
/// </summary>
public string Name { get; private set; } = string.Empty;
/// <summary>
/// 品类编码(唯一)
/// </summary>
public string Code { get; private set; } = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string Description { get; private set; } = string.Empty;
/// <summary>
/// 排序
/// </summary>
public int SortOrder { get; private set; }
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 更新品类信息
/// </summary>
public void UpdateInfo(
string? name = null,
string? description = null,
int? sortOrder = null)
{
if (!string.IsNullOrWhiteSpace(name))
Name = name;
if (description != null)
Description = description;
if (sortOrder.HasValue)
SortOrder = sortOrder.Value;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 激活
/// </summary>
public void Activate()
{
if (!IsActive)
{
IsActive = true;
UpdatedAt = DateTime.UtcNow;
}
}
/// <summary>
/// 停用
/// </summary>
public void Deactivate()
{
if (IsActive)
{
IsActive = false;
UpdatedAt = DateTime.UtcNow;
}
}
}

View File

@ -0,0 +1,116 @@
namespace Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
/// <summary>
/// 产品ID
/// </summary>
public partial record ProductId : IGuidStronglyTypedId;
/// <summary>
/// 产品聚合根
/// </summary>
public class Product : Entity<ProductId>, IAggregateRoot
{
protected Product() { }
public Product(
string name,
Guid categoryId,
string categoryName,
string? description = null)
{
Name = name;
CategoryId = categoryId;
CategoryName = categoryName;
Description = description ?? string.Empty;
IsActive = true;
CreatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 产品名称
/// </summary>
public string Name { get; private set; } = string.Empty;
/// <summary>
/// 品类ID
/// </summary>
public Guid CategoryId { get; private set; }
/// <summary>
/// 品类名称
/// </summary>
public string CategoryName { get; private set; } = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string Description { get; private set; } = string.Empty;
/// <summary>
/// 是否激活
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; private set; }
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
/// <summary>
/// 更新产品信息
/// </summary>
public void UpdateInfo(
string? name = null,
Guid? categoryId = null,
string? categoryName = null,
string? description = null)
{
if (!string.IsNullOrWhiteSpace(name))
Name = name;
if (categoryId.HasValue && categoryId.Value != Guid.Empty)
{
CategoryId = categoryId.Value;
if (!string.IsNullOrWhiteSpace(categoryName))
CategoryName = categoryName;
}
if (description != null)
Description = description;
UpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// 激活
/// </summary>
public void Activate()
{
if (!IsActive)
{
IsActive = true;
UpdatedAt = DateTime.UtcNow;
}
}
/// <summary>
/// 停用
/// </summary>
public void Deactivate()
{
if (IsActive)
{
IsActive = false;
UpdatedAt = DateTime.UtcNow;
}
}
}

View File

@ -0,0 +1,23 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
namespace Fengling.Backend.Domain.DomainEvents;
/// <summary>
/// 管理员创建领域事件
/// </summary>
public record AdminCreatedDomainEvent(Admin Admin) : IDomainEvent;
/// <summary>
/// 管理员登录领域事件
/// </summary>
public record AdminLoggedInDomainEvent(Admin Admin) : IDomainEvent;
/// <summary>
/// 管理员禁用领域事件
/// </summary>
public record AdminDisabledDomainEvent(Admin Admin) : IDomainEvent;
/// <summary>
/// 管理员启用领域事件
/// </summary>
public record AdminEnabledDomainEvent(Admin Admin) : IDomainEvent;

View File

@ -7,6 +7,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" />
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" /> <PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" /> <PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
<PackageReference Include="NetCorePal.Extensions.Primitives" /> <PackageReference Include="NetCorePal.Extensions.Primitives" />

View File

@ -1,12 +1,15 @@
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence; using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate; using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate; using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate; using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate; using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
namespace Fengling.Backend.Infrastructure; namespace Fengling.Backend.Infrastructure;
@ -14,6 +17,9 @@ public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
: AppDbContextBase(options, mediator) : AppDbContextBase(options, mediator)
, ISqliteCapDataStorage , ISqliteCapDataStorage
{ {
// 管理员聚合
public DbSet<Admin> Admins => Set<Admin>();
// 会员聚合 // 会员聚合
public DbSet<Member> Members => Set<Member>(); public DbSet<Member> Members => Set<Member>();
public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>(); public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>();
@ -30,6 +36,12 @@ public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
// 兑换订单聚合 // 兑换订单聚合
public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>(); public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>();
// 品类聚合
public DbSet<Category> Categories => Set<Category>();
// 产品聚合
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
if (modelBuilder is null) if (modelBuilder is null)

View File

@ -0,0 +1,49 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
/// <summary>
/// 管理员实体配置
/// </summary>
public class AdminEntityTypeConfiguration : IEntityTypeConfiguration<Admin>
{
public void Configure(EntityTypeBuilder<Admin> builder)
{
builder.ToTable("Admins");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("管理员ID");
builder.Property(x => x.Username)
.IsRequired()
.HasMaxLength(50)
.HasComment("用户名");
builder.Property(x => x.PasswordHash)
.IsRequired()
.HasMaxLength(255)
.HasComment("密码哈希");
builder.Property(x => x.Status)
.IsRequired()
.HasComment("管理员状态(1=Active,2=Disabled)");
builder.Property(x => x.LastLoginAt)
.HasComment("最后登录时间");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
// 索引
builder.HasIndex(x => x.Username)
.IsUnique()
.HasDatabaseName("IX_Admins_Username");
builder.HasIndex(x => x.Status)
.HasDatabaseName("IX_Admins_Status");
}
}

View File

@ -0,0 +1,51 @@
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class CategoryEntityTypeConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable("Categories");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("品类ID");
builder.Property(x => x.Name)
.IsRequired()
.HasMaxLength(100)
.HasComment("品类名称");
builder.Property(x => x.Code)
.IsRequired()
.HasMaxLength(50)
.HasComment("品类编码");
builder.Property(x => x.Description)
.IsRequired()
.HasMaxLength(500)
.HasComment("描述");
builder.Property(x => x.SortOrder)
.IsRequired()
.HasComment("排序");
builder.Property(x => x.IsActive)
.IsRequired()
.HasComment("是否激活");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
builder.Property(x => x.UpdatedAt)
.IsRequired()
.HasComment("更新时间");
builder.HasIndex(x => x.Code).IsUnique().HasDatabaseName("IX_Categories_Code");
builder.HasIndex(x => x.SortOrder).HasDatabaseName("IX_Categories_SortOrder");
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_Categories_IsActive");
}
}

View File

@ -48,7 +48,7 @@ public class PointsTransactionEntityTypeConfiguration : IEntityTypeConfiguration
// 索引 // 索引
builder.HasIndex(x => x.MemberId).HasDatabaseName("IX_PointsTransactions_MemberId"); builder.HasIndex(x => x.MemberId).HasDatabaseName("IX_PointsTransactions_MemberId");
builder.HasIndex(x => x.RelatedId).IsUnique().HasDatabaseName("IX_PointsTransactions_RelatedId"); builder.HasIndex(x => x.RelatedId).HasDatabaseName("IX_PointsTransactions_RelatedId");
builder.HasIndex(x => x.Type).HasDatabaseName("IX_PointsTransactions_Type"); builder.HasIndex(x => x.Type).HasDatabaseName("IX_PointsTransactions_Type");
builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt"); builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt");
} }

View File

@ -0,0 +1,50 @@
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
public class ProductEntityTypeConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.UseGuidVersion7ValueGenerator()
.HasComment("产品ID");
builder.Property(x => x.Name)
.IsRequired()
.HasMaxLength(100)
.HasComment("产品名称");
builder.Property(x => x.CategoryId)
.IsRequired()
.HasComment("品类ID");
builder.Property(x => x.CategoryName)
.IsRequired()
.HasMaxLength(100)
.HasComment("品类名称");
builder.Property(x => x.Description)
.IsRequired()
.HasMaxLength(500)
.HasComment("描述");
builder.Property(x => x.IsActive)
.IsRequired()
.HasComment("是否激活");
builder.Property(x => x.CreatedAt)
.IsRequired()
.HasComment("创建时间");
builder.Property(x => x.UpdatedAt)
.IsRequired()
.HasComment("更新时间");
builder.HasIndex(x => x.CategoryId).HasDatabaseName("IX_Products_CategoryId");
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_Products_IsActive");
}
}

View File

@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Fengling.Backend.Infrastructure.Migrations namespace Fengling.Backend.Infrastructure.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
[Migration("20260211044819_Init")] [Migration("20260211061437_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -355,7 +355,6 @@ namespace Fengling.Backend.Infrastructure.Migrations
.HasDatabaseName("IX_PointsTransactions_MemberId"); .HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId") b.HasIndex("RelatedId")
.IsUnique()
.HasDatabaseName("IX_PointsTransactions_RelatedId"); .HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type") b.HasIndex("Type")
@ -449,6 +448,112 @@ namespace Fengling.Backend.Infrastructure.Migrations
b.ToTable("RedemptionOrders", (string)null); b.ToTable("RedemptionOrders", (string)null);
}); });
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b => modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{ {
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 => b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>

View File

@ -11,6 +11,58 @@ namespace Fengling.Backend.Infrastructure.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.CreateTable(
name: "CAPLock",
columns: table => new
{
Key = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
Instance = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
LastLockTime = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPLock", x => x.Key);
});
migrationBuilder.CreateTable(
name: "CAPPublishedMessage",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "INTEGER", nullable: true),
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
StatusName = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
});
migrationBuilder.CreateTable(
name: "CAPReceivedMessage",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
Group = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Content = table.Column<string>(type: "TEXT", nullable: true),
Retries = table.Column<int>(type: "INTEGER", nullable: true),
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
StatusName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Gifts", name: "Gifts",
columns: table => new columns: table => new
@ -160,6 +212,26 @@ namespace Fengling.Backend.Infrastructure.Migrations
table.PrimaryKey("PK_RedemptionOrders", x => x.Id); table.PrimaryKey("PK_RedemptionOrders", x => x.Id);
}); });
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName",
table: "CAPPublishedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex(
name: "IX_Version_ExpiresAt_StatusName1",
table: "CAPReceivedMessage",
columns: new[] { "Version", "ExpiresAt", "StatusName" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Gifts_IsOnShelf", name: "IX_Gifts_IsOnShelf",
table: "Gifts", table: "Gifts",
@ -235,8 +307,7 @@ namespace Fengling.Backend.Infrastructure.Migrations
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_RelatedId", name: "IX_PointsTransactions_RelatedId",
table: "PointsTransactions", table: "PointsTransactions",
column: "RelatedId", column: "RelatedId");
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_PointsTransactions_Type", name: "IX_PointsTransactions_Type",
@ -268,6 +339,15 @@ namespace Fengling.Backend.Infrastructure.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "CAPLock");
migrationBuilder.DropTable(
name: "CAPPublishedMessage");
migrationBuilder.DropTable(
name: "CAPReceivedMessage");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Gifts"); name: "Gifts");

View File

@ -0,0 +1,753 @@
// <auto-generated />
using System;
using Fengling.Backend.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260211074433_AddAdminAggregate")]
partial class AddAdminAggregate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("管理员ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT")
.HasComment("最后登录时间");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.HasComment("密码哈希");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("管理员状态(1=Active,2=Disabled)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Status")
.HasDatabaseName("IX_Admins_Status");
b.HasIndex("Username")
.IsUnique()
.HasDatabaseName("IX_Admins_Username");
b.ToTable("Admins", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<int>("AvailableStock")
.HasColumnType("INTEGER")
.HasComment("可用库存");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("图片URL");
b.Property<bool>("IsOnShelf")
.HasColumnType("INTEGER")
.HasComment("是否上架");
b.Property<int?>("LimitPerMember")
.HasColumnType("INTEGER")
.HasComment("每人限兑数量");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasComment("所需积分");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<int>("TotalStock")
.HasColumnType("INTEGER")
.HasComment("总库存");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("IsOnShelf")
.HasDatabaseName("IX_Gifts_IsOnShelf");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Gifts_SortOrder");
b.HasIndex("Type")
.HasDatabaseName("IX_Gifts_Type");
b.ToTable("Gifts", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("营销码ID");
b.Property<string>("BatchNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("批次号");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("营销码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<bool>("IsUsed")
.HasColumnType("INTEGER")
.HasComment("是否已使用");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime?>("UsedAt")
.HasColumnType("TEXT")
.HasComment("使用时间");
b.Property<Guid?>("UsedByMemberId")
.HasColumnType("TEXT")
.HasComment("使用者会员ID");
b.HasKey("Id");
b.HasIndex("BatchNo")
.HasDatabaseName("IX_MarketingCodes_BatchNo");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_MarketingCodes_Code");
b.HasIndex("IsUsed")
.HasDatabaseName("IX_MarketingCodes_IsUsed");
b.ToTable("MarketingCodes", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<int>("AvailablePoints")
.HasColumnType("INTEGER")
.HasComment("可用积分");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("昵称");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("密码(已加密)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("手机号");
b.Property<DateTime>("RegisteredAt")
.HasColumnType("TEXT")
.HasComment("注册时间");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("状态(1:正常,2:禁用)");
b.Property<int>("TotalPoints")
.HasColumnType("INTEGER")
.HasComment("累计总积分");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique()
.HasDatabaseName("IX_Members_Phone");
b.HasIndex("Status")
.HasDatabaseName("IX_Members_Status");
b.ToTable("Members", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分规则ID");
b.Property<decimal>("BonusMultiplier")
.HasColumnType("TEXT")
.HasComment("奖励倍数");
b.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT")
.HasComment("生效结束时间");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("MemberLevelCode")
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("会员等级编码");
b.Property<int>("PointsValue")
.HasColumnType("INTEGER")
.HasComment("积分值");
b.Property<Guid?>("ProductId")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("RuleName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("规则名称");
b.Property<int>("RuleType")
.HasColumnType("INTEGER")
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
b.Property<DateTime>("StartDate")
.HasColumnType("TEXT")
.HasComment("生效开始时间");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_PointsRules_IsActive");
b.HasIndex("MemberLevelCode")
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
b.HasIndex("ProductId")
.HasDatabaseName("IX_PointsRules_ProductId");
b.HasIndex("StartDate", "EndDate")
.HasDatabaseName("IX_PointsRules_DateRange");
b.ToTable("PointsRules", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分流水ID");
b.Property<int>("Amount")
.HasColumnType("INTEGER")
.HasComment("积分数量");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasComment("原因描述");
b.Property<Guid>("RelatedId")
.HasColumnType("TEXT")
.HasComment("关联ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("来源");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId")
.HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type")
.HasDatabaseName("IX_PointsTransactions_Type");
b.ToTable("PointsTransactions", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("兑换订单ID");
b.Property<string>("CancelReason")
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("取消原因");
b.Property<int>("ConsumedPoints")
.HasColumnType("INTEGER")
.HasComment("消耗积分");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<Guid>("GiftId")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<string>("GiftName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("GiftType")
.HasColumnType("INTEGER")
.HasComment("礼品类型");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("订单号");
b.Property<int>("Quantity")
.HasColumnType("INTEGER")
.HasComment("数量");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
b.Property<string>("TrackingNo")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("物流单号");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_RedemptionOrders_MemberId");
b.HasIndex("OrderNo")
.IsUnique()
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
b.HasIndex("Status")
.HasDatabaseName("IX_RedemptionOrders_Status");
b.ToTable("RedemptionOrders", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
{
b1.Property<Guid>("MarketingCodeId")
.HasColumnType("TEXT");
b1.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasColumnName("CategoryId")
.HasComment("品类ID");
b1.Property<string>("CategoryName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("CategoryName")
.HasComment("品类名称");
b1.Property<Guid>("ProductId")
.HasColumnType("TEXT")
.HasColumnName("ProductId")
.HasComment("产品ID");
b1.Property<string>("ProductName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("ProductName")
.HasComment("产品名称");
b1.HasKey("MarketingCodeId");
b1.ToTable("MarketingCodes");
b1.WithOwner()
.HasForeignKey("MarketingCodeId");
});
b.Navigation("ProductInfo")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
{
b1.Property<Guid>("MemberId")
.HasColumnType("TEXT");
b1.Property<decimal>("BonusRate")
.HasColumnType("TEXT")
.HasColumnName("BonusRate")
.HasComment("积分奖励倍率");
b1.Property<string>("LevelCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("LevelCode")
.HasComment("等级编码");
b1.Property<string>("LevelName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("LevelName")
.HasComment("等级名称");
b1.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasColumnName("RequiredPoints")
.HasComment("所需积分");
b1.HasKey("MemberId");
b1.ToTable("Members");
b1.WithOwner()
.HasForeignKey("MemberId");
});
b.Navigation("Level")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
{
b1.Property<Guid>("RedemptionOrderId")
.HasColumnType("TEXT");
b1.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("City")
.HasComment("市");
b1.Property<string>("DetailAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("DetailAddress")
.HasComment("详细地址");
b1.Property<string>("District")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("District")
.HasComment("区/县");
b1.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("ReceiverPhone")
.HasComment("联系电话");
b1.Property<string>("Province")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("Province")
.HasComment("省");
b1.Property<string>("ReceiverName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("ReceiverName")
.HasComment("收货人姓名");
b1.HasKey("RedemptionOrderId");
b1.ToTable("RedemptionOrders");
b1.WithOwner()
.HasForeignKey("RedemptionOrderId");
});
b.Navigation("ShippingAddress");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddAdminAggregate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Admins",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "管理员ID"),
Username = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "用户名"),
PasswordHash = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false, comment: "密码哈希"),
Status = table.Column<int>(type: "INTEGER", nullable: false, comment: "管理员状态(1=Active,2=Disabled)"),
LastLoginAt = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "最后登录时间"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Admins", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Admins_Status",
table: "Admins",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_Admins_Username",
table: "Admins",
column: "Username",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Admins");
}
}
}

View File

@ -0,0 +1,873 @@
// <auto-generated />
using System;
using Fengling.Backend.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260211084650_AddProductAndCategoryAggregates")]
partial class AddProductAndCategoryAggregates
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("管理员ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT")
.HasComment("最后登录时间");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.HasComment("密码哈希");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("管理员状态(1=Active,2=Disabled)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Status")
.HasDatabaseName("IX_Admins_Status");
b.HasIndex("Username")
.IsUnique()
.HasDatabaseName("IX_Admins_Username");
b.ToTable("Admins", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.CategoryAggregate.Category", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("品类编码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("品类名称");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_Categories_Code");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Categories_IsActive");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Categories_SortOrder");
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<int>("AvailableStock")
.HasColumnType("INTEGER")
.HasComment("可用库存");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("图片URL");
b.Property<bool>("IsOnShelf")
.HasColumnType("INTEGER")
.HasComment("是否上架");
b.Property<int?>("LimitPerMember")
.HasColumnType("INTEGER")
.HasComment("每人限兑数量");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasComment("所需积分");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<int>("TotalStock")
.HasColumnType("INTEGER")
.HasComment("总库存");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("IsOnShelf")
.HasDatabaseName("IX_Gifts_IsOnShelf");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Gifts_SortOrder");
b.HasIndex("Type")
.HasDatabaseName("IX_Gifts_Type");
b.ToTable("Gifts", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("营销码ID");
b.Property<string>("BatchNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("批次号");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("营销码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<bool>("IsUsed")
.HasColumnType("INTEGER")
.HasComment("是否已使用");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime?>("UsedAt")
.HasColumnType("TEXT")
.HasComment("使用时间");
b.Property<Guid?>("UsedByMemberId")
.HasColumnType("TEXT")
.HasComment("使用者会员ID");
b.HasKey("Id");
b.HasIndex("BatchNo")
.HasDatabaseName("IX_MarketingCodes_BatchNo");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_MarketingCodes_Code");
b.HasIndex("IsUsed")
.HasDatabaseName("IX_MarketingCodes_IsUsed");
b.ToTable("MarketingCodes", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<int>("AvailablePoints")
.HasColumnType("INTEGER")
.HasComment("可用积分");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("昵称");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("密码(已加密)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("手机号");
b.Property<DateTime>("RegisteredAt")
.HasColumnType("TEXT")
.HasComment("注册时间");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("状态(1:正常,2:禁用)");
b.Property<int>("TotalPoints")
.HasColumnType("INTEGER")
.HasComment("累计总积分");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique()
.HasDatabaseName("IX_Members_Phone");
b.HasIndex("Status")
.HasDatabaseName("IX_Members_Status");
b.ToTable("Members", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分规则ID");
b.Property<decimal>("BonusMultiplier")
.HasColumnType("TEXT")
.HasComment("奖励倍数");
b.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("EndDate")
.HasColumnType("TEXT")
.HasComment("生效结束时间");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("MemberLevelCode")
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasComment("会员等级编码");
b.Property<int>("PointsValue")
.HasColumnType("INTEGER")
.HasComment("积分值");
b.Property<Guid?>("ProductId")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("RuleName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("规则名称");
b.Property<int>("RuleType")
.HasColumnType("INTEGER")
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
b.Property<DateTime>("StartDate")
.HasColumnType("TEXT")
.HasComment("生效开始时间");
b.HasKey("Id");
b.HasIndex("IsActive")
.HasDatabaseName("IX_PointsRules_IsActive");
b.HasIndex("MemberLevelCode")
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
b.HasIndex("ProductId")
.HasDatabaseName("IX_PointsRules_ProductId");
b.HasIndex("StartDate", "EndDate")
.HasDatabaseName("IX_PointsRules_DateRange");
b.ToTable("PointsRules", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("积分流水ID");
b.Property<int>("Amount")
.HasColumnType("INTEGER")
.HasComment("积分数量");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("ExpiryDate")
.HasColumnType("TEXT")
.HasComment("过期时间");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("Reason")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasComment("原因描述");
b.Property<Guid>("RelatedId")
.HasColumnType("TEXT")
.HasComment("关联ID");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("来源");
b.Property<int>("Type")
.HasColumnType("INTEGER")
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId")
.HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type")
.HasDatabaseName("IX_PointsTransactions_Type");
b.ToTable("PointsTransactions", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.ProductAggregate.Product", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<Guid>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<string>("CategoryName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("品类名称");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("产品名称");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CategoryId")
.HasDatabaseName("IX_Products_CategoryId");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Products_IsActive");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("兑换订单ID");
b.Property<string>("CancelReason")
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("取消原因");
b.Property<int>("ConsumedPoints")
.HasColumnType("INTEGER")
.HasComment("消耗积分");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<Guid>("GiftId")
.HasColumnType("TEXT")
.HasComment("礼品ID");
b.Property<string>("GiftName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("礼品名称");
b.Property<int>("GiftType")
.HasColumnType("INTEGER")
.HasComment("礼品类型");
b.Property<Guid>("MemberId")
.HasColumnType("TEXT")
.HasComment("会员ID");
b.Property<string>("OrderNo")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("订单号");
b.Property<int>("Quantity")
.HasColumnType("INTEGER")
.HasComment("数量");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
b.Property<string>("TrackingNo")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("物流单号");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CreatedAt")
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
b.HasIndex("MemberId")
.HasDatabaseName("IX_RedemptionOrders_MemberId");
b.HasIndex("OrderNo")
.IsUnique()
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
b.HasIndex("Status")
.HasDatabaseName("IX_RedemptionOrders_Status");
b.ToTable("RedemptionOrders", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
{
b1.Property<Guid>("MarketingCodeId")
.HasColumnType("TEXT");
b1.Property<Guid?>("CategoryId")
.HasColumnType("TEXT")
.HasColumnName("CategoryId")
.HasComment("品类ID");
b1.Property<string>("CategoryName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("CategoryName")
.HasComment("品类名称");
b1.Property<Guid>("ProductId")
.HasColumnType("TEXT")
.HasColumnName("ProductId")
.HasComment("产品ID");
b1.Property<string>("ProductName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("ProductName")
.HasComment("产品名称");
b1.HasKey("MarketingCodeId");
b1.ToTable("MarketingCodes");
b1.WithOwner()
.HasForeignKey("MarketingCodeId");
});
b.Navigation("ProductInfo")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
{
b1.Property<Guid>("MemberId")
.HasColumnType("TEXT");
b1.Property<decimal>("BonusRate")
.HasColumnType("TEXT")
.HasColumnName("BonusRate")
.HasComment("积分奖励倍率");
b1.Property<string>("LevelCode")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("LevelCode")
.HasComment("等级编码");
b1.Property<string>("LevelName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("LevelName")
.HasComment("等级名称");
b1.Property<int>("RequiredPoints")
.HasColumnType("INTEGER")
.HasColumnName("RequiredPoints")
.HasComment("所需积分");
b1.HasKey("MemberId");
b1.ToTable("Members");
b1.WithOwner()
.HasForeignKey("MemberId");
});
b.Navigation("Level")
.IsRequired();
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
{
b1.Property<Guid>("RedemptionOrderId")
.HasColumnType("TEXT");
b1.Property<string>("City")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("City")
.HasComment("市");
b1.Property<string>("DetailAddress")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("DetailAddress")
.HasComment("详细地址");
b1.Property<string>("District")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("District")
.HasComment("区/县");
b1.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("ReceiverPhone")
.HasComment("联系电话");
b1.Property<string>("Province")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("Province")
.HasComment("省");
b1.Property<string>("ReceiverName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("ReceiverName")
.HasComment("收货人姓名");
b1.HasKey("RedemptionOrderId");
b1.ToTable("RedemptionOrders");
b1.WithOwner()
.HasForeignKey("RedemptionOrderId");
});
b.Navigation("ShippingAddress");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Fengling.Backend.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddProductAndCategoryAggregates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "品类ID"),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"),
Code = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "品类编码"),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"),
SortOrder = table.Column<int>(type: "INTEGER", nullable: false, comment: "排序"),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否激活"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "产品ID"),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "产品名称"),
CategoryId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "品类ID"),
CategoryName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否激活"),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Categories_Code",
table: "Categories",
column: "Code",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Categories_IsActive",
table: "Categories",
column: "IsActive");
migrationBuilder.CreateIndex(
name: "IX_Categories_SortOrder",
table: "Categories",
column: "SortOrder");
migrationBuilder.CreateIndex(
name: "IX_Products_CategoryId",
table: "Products",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_Products_IsActive",
table: "Products",
column: "IsActive");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Categories");
migrationBuilder.DropTable(
name: "Products");
}
}
}

View File

@ -17,6 +17,117 @@ namespace Fengling.Backend.Infrastructure.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("管理员ID");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT")
.HasComment("最后登录时间");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.HasComment("密码哈希");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER")
.HasComment("管理员状态(1=Active,2=Disabled)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Status")
.HasDatabaseName("IX_Admins_Status");
b.HasIndex("Username")
.IsUnique()
.HasDatabaseName("IX_Admins_Username");
b.ToTable("Admins", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.CategoryAggregate.Category", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasComment("品类编码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("品类名称");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasComment("排序");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("IX_Categories_Code");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Categories_IsActive");
b.HasIndex("SortOrder")
.HasDatabaseName("IX_Categories_SortOrder");
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b => modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -352,7 +463,6 @@ namespace Fengling.Backend.Infrastructure.Migrations
.HasDatabaseName("IX_PointsTransactions_MemberId"); .HasDatabaseName("IX_PointsTransactions_MemberId");
b.HasIndex("RelatedId") b.HasIndex("RelatedId")
.IsUnique()
.HasDatabaseName("IX_PointsTransactions_RelatedId"); .HasDatabaseName("IX_PointsTransactions_RelatedId");
b.HasIndex("Type") b.HasIndex("Type")
@ -361,6 +471,64 @@ namespace Fengling.Backend.Infrastructure.Migrations
b.ToTable("PointsTransactions", (string)null); b.ToTable("PointsTransactions", (string)null);
}); });
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.ProductAggregate.Product", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasComment("产品ID");
b.Property<Guid>("CategoryId")
.HasColumnType("TEXT")
.HasComment("品类ID");
b.Property<string>("CategoryName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("品类名称");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasComment("创建时间");
b.Property<bool>("Deleted")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT")
.HasComment("描述");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER")
.HasComment("是否激活");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasComment("产品名称");
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("CategoryId")
.HasDatabaseName("IX_Products_CategoryId");
b.HasIndex("IsActive")
.HasDatabaseName("IX_Products_IsActive");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b => modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -446,6 +614,112 @@ namespace Fengling.Backend.Infrastructure.Migrations
b.ToTable("RedemptionOrders", (string)null); b.ToTable("RedemptionOrders", (string)null);
}); });
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Added")
.HasColumnType("TEXT");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("TEXT");
b.Property<int?>("Retries")
.HasColumnType("INTEGER");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b => modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
{ {
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 => b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>

View File

@ -0,0 +1,48 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
/// <summary>
/// 管理员仓储接口
/// </summary>
public interface IAdminRepository : IRepository<Admin, AdminId>
{
/// <summary>
/// 通过用户名获取管理员
/// </summary>
Task<Admin?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
/// <summary>
/// 检查用户名是否存在
/// </summary>
Task<bool> ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default);
/// <summary>
/// 检查是否存在任何管理员
/// </summary>
Task<bool> AnyAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// 管理员仓储实现
/// </summary>
public class AdminRepository(ApplicationDbContext context)
: RepositoryBase<Admin, AdminId, ApplicationDbContext>(context), IAdminRepository
{
public async Task<Admin?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default)
{
return await DbContext.Admins
.FirstOrDefaultAsync(x => x.Username == username, cancellationToken);
}
public async Task<bool> ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default)
{
return await DbContext.Admins
.AnyAsync(x => x.Username == username, cancellationToken);
}
public async Task<bool> AnyAsync(CancellationToken cancellationToken = default)
{
return await DbContext.Admins.AnyAsync(cancellationToken);
}
}

View File

@ -0,0 +1,25 @@
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
public interface ICategoryRepository : IRepository<Category, CategoryId>
{
Task<Category?> GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default);
Task<Category?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
}
public class CategoryRepository(ApplicationDbContext context)
: RepositoryBase<Category, CategoryId, ApplicationDbContext>(context), ICategoryRepository
{
public async Task<Category?> GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default)
{
return await DbContext.Categories
.FirstOrDefaultAsync(x => x.Id == categoryId, cancellationToken);
}
public async Task<Category?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
{
return await DbContext.Categories
.FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
}
}

View File

@ -26,7 +26,8 @@ public interface IPointsRuleRepository : IRepository<PointsRule, PointsRuleId>
string? memberLevelCode, string? memberLevelCode,
DateTime startDate, DateTime startDate,
DateTime? endDate, DateTime? endDate,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default,
PointsRuleId? excludeRuleId = null);
} }
/// <summary> /// <summary>
@ -57,10 +58,17 @@ public class PointsRuleRepository(ApplicationDbContext context)
string? memberLevelCode, string? memberLevelCode,
DateTime startDate, DateTime startDate,
DateTime? endDate, DateTime? endDate,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default,
PointsRuleId? excludeRuleId = null)
{ {
var query = DbContext.PointsRules.AsQueryable(); var query = DbContext.PointsRules.AsQueryable();
// 排除特定规则
if (excludeRuleId != null)
{
query = query.Where(x => x.Id != excludeRuleId);
}
// 检查维度是否完全一致 // 检查维度是否完全一致
if (productId.HasValue) if (productId.HasValue)
query = query.Where(x => x.ProductId == productId); query = query.Where(x => x.ProductId == productId);
@ -78,8 +86,9 @@ public class PointsRuleRepository(ApplicationDbContext context)
query = query.Where(x => x.MemberLevelCode == null); query = query.Where(x => x.MemberLevelCode == null);
// 检查时间重叠 // 检查时间重叠
var effectiveEndDate = endDate ?? DateTime.MaxValue;
query = query.Where(x => query = query.Where(x =>
x.StartDate <= (endDate ?? DateTime.MaxValue) && x.StartDate <= effectiveEndDate &&
(x.EndDate == null || x.EndDate >= startDate)); (x.EndDate == null || x.EndDate >= startDate));
return await query.AnyAsync(cancellationToken); return await query.AnyAsync(cancellationToken);

View File

@ -0,0 +1,25 @@
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
namespace Fengling.Backend.Infrastructure.Repositories;
public interface IProductRepository : IRepository<Product, ProductId>
{
Task<Product?> GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default);
Task<Product?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
}
public class ProductRepository(ApplicationDbContext context)
: RepositoryBase<Product, ProductId, ApplicationDbContext>(context), IProductRepository
{
public async Task<Product?> GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default)
{
return await DbContext.Products
.FirstOrDefaultAsync(x => x.Id == productId, cancellationToken);
}
public async Task<Product?> GetByNameAsync(string name, CancellationToken cancellationToken = default)
{
return await DbContext.Products
.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
}
}

View File

@ -0,0 +1,109 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Infrastructure.Repositories;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Fengling.Backend.Web.Application.Commands.AdminAuth;
/// <summary>
/// 管理员登录命令
/// </summary>
public record AdminLoginCommand(string Username, string Password) : ICommand<AdminLoginResponse>;
/// <summary>
/// 管理员登录响应
/// </summary>
public record AdminLoginResponse(AdminId AdminId, string Username, string Token, DateTime ExpiresAt);
/// <summary>
/// 管理员登录命令验证器
/// </summary>
public class AdminLoginCommandValidator : AbstractValidator<AdminLoginCommand>
{
public AdminLoginCommandValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("用户名不能为空")
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空");
}
}
/// <summary>
/// 管理员登录命令处理器
/// </summary>
public class AdminLoginCommandHandler(
IAdminRepository adminRepository,
IConfiguration configuration)
: ICommandHandler<AdminLoginCommand, AdminLoginResponse>
{
public async Task<AdminLoginResponse> Handle(AdminLoginCommand command, CancellationToken cancellationToken)
{
// 1. 通过用户名查询管理员
var admin = await adminRepository.GetByUsernameAsync(command.Username, cancellationToken);
if (admin is null)
{
throw new KnownException("用户名或密码错误");
}
// 2. 检查状态
if (admin.Status == AdminStatus.Disabled)
{
throw new KnownException("账号已被禁用");
}
// 3. 验证密码
if (!admin.VerifyPassword(command.Password))
{
throw new KnownException("用户名或密码错误");
}
// 4. 记录登录时间
admin.RecordLogin();
// 5. 生成 JWT Token
var token = GenerateJwtToken(admin.Id, admin.Username);
var expiresAt = DateTime.UtcNow.AddHours(24);
return new AdminLoginResponse(admin.Id, admin.Username, token, expiresAt);
}
private string GenerateJwtToken(AdminId adminId, string username)
{
var appConfig = configuration.GetSection("AppConfiguration").Get<Utils.AppConfiguration>()
?? new Utils.AppConfiguration
{
JwtIssuer = "FenglingBackend",
JwtAudience = "FenglingBackend",
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!"
};
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, adminId.ToString()),
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, "Admin"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
// 使用对称密钥
var secret = appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!";
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: appConfig.JwtIssuer,
audience: appConfig.JwtAudience,
claims: claims,
expires: DateTime.UtcNow.AddHours(24),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

View File

@ -0,0 +1,39 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.AdminAuth;
/// <summary>
/// 初始化默认管理员命令
/// </summary>
public record InitializeDefaultAdminCommand : ICommand;
/// <summary>
/// 初始化默认管理员命令处理器
/// </summary>
public class InitializeDefaultAdminCommandHandler(
IAdminRepository adminRepository,
IConfiguration configuration,
ILogger<InitializeDefaultAdminCommandHandler> logger)
: ICommandHandler<InitializeDefaultAdminCommand>
{
public async Task Handle(InitializeDefaultAdminCommand command, CancellationToken cancellationToken)
{
// 检查是否已存在管理员
if (await adminRepository.AnyAsync(cancellationToken))
{
logger.LogInformation("管理员账号已存在,跳过初始化");
return;
}
// 从配置读取默认管理员信息
var username = configuration.GetValue<string>("AppConfiguration:DefaultAdmin:Username") ?? "admin";
var password = configuration.GetValue<string>("AppConfiguration:DefaultAdmin:Password") ?? "Admin@123";
// 创建默认管理员
var admin = Admin.Create(username, password);
await adminRepository.AddAsync(admin, cancellationToken);
logger.LogInformation("默认管理员账号已初始化, 用户名: {Username}", username);
}
}

View File

@ -0,0 +1,99 @@
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.Categories;
/// <summary>
/// 创建品类命令
/// </summary>
public record CreateCategoryCommand(
string Name,
string Code,
string? Description = null,
int SortOrder = 0) : ICommand<CategoryId>;
public class CreateCategoryCommandValidator : AbstractValidator<CreateCategoryCommand>
{
public CreateCategoryCommandValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class CreateCategoryCommandHandler(ICategoryRepository categoryRepository)
: ICommandHandler<CreateCategoryCommand, CategoryId>
{
public async Task<CategoryId> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
{
var existingCategory = await categoryRepository.GetByCodeAsync(request.Code, cancellationToken);
if (existingCategory != null)
throw new KnownException("品类编码已存在");
var category = new Category(
request.Name,
request.Code,
request.Description,
request.SortOrder);
await categoryRepository.AddAsync(category, cancellationToken);
return category.Id;
}
}
/// <summary>
/// 更新品类命令
/// </summary>
public record UpdateCategoryCommand(
CategoryId CategoryId,
string? Name = null,
string? Description = null,
int? SortOrder = null) : ICommand<ResponseData>;
public class UpdateCategoryCommandValidator : AbstractValidator<UpdateCategoryCommand>
{
public UpdateCategoryCommandValidator()
{
RuleFor(x => x.CategoryId).NotEmpty();
RuleFor(x => x.Name).MaximumLength(100);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class UpdateCategoryCommandHandler(ICategoryRepository categoryRepository)
: ICommandHandler<UpdateCategoryCommand, ResponseData>
{
public async Task<ResponseData> Handle(UpdateCategoryCommand request, CancellationToken cancellationToken)
{
var category = await categoryRepository.GetByIdAsync(request.CategoryId, cancellationToken);
if (category == null)
throw new KnownException("品类不存在");
category.UpdateInfo(request.Name, request.Description, request.SortOrder);
await categoryRepository.UpdateAsync(category, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 删除品类命令
/// </summary>
public record DeleteCategoryCommand(CategoryId CategoryId) : ICommand<ResponseData>;
public class DeleteCategoryCommandHandler(ICategoryRepository categoryRepository)
: ICommandHandler<DeleteCategoryCommand, ResponseData>
{
public async Task<ResponseData> Handle(DeleteCategoryCommand request, CancellationToken cancellationToken)
{
var category = await categoryRepository.GetByIdAsync(request.CategoryId, cancellationToken);
if (category == null)
throw new KnownException("品类不存在");
await categoryRepository.DeleteAsync(category);
return new ResponseData();
}
}

View File

@ -8,7 +8,7 @@ namespace Fengling.Backend.Web.Application.Commands.Gifts;
/// </summary> /// </summary>
public record CreateGiftCommand( public record CreateGiftCommand(
string Name, string Name,
int Type, GiftType Type,
string Description, string Description,
string ImageUrl, string ImageUrl,
int RequiredPoints, int RequiredPoints,
@ -20,7 +20,7 @@ public class CreateGiftCommandValidator : AbstractValidator<CreateGiftCommand>
public CreateGiftCommandValidator() public CreateGiftCommandValidator()
{ {
RuleFor(x => x.Name).NotEmpty().MaximumLength(100); RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Type).IsInEnum(); RuleFor(x => x.Type).IsInEnum().WithMessage("礼品类型无效");
RuleFor(x => x.Description).NotEmpty().MaximumLength(500); RuleFor(x => x.Description).NotEmpty().MaximumLength(500);
RuleFor(x => x.ImageUrl).NotEmpty().MaximumLength(500); RuleFor(x => x.ImageUrl).NotEmpty().MaximumLength(500);
RuleFor(x => x.RequiredPoints).GreaterThan(0); RuleFor(x => x.RequiredPoints).GreaterThan(0);
@ -33,11 +33,9 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository)
{ {
public async Task<GiftId> Handle(CreateGiftCommand request, CancellationToken cancellationToken) public async Task<GiftId> Handle(CreateGiftCommand request, CancellationToken cancellationToken)
{ {
var giftType = (GiftType)request.Type;
var gift = new Gift( var gift = new Gift(
request.Name, request.Name,
giftType, request.Type,
request.Description, request.Description,
request.ImageUrl, request.ImageUrl,
request.RequiredPoints, request.RequiredPoints,
@ -54,7 +52,7 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository)
/// 更新礼品命令 /// 更新礼品命令
/// </summary> /// </summary>
public record UpdateGiftCommand( public record UpdateGiftCommand(
Guid GiftId, GiftId GiftId,
string? Name = null, string? Name = null,
string? Description = null, string? Description = null,
string? ImageUrl = null, string? ImageUrl = null,
@ -78,8 +76,7 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository)
{ {
public async Task<ResponseData> Handle(UpdateGiftCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(UpdateGiftCommand request, CancellationToken cancellationToken)
{ {
var giftId = new GiftId(request.GiftId); var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null) if (gift == null)
throw new KnownException("礼品不存在"); throw new KnownException("礼品不存在");
@ -100,15 +97,14 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository)
/// <summary> /// <summary>
/// 上架礼品命令 /// 上架礼品命令
/// </summary> /// </summary>
public record PutOnShelfCommand(Guid GiftId) : ICommand<ResponseData>; public record PutOnShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
public class PutOnShelfCommandHandler(IGiftRepository giftRepository) public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<PutOnShelfCommand, ResponseData> : ICommandHandler<PutOnShelfCommand, ResponseData>
{ {
public async Task<ResponseData> Handle(PutOnShelfCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(PutOnShelfCommand request, CancellationToken cancellationToken)
{ {
var giftId = new GiftId(request.GiftId); var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null) if (gift == null)
throw new KnownException("礼品不存在"); throw new KnownException("礼品不存在");
@ -123,15 +119,14 @@ public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
/// <summary> /// <summary>
/// 下架礼品命令 /// 下架礼品命令
/// </summary> /// </summary>
public record PutOffShelfCommand(Guid GiftId) : ICommand<ResponseData>; public record PutOffShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
public class PutOffShelfCommandHandler(IGiftRepository giftRepository) public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
: ICommandHandler<PutOffShelfCommand, ResponseData> : ICommandHandler<PutOffShelfCommand, ResponseData>
{ {
public async Task<ResponseData> Handle(PutOffShelfCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(PutOffShelfCommand request, CancellationToken cancellationToken)
{ {
var giftId = new GiftId(request.GiftId); var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null) if (gift == null)
throw new KnownException("礼品不存在"); throw new KnownException("礼品不存在");
@ -146,7 +141,7 @@ public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
/// <summary> /// <summary>
/// 增加库存命令 /// 增加库存命令
/// </summary> /// </summary>
public record AddGiftStockCommand(Guid GiftId, int Quantity) : ICommand<ResponseData>; public record AddGiftStockCommand(GiftId GiftId, int Quantity) : ICommand<ResponseData>;
public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand> public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand>
{ {
@ -162,8 +157,7 @@ public class AddGiftStockCommandHandler(IGiftRepository giftRepository)
{ {
public async Task<ResponseData> Handle(AddGiftStockCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(AddGiftStockCommand request, CancellationToken cancellationToken)
{ {
var giftId = new GiftId(request.GiftId); var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null) if (gift == null)
throw new KnownException("礼品不存在"); throw new KnownException("礼品不存在");

View File

@ -1,5 +1,10 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories; using Fengling.Backend.Infrastructure.Repositories;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Fengling.Backend.Web.Utils;
namespace Fengling.Backend.Web.Application.Commands.Members; namespace Fengling.Backend.Web.Application.Commands.Members;
@ -32,7 +37,8 @@ public class LoginMemberCommandValidator : AbstractValidator<LoginMemberCommand>
/// 会员登录命令处理器 /// 会员登录命令处理器
/// </summary> /// </summary>
public class LoginMemberCommandHandler( public class LoginMemberCommandHandler(
IMemberRepository memberRepository) IMemberRepository memberRepository,
IConfiguration configuration)
: ICommandHandler<LoginMemberCommand, LoginMemberResponse> : ICommandHandler<LoginMemberCommand, LoginMemberResponse>
{ {
public async Task<LoginMemberResponse> Handle(LoginMemberCommand command, CancellationToken cancellationToken) public async Task<LoginMemberResponse> Handle(LoginMemberCommand command, CancellationToken cancellationToken)
@ -50,8 +56,8 @@ public class LoginMemberCommandHandler(
if (member.Status == MemberStatus.Disabled) if (member.Status == MemberStatus.Disabled)
throw new KnownException("该账号已被禁用"); throw new KnownException("该账号已被禁用");
// 生成Token(这里简化处理) // 生成JWT Token
var token = GenerateToken(member.Id); var token = GenerateJwtToken(member.Id, member.Phone);
return new LoginMemberResponse(member.Id, token); return new LoginMemberResponse(member.Id, token);
} }
@ -61,9 +67,37 @@ public class LoginMemberCommandHandler(
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password)); return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
} }
private static string GenerateToken(MemberId memberId) private string GenerateJwtToken(MemberId memberId, string phone)
{ {
// TODO: 实际项目中应使用JWT var appConfig = configuration.GetSection("AppConfiguration").Get<AppConfiguration>()
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"Member:{memberId}:{DateTime.UtcNow:O}")); ?? new AppConfiguration
{
JwtIssuer = "FenglingBackend",
JwtAudience = "FenglingBackend",
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!",
TokenExpiryInMinutes = 1440 // 24小时
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, memberId.ToString()),
new Claim(ClaimTypes.Name, phone),
new Claim(ClaimTypes.Role, "Member"),
new Claim("member_id", memberId.ToString())
};
var token = new JwtSecurityToken(
issuer: appConfig.JwtIssuer,
audience: appConfig.JwtAudience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(appConfig.TokenExpiryInMinutes),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
} }
} }

View File

@ -0,0 +1,41 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
/// <summary>
/// 激活积分规则命令
/// </summary>
public record ActivatePointsRuleCommand(PointsRuleId RuleId) : ICommand;
/// <summary>
/// 激活积分规则命令验证器
/// </summary>
public class ActivatePointsRuleCommandValidator : AbstractValidator<ActivatePointsRuleCommand>
{
public ActivatePointsRuleCommandValidator()
{
RuleFor(x => x.RuleId)
.NotNull().WithMessage("规则ID不能为空");
}
}
/// <summary>
/// 激活积分规则命令处理器
/// </summary>
public class ActivatePointsRuleCommandHandler(
IPointsRuleRepository pointsRuleRepository)
: ICommandHandler<ActivatePointsRuleCommand>
{
public async Task Handle(ActivatePointsRuleCommand command, CancellationToken cancellationToken)
{
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
if (rule == null)
{
throw new KnownException("积分规则不存在");
}
rule.Activate();
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
}
}

View File

@ -55,7 +55,8 @@ public class CreatePointsRuleCommandHandler(
command.MemberLevelCode, command.MemberLevelCode,
command.StartDate, command.StartDate,
command.EndDate, command.EndDate,
cancellationToken)) cancellationToken,
null)) // 新创建的规则,不需要排除任何规则
{ {
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则"); throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
} }

View File

@ -0,0 +1,41 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
/// <summary>
/// 停用积分规则命令
/// </summary>
public record DeactivatePointsRuleCommand(PointsRuleId RuleId) : ICommand;
/// <summary>
/// 停用积分规则命令验证器
/// </summary>
public class DeactivatePointsRuleCommandValidator : AbstractValidator<DeactivatePointsRuleCommand>
{
public DeactivatePointsRuleCommandValidator()
{
RuleFor(x => x.RuleId)
.NotNull().WithMessage("规则ID不能为空");
}
}
/// <summary>
/// 停用积分规则命令处理器
/// </summary>
public class DeactivatePointsRuleCommandHandler(
IPointsRuleRepository pointsRuleRepository)
: ICommandHandler<DeactivatePointsRuleCommand>
{
public async Task Handle(DeactivatePointsRuleCommand command, CancellationToken cancellationToken)
{
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
if (rule == null)
{
throw new KnownException("积分规则不存在");
}
rule.Deactivate();
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
}
}

View File

@ -0,0 +1,89 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
/// <summary>
/// 更新积分规则命令
/// </summary>
public record UpdatePointsRuleCommand(
PointsRuleId RuleId,
string? RuleName = null,
int? PointsValue = null,
decimal? BonusMultiplier = null,
DateTime? StartDate = null,
DateTime? EndDate = null) : ICommand;
/// <summary>
/// 更新积分规则命令验证器
/// </summary>
public class UpdatePointsRuleCommandValidator : AbstractValidator<UpdatePointsRuleCommand>
{
public UpdatePointsRuleCommandValidator()
{
RuleFor(x => x.RuleId)
.NotNull().WithMessage("规则ID不能为空");
RuleFor(x => x.RuleName)
.MaximumLength(100).WithMessage("规则名称最多100个字符")
.When(x => !string.IsNullOrEmpty(x.RuleName));
RuleFor(x => x.PointsValue)
.GreaterThan(0).WithMessage("积分值必须大于0")
.When(x => x.PointsValue.HasValue);
RuleFor(x => x.BonusMultiplier)
.GreaterThan(0).WithMessage("奖励倍数必须大于0")
.When(x => x.BonusMultiplier.HasValue);
RuleFor(x => x.StartDate)
.LessThan(x => x.EndDate).WithMessage("开始时间必须早于结束时间")
.When(x => x.StartDate.HasValue && x.EndDate.HasValue);
}
}
/// <summary>
/// 更新积分规则命令处理器
/// </summary>
public class UpdatePointsRuleCommandHandler(
IPointsRuleRepository pointsRuleRepository)
: ICommandHandler<UpdatePointsRuleCommand>
{
public async Task Handle(UpdatePointsRuleCommand command, CancellationToken cancellationToken)
{
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
if (rule == null)
{
throw new KnownException("积分规则不存在");
}
// 如果有修改时间范围,检查是否存在冲突的规则
if (command.StartDate.HasValue || command.EndDate.HasValue)
{
var newStartDate = command.StartDate ?? rule.StartDate;
var newEndDate = command.EndDate ?? rule.EndDate;
if (await pointsRuleRepository.HasConflictingRuleAsync(
rule.ProductId,
rule.CategoryId,
rule.MemberLevelCode,
newStartDate,
newEndDate,
cancellationToken,
rule.Id))
{
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
}
}
// 更新规则
rule.Update(
command.RuleName,
command.PointsValue,
command.BonusMultiplier,
command.StartDate,
command.EndDate);
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
}
}

View File

@ -0,0 +1,98 @@
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
using Fengling.Backend.Infrastructure.Repositories;
namespace Fengling.Backend.Web.Application.Commands.Products;
/// <summary>
/// 创建产品命令
/// </summary>
public record CreateProductCommand(
string Name,
Guid CategoryId,
string CategoryName,
string? Description = null) : ICommand<ProductId>;
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.CategoryId).NotEmpty();
RuleFor(x => x.CategoryName).NotEmpty().MaximumLength(100);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class CreateProductCommandHandler(IProductRepository productRepository)
: ICommandHandler<CreateProductCommand, ProductId>
{
public async Task<ProductId> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product(
request.Name,
request.CategoryId,
request.CategoryName,
request.Description);
await productRepository.AddAsync(product, cancellationToken);
return product.Id;
}
}
/// <summary>
/// 更新产品命令
/// </summary>
public record UpdateProductCommand(
ProductId ProductId,
string? Name = null,
Guid? CategoryId = null,
string? CategoryName = null,
string? Description = null) : ICommand<ResponseData>;
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
{
public UpdateProductCommandValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Name).MaximumLength(100);
RuleFor(x => x.CategoryName).MaximumLength(100);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class UpdateProductCommandHandler(IProductRepository productRepository)
: ICommandHandler<UpdateProductCommand, ResponseData>
{
public async Task<ResponseData> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
{
var product = await productRepository.GetByIdAsync(request.ProductId, cancellationToken);
if (product == null)
throw new KnownException("产品不存在");
product.UpdateInfo(request.Name, request.CategoryId, request.CategoryName, request.Description);
await productRepository.UpdateAsync(product, cancellationToken);
return new ResponseData();
}
}
/// <summary>
/// 删除产品命令
/// </summary>
public record DeleteProductCommand(ProductId ProductId) : ICommand<ResponseData>;
public class DeleteProductCommandHandler(IProductRepository productRepository)
: ICommandHandler<DeleteProductCommand, ResponseData>
{
public async Task<ResponseData> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
{
var product = await productRepository.GetByIdAsync(request.ProductId, cancellationToken);
if (product == null)
throw new KnownException("产品不存在");
await productRepository.DeleteAsync(product);
return new ResponseData();
}
}

View File

@ -9,12 +9,12 @@ namespace Fengling.Backend.Web.Application.Commands.RedemptionOrders;
/// 创建兑换订单命令 /// 创建兑换订单命令
/// </summary> /// </summary>
public record CreateRedemptionOrderCommand( public record CreateRedemptionOrderCommand(
Guid MemberId, MemberId MemberId,
Guid GiftId, GiftId GiftId,
int Quantity, int Quantity,
AddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>; CreateRedemptionOrderAddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
public record AddressDto( public record CreateRedemptionOrderAddressDto(
string ReceiverName, string ReceiverName,
string Phone, string Phone,
string Province, string Province,
@ -30,8 +30,7 @@ public class CreateRedemptionOrderCommandHandler(
public async Task<RedemptionOrderId> Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken) public async Task<RedemptionOrderId> Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken)
{ {
// 1. 获取礼品信息 // 1. 获取礼品信息
var giftId = new GiftId(request.GiftId); var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
if (gift == null) if (gift == null)
throw new KnownException("礼品不存在"); throw new KnownException("礼品不存在");
@ -46,15 +45,14 @@ public class CreateRedemptionOrderCommandHandler(
if (gift.LimitPerMember.HasValue) if (gift.LimitPerMember.HasValue)
{ {
var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync( var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync(
request.MemberId, request.GiftId, cancellationToken); request.MemberId.Id, request.GiftId.Id, cancellationToken);
if (redeemedCount + request.Quantity > gift.LimitPerMember.Value) if (redeemedCount + request.Quantity > gift.LimitPerMember.Value)
throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个"); throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个");
} }
// 4. 获取会员信息并检查积分 // 4. 获取会员信息并检查积分
var memberId = new MemberId(request.MemberId); var member = await memberRepository.GetAsync(request.MemberId, cancellationToken);
var member = await memberRepository.GetAsync(memberId, cancellationToken);
if (member == null) if (member == null)
throw new KnownException("会员不存在"); throw new KnownException("会员不存在");
@ -89,8 +87,8 @@ public class CreateRedemptionOrderCommandHandler(
// 8. 创建订单 // 8. 创建订单
var order = new RedemptionOrder( var order = new RedemptionOrder(
orderNo, orderNo,
request.MemberId, request.MemberId.Id,
request.GiftId, request.GiftId.Id,
gift.Name, gift.Name,
(int)gift.Type, (int)gift.Type,
request.Quantity, request.Quantity,
@ -106,15 +104,14 @@ public class CreateRedemptionOrderCommandHandler(
/// <summary> /// <summary>
/// 标记订单为已发货命令 /// 标记订单为已发货命令
/// </summary> /// </summary>
public record MarkOrderAsDispatchedCommand(Guid OrderId, string? TrackingNo = null) : ICommand<ResponseData>; public record MarkOrderAsDispatchedCommand(RedemptionOrderId OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
public class MarkOrderAsDispatchedCommandHandler( public class MarkOrderAsDispatchedCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData> IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData>
{ {
public async Task<ResponseData> Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken)
{ {
var orderId = new RedemptionOrderId(request.OrderId); var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null) if (order == null)
throw new KnownException("订单不存在"); throw new KnownException("订单不存在");
@ -129,15 +126,14 @@ public class MarkOrderAsDispatchedCommandHandler(
/// <summary> /// <summary>
/// 完成订单命令 /// 完成订单命令
/// </summary> /// </summary>
public record CompleteOrderCommand(Guid OrderId) : ICommand<ResponseData>; public record CompleteOrderCommand(RedemptionOrderId OrderId) : ICommand<ResponseData>;
public class CompleteOrderCommandHandler( public class CompleteOrderCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData> IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData>
{ {
public async Task<ResponseData> Handle(CompleteOrderCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
{ {
var orderId = new RedemptionOrderId(request.OrderId); var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null) if (order == null)
throw new KnownException("订单不存在"); throw new KnownException("订单不存在");
@ -152,15 +148,14 @@ public class CompleteOrderCommandHandler(
/// <summary> /// <summary>
/// 取消订单命令 /// 取消订单命令
/// </summary> /// </summary>
public record CancelOrderCommand(Guid OrderId, string Reason) : ICommand<ResponseData>; public record CancelOrderCommand(RedemptionOrderId OrderId, string Reason) : ICommand<ResponseData>;
public class CancelOrderCommandHandler( public class CancelOrderCommandHandler(
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData> IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData>
{ {
public async Task<ResponseData> Handle(CancelOrderCommand request, CancellationToken cancellationToken) public async Task<ResponseData> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
{ {
var orderId = new RedemptionOrderId(request.OrderId); var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
if (order == null) if (order == null)
throw new KnownException("订单不存在"); throw new KnownException("订单不存在");

View File

@ -0,0 +1,46 @@
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.AdminAuth;
/// <summary>
/// 获取当前管理员查询
/// </summary>
public record GetCurrentAdminQuery(AdminId AdminId) : IQuery<AdminDto>;
/// <summary>
/// 管理员DTO
/// </summary>
public record AdminDto(
AdminId AdminId,
string Username,
string Status,
DateTime? LastLoginAt,
DateTime CreatedAt);
/// <summary>
/// 获取当前管理员查询处理器
/// </summary>
public class GetCurrentAdminQueryHandler(ApplicationDbContext context)
: IQueryHandler<GetCurrentAdminQuery, AdminDto>
{
public async Task<AdminDto> Handle(GetCurrentAdminQuery request, CancellationToken cancellationToken)
{
var admin = await context.Admins
.Where(x => x.Id == request.AdminId)
.Select(x => new AdminDto(
x.Id,
x.Username,
x.Status == AdminStatus.Active ? "Active" : "Disabled",
x.LastLoginAt,
x.CreatedAt))
.FirstOrDefaultAsync(cancellationToken);
if (admin is null)
{
throw new KnownException($"未找到管理员AdminId = {request.AdminId}");
}
return admin;
}
}

View File

@ -0,0 +1,81 @@
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.Categories;
public record CategoryDto
{
public Guid Id { get; init; }
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public int SortOrder { get; init; }
public bool IsActive { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
/// <summary>
/// 查询品类列表
/// </summary>
public record GetCategoriesQuery(bool? IsActive = null) : IQuery<List<CategoryDto>>;
public class GetCategoriesQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetCategoriesQuery, List<CategoryDto>>
{
public async Task<List<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
{
var query = dbContext.Categories.AsQueryable();
if (request.IsActive.HasValue)
{
query = query.Where(x => x.IsActive == request.IsActive.Value);
}
var categories = await query
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.Name)
.Select(x => new CategoryDto
{
Id = x.Id.Id,
Name = x.Name,
Code = x.Code,
Description = x.Description,
SortOrder = x.SortOrder,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.ToListAsync(cancellationToken);
return categories;
}
}
/// <summary>
/// 查询品类详情
/// </summary>
public record GetCategoryByIdQuery(Guid CategoryId) : IQuery<CategoryDto?>;
public class GetCategoryByIdQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetCategoryByIdQuery, CategoryDto?>
{
public async Task<CategoryDto?> Handle(GetCategoryByIdQuery request, CancellationToken cancellationToken)
{
var category = await dbContext.Categories
.Where(x => x.Id.Id == request.CategoryId)
.Select(x => new CategoryDto
{
Id = x.Id.Id,
Name = x.Name,
Code = x.Code,
Description = x.Description,
SortOrder = x.SortOrder,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.FirstOrDefaultAsync(cancellationToken);
return category;
}
}

View File

@ -1,3 +1,4 @@
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Infrastructure; using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.Gifts; namespace Fengling.Backend.Web.Application.Queries.Gifts;
@ -66,14 +67,14 @@ public class GetGiftsQueryHandler(ApplicationDbContext dbContext) : IQueryHandle
/// <summary> /// <summary>
/// 礼品详情查询 /// 礼品详情查询
/// </summary> /// </summary>
public record GetGiftByIdQuery(Guid GiftId) : IQuery<GiftDto?>; public record GetGiftByIdQuery(GiftId GiftId) : IQuery<GiftDto?>;
public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?> public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?>
{ {
public async Task<GiftDto?> Handle(GetGiftByIdQuery request, CancellationToken cancellationToken) public async Task<GiftDto?> Handle(GetGiftByIdQuery request, CancellationToken cancellationToken)
{ {
var gift = await dbContext.Gifts var gift = await dbContext.Gifts
.Where(x => x.Id.Id == request.GiftId) .Where(x => x.Id == request.GiftId)
.Select(x => new GiftDto .Select(x => new GiftDto
{ {
Id = x.Id.Id, Id = x.Id.Id,

View File

@ -0,0 +1,148 @@
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.MarketingCodes;
/// <summary>
/// 营销码DTO
/// </summary>
public record MarketingCodeDto
{
public Guid Id { get; init; }
public string Code { get; init; } = string.Empty;
public string BatchNo { get; init; } = string.Empty;
public Guid ProductId { get; init; }
public string ProductName { get; init; } = string.Empty;
public Guid? CategoryId { get; init; }
public string? CategoryName { get; init; }
public bool IsUsed { get; init; }
public Guid? UsedByMemberId { get; init; }
public DateTime? UsedAt { get; init; }
public DateTime? ExpiryDate { get; init; }
public DateTime CreatedAt { get; init; }
}
/// <summary>
/// 批次信息DTO
/// </summary>
public record MarketingCodeBatchDto
{
public string BatchNo { get; init; } = string.Empty;
public Guid ProductId { get; init; }
public string ProductName { get; init; } = string.Empty;
public Guid? CategoryId { get; init; }
public string? CategoryName { get; init; }
public int TotalCount { get; init; }
public int UsedCount { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime? ExpiryDate { get; init; }
}
/// <summary>
/// 查询营销码列表
/// </summary>
public record GetMarketingCodesQuery(
string? BatchNo = null,
Guid? ProductId = null,
bool? IsUsed = null,
DateTime? StartDate = null,
DateTime? EndDate = null) : IQuery<List<MarketingCodeDto>>;
public class GetMarketingCodesQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetMarketingCodesQuery, List<MarketingCodeDto>>
{
public async Task<List<MarketingCodeDto>> Handle(GetMarketingCodesQuery request, CancellationToken cancellationToken)
{
var query = dbContext.MarketingCodes.AsQueryable();
// 按批次号筛选
if (!string.IsNullOrWhiteSpace(request.BatchNo))
{
query = query.Where(x => x.BatchNo == request.BatchNo);
}
// 按产品ID筛选
if (request.ProductId.HasValue)
{
query = query.Where(x => x.ProductInfo.ProductId == request.ProductId.Value);
}
// 按使用状态筛选
if (request.IsUsed.HasValue)
{
query = query.Where(x => x.IsUsed == request.IsUsed.Value);
}
// 按创建时间范围筛选
if (request.StartDate.HasValue)
{
query = query.Where(x => x.CreatedAt >= request.StartDate.Value);
}
if (request.EndDate.HasValue)
{
// EndDate包含当天整天
var endOfDay = request.EndDate.Value.Date.AddDays(1);
query = query.Where(x => x.CreatedAt < endOfDay);
}
var marketingCodes = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new MarketingCodeDto
{
Id = x.Id.Id,
Code = x.Code,
BatchNo = x.BatchNo,
ProductId = x.ProductInfo.ProductId,
ProductName = x.ProductInfo.ProductName,
CategoryId = x.ProductInfo.CategoryId,
CategoryName = x.ProductInfo.CategoryName,
IsUsed = x.IsUsed,
UsedByMemberId = x.UsedByMemberId,
UsedAt = x.UsedAt,
ExpiryDate = x.ExpiryDate,
CreatedAt = x.CreatedAt
})
.ToListAsync(cancellationToken);
return marketingCodes;
}
}
/// <summary>
/// 查询所有批次列表
/// </summary>
public record GetMarketingCodeBatchesQuery : IQuery<List<MarketingCodeBatchDto>>;
public class GetMarketingCodeBatchesQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetMarketingCodeBatchesQuery, List<MarketingCodeBatchDto>>
{
public async Task<List<MarketingCodeBatchDto>> Handle(GetMarketingCodeBatchesQuery request, CancellationToken cancellationToken)
{
var batches = await dbContext.MarketingCodes
.GroupBy(x => new
{
x.BatchNo,
x.ProductInfo.ProductId,
x.ProductInfo.ProductName,
x.ProductInfo.CategoryId,
x.ProductInfo.CategoryName,
x.ExpiryDate
})
.Select(g => new MarketingCodeBatchDto
{
BatchNo = g.Key.BatchNo,
ProductId = g.Key.ProductId,
ProductName = g.Key.ProductName,
CategoryId = g.Key.CategoryId,
CategoryName = g.Key.CategoryName,
TotalCount = g.Count(),
UsedCount = g.Count(x => x.IsUsed),
CreatedAt = g.Min(x => x.CreatedAt),
ExpiryDate = g.Key.ExpiryDate
})
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
return batches;
}
}

View File

@ -0,0 +1,96 @@
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.PointsRules;
/// <summary>
/// 查询所有积分规则
/// </summary>
public record GetAllPointsRulesQuery(bool? IsActive = null, int? RuleType = null) : IQuery<List<PointsRuleDto>>;
public record PointsRuleDto
{
public Guid Id { get; init; }
public string RuleName { get; init; } = string.Empty;
public int RuleType { get; init; }
public int PointsValue { get; init; }
public decimal BonusMultiplier { get; init; }
public DateTime StartDate { get; init; }
public DateTime? EndDate { get; init; }
public Guid? ProductId { get; init; }
public Guid? CategoryId { get; init; }
public string? MemberLevelCode { get; init; }
public bool IsActive { get; init; }
public DateTime CreatedAt { get; init; }
}
public class GetAllPointsRulesQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetAllPointsRulesQuery, List<PointsRuleDto>>
{
public async Task<List<PointsRuleDto>> Handle(GetAllPointsRulesQuery request, CancellationToken cancellationToken)
{
var query = dbContext.PointsRules.AsQueryable();
if (request.IsActive.HasValue)
{
query = query.Where(x => x.IsActive == request.IsActive.Value);
}
if (request.RuleType.HasValue)
{
query = query.Where(x => (int)x.RuleType == request.RuleType.Value);
}
var rules = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new PointsRuleDto
{
Id = x.Id.Id,
RuleName = x.RuleName,
RuleType = (int)x.RuleType,
PointsValue = x.PointsValue,
BonusMultiplier = x.BonusMultiplier,
StartDate = x.StartDate,
EndDate = x.EndDate,
ProductId = x.ProductId,
CategoryId = x.CategoryId,
MemberLevelCode = x.MemberLevelCode,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt
})
.ToListAsync(cancellationToken);
return rules;
}
}
/// <summary>
/// 根据ID查询积分规则详情
/// </summary>
public record GetPointsRuleByIdQuery(PointsRuleId RuleId) : IQuery<PointsRuleDto?>;
public class GetPointsRuleByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetPointsRuleByIdQuery, PointsRuleDto?>
{
public async Task<PointsRuleDto?> Handle(GetPointsRuleByIdQuery request, CancellationToken cancellationToken)
{
var rule = await dbContext.PointsRules
.Where(x => x.Id == request.RuleId)
.Select(x => new PointsRuleDto
{
Id = x.Id.Id,
RuleName = x.RuleName,
RuleType = (int)x.RuleType,
PointsValue = x.PointsValue,
BonusMultiplier = x.BonusMultiplier,
StartDate = x.StartDate,
EndDate = x.EndDate,
ProductId = x.ProductId,
CategoryId = x.CategoryId,
MemberLevelCode = x.MemberLevelCode,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt
})
.FirstOrDefaultAsync(cancellationToken);
return rule;
}
}

View File

@ -0,0 +1,57 @@
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.PointsTransactions;
/// <summary>
/// 获取会员积分流水查询
/// </summary>
public record GetPointsTransactionsQuery(Guid MemberId, int? Type = null) : IQuery<List<PointsTransactionDto>>;
public record PointsTransactionDto
{
public Guid Id { get; init; }
public Guid MemberId { get; init; }
public int Type { get; init; }
public int Amount { get; init; }
public string Source { get; init; } = string.Empty;
public string Reason { get; init; } = string.Empty;
public Guid RelatedId { get; init; }
public DateTime? ExpiryDate { get; init; }
public DateTime CreatedAt { get; init; }
}
public class GetPointsTransactionsQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetPointsTransactionsQuery, List<PointsTransactionDto>>
{
public async Task<List<PointsTransactionDto>> Handle(GetPointsTransactionsQuery request,
CancellationToken cancellationToken)
{
var query = dbContext.PointsTransactions.AsQueryable();
query = query.Where(x => x.MemberId == new MemberId(request.MemberId));
if (request.Type.HasValue)
{
query = query.Where(x => (int)x.Type == request.Type.Value);
}
var transactions = await query
.OrderByDescending(x => x.CreatedAt)
.Select(x => new PointsTransactionDto
{
Id = x.Id.Id,
MemberId = x.MemberId.Id,
Type = (int)x.Type,
Amount = x.Amount,
Source = x.Source,
Reason = x.Reason,
RelatedId = x.RelatedId,
ExpiryDate = x.ExpiryDate,
CreatedAt = x.CreatedAt
})
.ToListAsync(cancellationToken);
return transactions;
}
}

View File

@ -0,0 +1,85 @@
using Fengling.Backend.Infrastructure;
namespace Fengling.Backend.Web.Application.Queries.Products;
public record ProductDto
{
public Guid Id { get; init; }
public string Name { get; init; } = string.Empty;
public Guid CategoryId { get; init; }
public string CategoryName { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public bool IsActive { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
/// <summary>
/// 查询产品列表
/// </summary>
public record GetProductsQuery(Guid? CategoryId = null, bool? IsActive = null) : IQuery<List<ProductDto>>;
public class GetProductsQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetProductsQuery, List<ProductDto>>
{
public async Task<List<ProductDto>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
{
var query = dbContext.Products.AsQueryable();
if (request.CategoryId.HasValue)
{
query = query.Where(x => x.CategoryId == request.CategoryId.Value);
}
if (request.IsActive.HasValue)
{
query = query.Where(x => x.IsActive == request.IsActive.Value);
}
var products = await query
.OrderBy(x => x.Name)
.Select(x => new ProductDto
{
Id = x.Id.Id,
Name = x.Name,
CategoryId = x.CategoryId,
CategoryName = x.CategoryName,
Description = x.Description,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.ToListAsync(cancellationToken);
return products;
}
}
/// <summary>
/// 查询产品详情
/// </summary>
public record GetProductByIdQuery(Guid ProductId) : IQuery<ProductDto?>;
public class GetProductByIdQueryHandler(ApplicationDbContext dbContext)
: IQueryHandler<GetProductByIdQuery, ProductDto?>
{
public async Task<ProductDto?> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
{
var product = await dbContext.Products
.Where(x => x.Id.Id == request.ProductId)
.Select(x => new ProductDto
{
Id = x.Id.Id,
Name = x.Name,
CategoryId = x.CategoryId,
CategoryName = x.CategoryName,
Description = x.Description,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt
})
.FirstOrDefaultAsync(cancellationToken);
return product;
}
}

View File

@ -17,7 +17,7 @@ public record RedemptionOrderDto
public int GiftType { get; init; } public int GiftType { get; init; }
public int Quantity { get; init; } public int Quantity { get; init; }
public int ConsumedPoints { get; init; } public int ConsumedPoints { get; init; }
public AddressDto? ShippingAddress { get; init; } public RedemptionOrderAddressDto? ShippingAddress { get; init; }
public string? TrackingNo { get; init; } public string? TrackingNo { get; init; }
public int Status { get; init; } public int Status { get; init; }
public string? CancelReason { get; init; } public string? CancelReason { get; init; }
@ -25,7 +25,7 @@ public record RedemptionOrderDto
public DateTime UpdatedAt { get; init; } public DateTime UpdatedAt { get; init; }
} }
public record AddressDto public record RedemptionOrderAddressDto
{ {
public string ReceiverName { get; init; } = string.Empty; public string ReceiverName { get; init; } = string.Empty;
public string Phone { get; init; } = string.Empty; public string Phone { get; init; } = string.Empty;
@ -64,7 +64,7 @@ public class GetRedemptionOrdersQueryHandler(ApplicationDbContext dbContext)
GiftType = x.GiftType, GiftType = x.GiftType,
Quantity = x.Quantity, Quantity = x.Quantity,
ConsumedPoints = x.ConsumedPoints, ConsumedPoints = x.ConsumedPoints,
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto
{ {
ReceiverName = x.ShippingAddress.ReceiverName, ReceiverName = x.ShippingAddress.ReceiverName,
Phone = x.ShippingAddress.Phone, Phone = x.ShippingAddress.Phone,
@ -107,7 +107,7 @@ public class GetRedemptionOrderByIdQueryHandler(ApplicationDbContext dbContext)
GiftType = x.GiftType, GiftType = x.GiftType,
Quantity = x.Quantity, Quantity = x.Quantity,
ConsumedPoints = x.ConsumedPoints, ConsumedPoints = x.ConsumedPoints,
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto
{ {
ReceiverName = x.ShippingAddress.ReceiverName, ReceiverName = x.ShippingAddress.ReceiverName,
Phone = x.ShippingAddress.Phone, Phone = x.ShippingAddress.Phone,

View File

@ -0,0 +1 @@
# Uploads Directory

View File

@ -0,0 +1,71 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.Categories;
using Fengling.Backend.Web.Application.Queries.Categories;
namespace Fengling.Backend.Web.Endpoints.Admin.Categories;
[Tags("Admin/Categories")]
[HttpGet("/api/admin/categories")]
[AllowAnonymous]
public class GetCategoriesEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<List<CategoryDto>>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var query = new GetCategoriesQuery();
var result = await mediator.Send(query, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
[Tags("Admin/Categories")]
[HttpGet("/api/admin/categories/{CategoryId}")]
[AllowAnonymous]
public class GetCategoryByIdEndpoint(IMediator mediator)
: Endpoint<GetCategoryByIdQuery, ResponseData<CategoryDto?>>
{
public override async Task HandleAsync(GetCategoryByIdQuery req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
[Tags("Admin/Categories")]
[HttpPost("/api/admin/categories")]
[AllowAnonymous]
public class CreateCategoryEndpoint(IMediator mediator)
: Endpoint<CreateCategoryCommand, ResponseData<Guid>>
{
public override async Task HandleAsync(CreateCategoryCommand req, CancellationToken ct)
{
var categoryId = await mediator.Send(req, ct);
await Send.OkAsync(categoryId.Id.AsResponseData(), ct);
}
}
[Tags("Admin/Categories")]
[HttpPut("/api/admin/categories/{CategoryId}")]
[AllowAnonymous]
public class UpdateCategoryEndpoint(IMediator mediator)
: Endpoint<UpdateCategoryCommand, ResponseData>
{
public override async Task HandleAsync(UpdateCategoryCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
[Tags("Admin/Categories")]
[HttpDelete("/api/admin/categories/{CategoryId}")]
[AllowAnonymous]
public class DeleteCategoryEndpoint(IMediator mediator)
: Endpoint<DeleteCategoryCommand, ResponseData>
{
public override async Task HandleAsync(DeleteCategoryCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}

View File

@ -0,0 +1,40 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Web.Application.Queries.PointsRules;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 获取积分规则列表端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpGet("/api/admin/points-rules")]
[AllowAnonymous]
public class GetPointsRulesEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<List<PointsRuleDto>>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var query = new GetAllPointsRulesQuery();
var rules = await mediator.Send(query, ct);
await Send.OkAsync(rules.AsResponseData(), ct);
}
}
/// <summary>
/// 根据ID获取积分规则详情端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpGet("/api/admin/points-rules/{id}")]
[AllowAnonymous]
public class GetPointsRuleByIdEndpoint(IMediator mediator)
: Endpoint<EmptyRequest, ResponseData<PointsRuleDto?>>
{
public override async Task HandleAsync(EmptyRequest _, CancellationToken ct)
{
var id = Route<PointsRuleId>("id");
var query = new GetPointsRuleByIdQuery(id!);
var rule = await mediator.Send(query, ct);
await Send.OkAsync(rule.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,56 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Queries.MarketingCodes;
namespace Fengling.Backend.Web.Endpoints.Admin.MarketingCodes;
/// <summary>
/// 查询营销码请求
/// </summary>
public record GetMarketingCodesRequest
{
public string? BatchNo { get; init; }
public Guid? ProductId { get; init; }
public bool? IsUsed { get; init; }
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
}
/// <summary>
/// 查询营销码列表端点
/// </summary>
[Tags("Admin-MarketingCodes")]
[HttpGet("/api/admin/marketing-codes")]
[AllowAnonymous]
public class GetMarketingCodesEndpoint(IMediator mediator)
: Endpoint<GetMarketingCodesRequest, ResponseData<List<MarketingCodeDto>>>
{
public override async Task HandleAsync(GetMarketingCodesRequest req, CancellationToken ct)
{
var query = new GetMarketingCodesQuery(
req.BatchNo,
req.ProductId,
req.IsUsed,
req.StartDate,
req.EndDate);
var result = await mediator.Send(query, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
/// <summary>
/// 查询营销码批次列表端点
/// </summary>
[Tags("Admin-MarketingCodes")]
[HttpGet("/api/admin/marketing-codes/batches")]
[AllowAnonymous]
public class GetMarketingCodeBatchesEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<List<MarketingCodeBatchDto>>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var query = new GetMarketingCodeBatchesQuery();
var result = await mediator.Send(query, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,43 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Web.Application.Commands.PointsRules;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 激活积分规则端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpPost("/api/admin/points-rules/{id}/activate")]
[AllowAnonymous]
public class ActivatePointsRuleEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<bool>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var id = Route<Guid>("id");
var ruleId = new PointsRuleId(id);
var command = new ActivatePointsRuleCommand(ruleId);
await mediator.Send(command, ct);
await Send.OkAsync(true.AsResponseData(), ct);
}
}
/// <summary>
/// 停用积分规则端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpPost("/api/admin/points-rules/{id}/deactivate")]
[AllowAnonymous]
public class DeactivatePointsRuleEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<bool>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var id = Route<Guid>("id");
var ruleId = new PointsRuleId(id);
var command = new DeactivatePointsRuleCommand(ruleId);
await mediator.Send(command, ct);
await Send.OkAsync(true.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,71 @@
using FastEndpoints;
using Fengling.Backend.Web.Application.Commands.Products;
using Fengling.Backend.Web.Application.Queries.Products;
namespace Fengling.Backend.Web.Endpoints.Admin.Products;
[Tags("Admin/Products")]
[HttpGet("/api/admin/products")]
[AllowAnonymous]
public class GetProductsEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<List<ProductDto>>>
{
public override async Task HandleAsync(CancellationToken ct)
{
var query = new GetProductsQuery();
var result = await mediator.Send(query, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
[Tags("Admin/Products")]
[HttpGet("/api/admin/products/{ProductId}")]
[AllowAnonymous]
public class GetProductByIdEndpoint(IMediator mediator)
: Endpoint<GetProductByIdQuery, ResponseData<ProductDto?>>
{
public override async Task HandleAsync(GetProductByIdQuery req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result.AsResponseData(), ct);
}
}
[Tags("Admin/Products")]
[HttpPost("/api/admin/products")]
[AllowAnonymous]
public class CreateProductEndpoint(IMediator mediator)
: Endpoint<CreateProductCommand, ResponseData<Guid>>
{
public override async Task HandleAsync(CreateProductCommand req, CancellationToken ct)
{
var productId = await mediator.Send(req, ct);
await Send.OkAsync(productId.Id.AsResponseData(), ct);
}
}
[Tags("Admin/Products")]
[HttpPut("/api/admin/products/{ProductId}")]
[AllowAnonymous]
public class UpdateProductEndpoint(IMediator mediator)
: Endpoint<UpdateProductCommand, ResponseData>
{
public override async Task HandleAsync(UpdateProductCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}
[Tags("Admin/Products")]
[HttpDelete("/api/admin/products/{ProductId}")]
[AllowAnonymous]
public class DeleteProductEndpoint(IMediator mediator)
: Endpoint<DeleteProductCommand, ResponseData>
{
public override async Task HandleAsync(DeleteProductCommand req, CancellationToken ct)
{
var result = await mediator.Send(req, ct);
await Send.OkAsync(result, ct);
}
}

View File

@ -0,0 +1,42 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
using Fengling.Backend.Web.Application.Commands.PointsRules;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 更新积分规则请求
/// </summary>
public record UpdatePointsRuleRequest(
string? RuleName = null,
int? PointsValue = null,
decimal? BonusMultiplier = null,
DateTime? StartDate = null,
DateTime? EndDate = null);
/// <summary>
/// 更新积分规则端点
/// </summary>
[Tags("Admin-PointsRules")]
[HttpPut("/api/admin/points-rules/{id}")]
[AllowAnonymous]
public class UpdatePointsRuleEndpoint(IMediator mediator)
: Endpoint<UpdatePointsRuleRequest, ResponseData<bool>>
{
public override async Task HandleAsync(UpdatePointsRuleRequest req, CancellationToken ct)
{
var id = Route<Guid>("id");
var ruleId = new PointsRuleId(id);
var command = new UpdatePointsRuleCommand(
ruleId,
req.RuleName,
req.PointsValue,
req.BonusMultiplier,
req.StartDate,
req.EndDate);
await mediator.Send(command, ct);
await Send.OkAsync(true.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,31 @@
using FastEndpoints;
using Fengling.Backend.Web.Services;
namespace Fengling.Backend.Web.Endpoints.Admin;
/// <summary>
/// 图片上传请求
/// </summary>
public class UploadImageRequest
{
public IFormFile File { get; set; } = null!;
public string? Folder { get; set; }
}
/// <summary>
/// 图片上传端点
/// </summary>
[Tags("Admin/Upload")]
[HttpPost("/api/admin/upload/image")]
[AllowAnonymous]
[AllowFileUploads]
public class UploadImageEndpoint(IFileStorageService fileStorageService)
: Endpoint<UploadImageRequest, ResponseData<string>>
{
public override async Task HandleAsync(UploadImageRequest req, CancellationToken ct)
{
var folder = string.IsNullOrWhiteSpace(req.Folder) ? "common" : req.Folder;
var url = await fileStorageService.UploadImageAsync(req.File, folder, ct);
await Send.OkAsync(url.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,39 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Web.Application.Commands.AdminAuth;
namespace Fengling.Backend.Web.Endpoints.AdminAuth;
/// <summary>
/// 管理员登录请求
/// </summary>
public record AdminLoginRequest(string Username, string Password);
/// <summary>
/// 管理员登录响应
/// </summary>
public record AdminLoginResponseDto(AdminId AdminId, string Username, string Token, DateTime ExpiresAt);
/// <summary>
/// 管理员登录端点
/// </summary>
[Tags("AdminAuth")]
[HttpPost("/api/admin/auth/login")]
[AllowAnonymous]
public class AdminLoginEndpoint(IMediator mediator)
: Endpoint<AdminLoginRequest, ResponseData<AdminLoginResponseDto>>
{
public override async Task HandleAsync(AdminLoginRequest req, CancellationToken ct)
{
var command = new AdminLoginCommand(req.Username, req.Password);
var response = await mediator.Send(command, ct);
var dto = new AdminLoginResponseDto(
response.AdminId,
response.Username,
response.Token,
response.ExpiresAt);
await Send.OkAsync(dto.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,40 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
using Fengling.Backend.Web.Application.Queries.AdminAuth;
using System.Security.Claims;
namespace Fengling.Backend.Web.Endpoints.AdminAuth;
/// <summary>
/// 获取当前管理员端点
/// </summary>
[Tags("AdminAuth")]
// [HttpGet("/api/admin/auth/me")]
public class GetCurrentAdminEndpoint(IMediator mediator)
: EndpointWithoutRequest<ResponseData<AdminDto>>
{
public override void Configure()
{
Get("/api/admin/auth/me");
Tags("AdminAuth");
Description(x => x.WithTags("AdminAuth"));
}
public override async Task HandleAsync(CancellationToken ct)
{
// 从 JWT Claims 中提取 AdminId
var adminIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(adminIdClaim) || !Guid.TryParse(adminIdClaim, out var adminGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
var adminId = new AdminId(adminGuid);
var query = new GetCurrentAdminQuery(adminId);
var admin = await mediator.Send(query, ct);
await Send.OkAsync(admin.AsResponseData(), ct);
}
}

View File

@ -1,6 +1,9 @@
using FastEndpoints; using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Web.Application.Commands.RedemptionOrders; using Fengling.Backend.Web.Application.Commands.RedemptionOrders;
using Fengling.Backend.Web.Application.Queries.Gifts; using Fengling.Backend.Web.Application.Queries.Gifts;
using System.Security.Claims;
namespace Fengling.Backend.Web.Endpoints.Gifts; namespace Fengling.Backend.Web.Endpoints.Gifts;
@ -37,17 +40,71 @@ public class GetGiftDetailEndpoint(IMediator mediator)
} }
} }
/// <summary>
/// 兑换礼品请求
/// </summary>
public record RedeemGiftRequest(
string GiftId,
int Quantity,
RedeemGiftAddressDto? ShippingAddress = null);
/// <summary>
/// 收货地址
/// </summary>
public record RedeemGiftAddressDto(
string ReceiverName,
string ReceiverPhone,
string Province,
string City,
string District,
string DetailAddress);
/// <summary> /// <summary>
/// 兑换礼品端点(会员端) /// 兑换礼品端点(会员端)
/// </summary> /// </summary>
[Tags("Gifts")] [Tags("Gifts")]
[HttpPost("/api/gifts/redeem")] [HttpPost("/api/gifts/redeem")]
public class RedeemGiftEndpoint(IMediator mediator) public class RedeemGiftEndpoint(IMediator mediator)
: Endpoint<CreateRedemptionOrderCommand, ResponseData<Guid>> : Endpoint<RedeemGiftRequest, ResponseData<string>>
{ {
public override async Task HandleAsync(CreateRedemptionOrderCommand req, CancellationToken ct) public override async Task HandleAsync(RedeemGiftRequest req, CancellationToken ct)
{ {
var orderId = await mediator.Send(req, ct); // 从 JWT Claims 中提取 MemberId
await Send.OkAsync(orderId.Id.AsResponseData(), ct); var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
if (!Guid.TryParse(req.GiftId, out var giftGuid))
{
throw new KnownException("礼品ID格式错误");
}
var memberId = new MemberId(memberGuid);
var giftId = new GiftId(giftGuid);
CreateRedemptionOrderAddressDto? shippingAddress = null;
if (req.ShippingAddress != null)
{
shippingAddress = new CreateRedemptionOrderAddressDto(
req.ShippingAddress.ReceiverName,
req.ShippingAddress.ReceiverPhone,
req.ShippingAddress.Province,
req.ShippingAddress.City,
req.ShippingAddress.District,
req.ShippingAddress.DetailAddress);
}
var command = new CreateRedemptionOrderCommand(
memberId,
giftId,
req.Quantity,
shippingAddress);
var orderId = await mediator.Send(command, ct);
await Send.OkAsync(orderId.Id.ToString().AsResponseData(), ct);
} }
} }

View File

@ -1,3 +1,4 @@
using System.Security.Claims;
using FastEndpoints; using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate; using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
@ -8,7 +9,7 @@ namespace Fengling.Backend.Web.Endpoints.MarketingCodes;
/// <summary> /// <summary>
/// 扫码请求 /// 扫码请求
/// </summary> /// </summary>
public record UseMarketingCodeRequest(string Code, MemberId MemberId); public record UseMarketingCodeRequest(string Code);
/// <summary> /// <summary>
/// 扫码响应 /// 扫码响应
@ -24,13 +25,22 @@ public record UseMarketingCodeEndpointResponse(
/// </summary> /// </summary>
[Tags("MarketingCodes")] [Tags("MarketingCodes")]
[HttpPost("/api/marketing-codes/scan")] [HttpPost("/api/marketing-codes/scan")]
[AllowAnonymous]
public class UseMarketingCodeEndpoint(IMediator mediator) public class UseMarketingCodeEndpoint(IMediator mediator)
: Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>> : Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>>
{ {
public override async Task HandleAsync(UseMarketingCodeRequest req, CancellationToken ct) public override async Task HandleAsync(UseMarketingCodeRequest req, CancellationToken ct)
{ {
var command = new UseMarketingCodeCommand(req.Code, req.MemberId); // 从 JWT Claims 中提取 MemberId
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
var memberId = new MemberId(memberGuid);
var command = new UseMarketingCodeCommand(req.Code, memberId);
var result = await mediator.Send(command, ct); var result = await mediator.Send(command, ct);
var response = new UseMarketingCodeEndpointResponse( var response = new UseMarketingCodeEndpointResponse(

View File

@ -0,0 +1,62 @@
using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Infrastructure.Repositories;
using System.Security.Claims;
namespace Fengling.Backend.Web.Endpoints.Members;
/// <summary>
/// 获取当前会员信息响应
/// </summary>
public record GetCurrentMemberResponse(
string Id,
string Phone,
string? Nickname,
int TotalPoints,
int AvailablePoints,
string Level,
string Status,
DateTime CreatedAt);
/// <summary>
/// 获取当前会员信息端点
/// </summary>
[Tags("Members")]
[HttpGet("/api/members/current")]
public class GetCurrentMemberEndpoint(IMemberRepository memberRepository)
: EndpointWithoutRequest<ResponseData<GetCurrentMemberResponse>>
{
public override async Task HandleAsync(CancellationToken ct)
{
// 从 JWT Claims 中提取 MemberId
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
var memberId = new MemberId(memberGuid);
var member = await memberRepository.GetAsync(memberId, ct);
if (member == null)
{
await Send.NotFoundAsync(ct);
return;
}
var response = new GetCurrentMemberResponse(
member.Id.ToString(),
member.Phone,
member.Nickname,
member.TotalPoints,
member.AvailablePoints,
member.Level.LevelName,
member.Status.ToString(),
member.RegisteredAt
);
await Send.OkAsync(response.AsResponseData(), ct);
}
}

View File

@ -0,0 +1,18 @@
using FastEndpoints;
namespace Fengling.Backend.Web.Endpoints.Members;
/// <summary>
/// 退出登录端点
/// </summary>
[Tags("Members")]
[HttpPost("/api/members/logout")]
public class LogoutMemberEndpoint : EndpointWithoutRequest
{
public override async Task HandleAsync(CancellationToken ct)
{
// JWT Token 由客户端负责清除,服务端无需特殊处理
// 这里可以添加黑名单Token逻辑如果需要
await Send.NoContentAsync(ct);
}
}

View File

@ -0,0 +1,32 @@
using System.Security.Claims;
using FastEndpoints;
using Fengling.Backend.Web.Application.Queries.PointsTransactions;
namespace Fengling.Backend.Web.Endpoints.PointsTransactions;
/// <summary>
/// 获取我的积分流水记录
/// </summary>
[Tags("PointsTransactions")]
[HttpGet("/api/points-transactions/my")]
public class GetMyPointsTransactionsEndpoint(IMediator mediator)
: Endpoint<GetMyPointsTransactionsRequest, ResponseData<List<PointsTransactionDto>>>
{
public override async Task HandleAsync(GetMyPointsTransactionsRequest req, CancellationToken ct)
{
// 从 JWT Claims 中提取 MemberId
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
var query = new GetPointsTransactionsQuery(MemberId: memberGuid, Type: req.Type);
var transactions = await mediator.Send(query, ct);
await Send.OkAsync(transactions.AsResponseData(), ct);
}
}
public record GetMyPointsTransactionsRequest(int? Type = null);

View File

@ -1,5 +1,7 @@
using FastEndpoints; using FastEndpoints;
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
using Fengling.Backend.Web.Application.Queries.RedemptionOrders; using Fengling.Backend.Web.Application.Queries.RedemptionOrders;
using System.Security.Claims;
namespace Fengling.Backend.Web.Endpoints.RedemptionOrders; namespace Fengling.Backend.Web.Endpoints.RedemptionOrders;
@ -13,15 +15,23 @@ public class GetMyRedemptionOrdersEndpoint(IMediator mediator)
{ {
public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct) public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct)
{ {
// TODO: 从JWT Token中获取当前登录会员ID // 从 JWT Claims 中提取 MemberId
// 暂时使用请求中的MemberId var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var query = new GetRedemptionOrdersQuery(MemberId: req.MemberId, Status: req.Status);
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
{
await Send.UnauthorizedAsync(ct);
return;
}
var memberId = memberGuid;
var query = new GetRedemptionOrdersQuery(MemberId: memberId, Status: req.Status);
var orders = await mediator.Send(query, ct); var orders = await mediator.Send(query, ct);
await Send.OkAsync(orders.AsResponseData(), ct); await Send.OkAsync(orders.AsResponseData(), ct);
} }
} }
public record GetMyRedemptionOrdersRequest(Guid MemberId, int? Status = null); public record GetMyRedemptionOrdersRequest(int? Status = null);
/// <summary> /// <summary>
/// 获取订单详情端点(会员端) /// 获取订单详情端点(会员端)

View File

@ -19,10 +19,13 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Refit; using Refit;
using NetCorePal.Extensions.CodeAnalysis; using NetCorePal.Extensions.CodeAnalysis;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.Enrich.WithClientIp() .Enrich.WithClientIp()
.WriteTo.Console(new JsonFormatter()) .WriteTo.Console()
.CreateLogger(); .CreateLogger();
try try
{ {
@ -59,17 +62,34 @@ try
// 配置JWT认证 // 配置JWT认证
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration")); builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" }; var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>()
?? new AppConfiguration
{
JwtIssuer = "FenglingBackend",
JwtAudience = "FenglingBackend",
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!"
};
builder.Services.AddAuthentication().AddJwtBearer(options => var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!"));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{ {
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience; options.SaveToken = true;
options.TokenValidationParameters.ValidateAudience = true; options.TokenValidationParameters = new TokenValidationParameters
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer; {
options.TokenValidationParameters.ValidateIssuer = true; ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = appConfig.JwtIssuer,
ValidAudience = appConfig.JwtAudience,
IssuerSigningKey = key,
ClockSkew = TimeSpan.Zero
};
}); });
builder.Services.AddNetCorePalJwt().AddRedisStore();
#endregion #endregion
@ -143,6 +163,9 @@ try
.AddKnownExceptionValidationBehavior() .AddKnownExceptionValidationBehavior()
.AddUnitOfWorkBehaviors()); .AddUnitOfWorkBehaviors());
// 文件存储服务
builder.Services.AddSingleton<Fengling.Backend.Web.Services.IFileStorageService, Fengling.Backend.Web.Services.LocalFileStorageService>();
#region #region
builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template") builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template")
@ -180,12 +203,16 @@ try
var app = builder.Build(); var app = builder.Build();
// 在非生产环境中执行数据库迁移包括开发、测试、Staging等环境 // 在非生产环境中执行数据库迁移(包括开发、测试、Staging等环境)
if (!app.Environment.IsProduction()) if (!app.Environment.IsProduction())
{ {
using var scope = app.Services.CreateScope(); using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync(); await dbContext.Database.MigrateAsync();
// 初始化默认管理员账号
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(new Fengling.Backend.Web.Application.Commands.AdminAuth.InitializeDefaultAdminCommand());
} }
@ -198,6 +225,15 @@ try
} }
app.UseStaticFiles(); app.UseStaticFiles();
app.UseCors(x =>
{
x
.SetIsOriginAllowed(_=>true)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyMethod()
.Build();
});
//app.UseHttpsRedirection(); //app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); // Authentication 必须在 Authorization 之前 app.UseAuthentication(); // Authentication 必须在 Authorization 之前

View File

@ -0,0 +1,16 @@
namespace Fengling.Backend.Web.Services;
/// <summary>
/// 文件存储服务接口
/// </summary>
public interface IFileStorageService
{
/// <summary>
/// 上传图片
/// </summary>
/// <param name="file">图片文件</param>
/// <param name="folder">文件夹名称</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>图片URL</returns>
Task<string> UploadImageAsync(IFormFile file, string folder, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,46 @@
namespace Fengling.Backend.Web.Services;
/// <summary>
/// 本地文件存储服务实现
/// </summary>
public class LocalFileStorageService(IWebHostEnvironment environment) : IFileStorageService
{
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
private const long MaxFileSize = 5 * 1024 * 1024; // 5MB
public async Task<string> UploadImageAsync(IFormFile file, string folder, CancellationToken cancellationToken = default)
{
// 验证文件
if (file == null || file.Length == 0)
throw new KnownException("文件不能为空");
if (file.Length > MaxFileSize)
throw new KnownException($"文件大小不能超过{MaxFileSize / 1024 / 1024}MB");
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!AllowedExtensions.Contains(extension))
throw new KnownException($"不支持的文件格式,仅支持: {string.Join(", ", AllowedExtensions)}");
// 生成文件路径
var yearMonth = DateTime.Now.ToString("yyyy-MM");
var fileName = $"{Guid.NewGuid()}{extension}";
var relativePath = Path.Combine("uploads", folder, yearMonth, fileName);
var absolutePath = Path.Combine(environment.WebRootPath, relativePath);
// 确保目录存在
var directory = Path.GetDirectoryName(absolutePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 保存文件
using (var stream = new FileStream(absolutePath, FileMode.Create))
{
await file.CopyToAsync(stream, cancellationToken);
}
// 返回相对URL
return $"/{relativePath.Replace("\\", "/")}";
}
}

View File

@ -7,7 +7,7 @@
}, },
"ConnectionStrings": { "ConnectionStrings": {
"SQLite": "Data Source=fengling.db", "SQLite": "Data Source=fengling.db",
"Redis": "81.68.223.70:6379" "Redis": "81.68.223.70:16379,password=sl52788542"
}, },
"Services": { "Services": {
"user": { "user": {

View File

@ -8,10 +8,10 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"SQLite": "Data Source=fengling.db", "SQLite": "Data Source=fengling.db",
"Redis": "81.68.223.70:6379" "Redis": "81.68.223.70:16379,password=sl52788542"
}, },
"RedisStreams": { "RedisStreams": {
"ConnectionString": "81.68.223.70:6379" "ConnectionString": "81.68.223.70:16379,password=sl52788542"
}, },
"Services": { "Services": {
"user": { "user": {
@ -24,5 +24,11 @@
"https://user-v2:8443" "https://user-v2:8443"
] ]
} }
},
"AppConfiguration": {
"Secret": "YourVerySecretKeyForJwtTokenGeneration12345!",
"TokenExpiryInMinutes": 1440,
"JwtIssuer": "FenglingBackend",
"JwtAudience": "FenglingBackend"
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://localhost:5511
VITE_APP_TITLE=Fengling 管理后台

View File

@ -0,0 +1,2 @@
VITE_API_BASE_URL=
VITE_APP_TITLE=Fengling 管理后台

View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@ -0,0 +1,21 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "",
"css": "src/style.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"registries": {}
}

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fengling 管理后台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,36 @@
{
"name": "fengling-backend-admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-table": "^8.21.3",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.563.0",
"pinia": "^3.0.4",
"reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0",
"vue": "^3.5.25",
"vue-router": "^4.6.4",
"vue-sonner": "^2.0.9"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.1.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
import { Toaster } from '@/components/ui/sonner'
</script>
<template>
<RouterView />
<Toaster position="top-right" :duration="3000" />
</template>

View File

@ -0,0 +1,47 @@
import apiClient from './client'
import type { ResponseData } from '@/types/api'
/**
*
*/
export interface AdminLoginRequest {
username: string
password: string
}
/**
*
*/
export interface AdminLoginResponse {
adminId: string
username: string
token: string
expiresAt: string
}
/**
* DTO
*/
export interface AdminDto {
adminId: string
username: string
status: string
lastLoginAt: string | null
createdAt: string
}
/**
*
*/
export const adminLogin = async (data: AdminLoginRequest): Promise<AdminLoginResponse> => {
const res = await apiClient.post<ResponseData<AdminLoginResponse>>('/api/admin/auth/login', data)
return res.data.data
}
/**
*
*/
export const getCurrentAdmin = async (): Promise<AdminDto> => {
const res = await apiClient.get<ResponseData<AdminDto>>('/api/admin/auth/me')
return res.data.data
}

View File

@ -0,0 +1,26 @@
import apiClient from './client'
import type { CategoryDto, CreateCategoryRequest, UpdateCategoryRequest } from '@/types/category'
import type { ResponseData } from '@/types/api'
export async function getCategories(): Promise<CategoryDto[]> {
const res = await apiClient.get<ResponseData<CategoryDto[]>>('/api/admin/categories')
return res.data.data
}
export async function getCategoryById(id: string): Promise<CategoryDto> {
const res = await apiClient.get<ResponseData<CategoryDto>>(`/api/admin/categories/${id}`)
return res.data.data
}
export async function createCategory(data: CreateCategoryRequest): Promise<string> {
const res = await apiClient.post<ResponseData<string>>('/api/admin/categories', data)
return res.data.data
}
export async function updateCategory(id: string, data: UpdateCategoryRequest): Promise<void> {
await apiClient.put(`/api/admin/categories/${id}`, { ...data, categoryId: id })
}
export async function deleteCategory(id: string): Promise<void> {
await apiClient.delete(`/api/admin/categories/${id}`)
}

View File

@ -0,0 +1,42 @@
import axios from 'axios'
import type { ResponseData } from '@/types/api'
import { toast } from 'vue-sonner'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('admin_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
apiClient.interceptors.response.use(
(response) => {
const data = response.data as ResponseData
if (data.success === false) {
toast.error(data.message || '操作失败')
return Promise.reject(new Error(data.message || '操作失败'))
}
return response
},
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('admin_token')
window.location.href = '/login'
return Promise.reject(error)
}
const message = error.response?.data?.message || error.message || '网络请求失败'
toast.error(message)
return Promise.reject(error)
},
)
export default apiClient

View File

@ -0,0 +1,34 @@
import apiClient from './client'
import type { GiftDto, CreateGiftRequest, UpdateGiftRequest } from '@/types/gift'
import type { ResponseData } from '@/types/api'
export async function getGifts(): Promise<GiftDto[]> {
const res = await apiClient.get<ResponseData<GiftDto[]>>('/api/admin/gifts')
return res.data.data
}
export async function getGiftById(id: string): Promise<GiftDto> {
const res = await apiClient.get<ResponseData<GiftDto>>(`/api/admin/gifts/${id}`)
return res.data.data
}
export async function createGift(data: CreateGiftRequest): Promise<string> {
const res = await apiClient.post<ResponseData<string>>('/api/admin/gifts', data)
return res.data.data
}
export async function updateGift(id: string, data: UpdateGiftRequest): Promise<void> {
await apiClient.put(`/api/admin/gifts/${id}`, { ...data, giftId: id })
}
export async function putOnShelf(id: string): Promise<void> {
await apiClient.post(`/api/admin/gifts/${id}/putonshelf`, {})
}
export async function putOffShelf(id: string): Promise<void> {
await apiClient.post(`/api/admin/gifts/${id}/putoffshelf`, {})
}
export async function addGiftStock(id: string, quantity: number): Promise<void> {
await apiClient.post(`/api/admin/gifts/${id}/addstock`, { giftId: id, quantity })
}

View File

@ -0,0 +1,19 @@
import apiClient from './client'
import type { GenerateMarketingCodesRequest, GenerateMarketingCodesResponse, MarketingCodeDto, MarketingCodeBatchDto, GetMarketingCodesParams } from '@/types/marketing-code'
import type { ResponseData } from '@/types/api'
export async function generateMarketingCodes(data: GenerateMarketingCodesRequest): Promise<GenerateMarketingCodesResponse> {
const res = await apiClient.post<ResponseData<GenerateMarketingCodesResponse>>('/api/admin/marketing-codes/generate', data)
return res.data.data
}
export async function getMarketingCodes(params: GetMarketingCodesParams): Promise<MarketingCodeDto[]> {
const res = await apiClient.get<ResponseData<MarketingCodeDto[]>>('/api/admin/marketing-codes', { params })
return res.data.data
}
export async function getMarketingCodeBatches(): Promise<MarketingCodeBatchDto[]> {
const res = await apiClient.get<ResponseData<MarketingCodeBatchDto[]>>('/api/admin/marketing-codes/batches')
return res.data.data
}

View File

@ -0,0 +1,8 @@
import apiClient from './client'
import type { MemberDto } from '@/types/member'
import type { ResponseData } from '@/types/api'
export async function getMemberById(memberId: string): Promise<MemberDto> {
const res = await apiClient.get<ResponseData<MemberDto>>(`/api/members/${memberId}`)
return res.data.data
}

View File

@ -0,0 +1,25 @@
import apiClient from './client'
import type { RedemptionOrderDto, DispatchOrderRequest, CancelOrderRequest } from '@/types/order'
import type { ResponseData } from '@/types/api'
export async function getOrders(): Promise<RedemptionOrderDto[]> {
const res = await apiClient.get<ResponseData<RedemptionOrderDto[]>>('/api/admin/redemption-orders')
return res.data.data
}
export async function getOrderById(id: string): Promise<RedemptionOrderDto> {
const res = await apiClient.get<ResponseData<RedemptionOrderDto>>(`/api/admin/redemption-orders/${id}`)
return res.data.data
}
export async function dispatchOrder(data: DispatchOrderRequest): Promise<void> {
await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/dispatch`, data)
}
export async function completeOrder(orderId: string): Promise<void> {
await apiClient.post(`/api/admin/redemption-orders/${orderId}/complete`, { orderId })
}
export async function cancelOrder(data: CancelOrderRequest): Promise<void> {
await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/cancel`, data)
}

View File

@ -0,0 +1,41 @@
import apiClient from './client'
import type {
CreatePointsRuleRequest,
CreatePointsRuleResponse,
PointsRuleDto,
UpdatePointsRuleRequest
} from '@/types/points-rule'
import type { ResponseData } from '@/types/api'
// 创建积分规则
export async function createPointsRule(data: CreatePointsRuleRequest): Promise<CreatePointsRuleResponse> {
const res = await apiClient.post<ResponseData<CreatePointsRuleResponse>>('/api/admin/points-rules', data)
return res.data.data
}
// 获取积分规则列表
export async function getPointsRules(): Promise<PointsRuleDto[]> {
const res = await apiClient.get<ResponseData<PointsRuleDto[]>>('/api/admin/points-rules')
return res.data.data
}
// 根据ID获取积分规则
export async function getPointsRuleById(id: string): Promise<PointsRuleDto> {
const res = await apiClient.get<ResponseData<PointsRuleDto>>(`/api/admin/points-rules/${id}`)
return res.data.data
}
// 更新积分规则
export async function updatePointsRule(id: string, data: UpdatePointsRuleRequest): Promise<void> {
await apiClient.put<ResponseData<object>>(`/api/admin/points-rules/${id}`, data)
}
// 激活积分规则
export async function activatePointsRule(id: string): Promise<void> {
await apiClient.post<ResponseData<object>>(`/api/admin/points-rules/${id}/activate`)
}
// 停用积分规则
export async function deactivatePointsRule(id: string): Promise<void> {
await apiClient.post<ResponseData<object>>(`/api/admin/points-rules/${id}/deactivate`)
}

View File

@ -0,0 +1,26 @@
import apiClient from './client'
import type { ProductDto, CreateProductRequest, UpdateProductRequest } from '@/types/product'
import type { ResponseData } from '@/types/api'
export async function getProducts(): Promise<ProductDto[]> {
const res = await apiClient.get<ResponseData<ProductDto[]>>('/api/admin/products')
return res.data.data
}
export async function getProductById(id: string): Promise<ProductDto> {
const res = await apiClient.get<ResponseData<ProductDto>>(`/api/admin/products/${id}`)
return res.data.data
}
export async function createProduct(data: CreateProductRequest): Promise<string> {
const res = await apiClient.post<ResponseData<string>>('/api/admin/products', data)
return res.data.data
}
export async function updateProduct(id: string, data: UpdateProductRequest): Promise<void> {
await apiClient.put(`/api/admin/products/${id}`, { ...data, productId: id })
}
export async function deleteProduct(id: string): Promise<void> {
await apiClient.delete(`/api/admin/products/${id}`)
}

View File

@ -0,0 +1,17 @@
import apiClient from './client'
import type { ResponseData } from '@/types/api'
export async function uploadImage(file: File, folder?: string): Promise<string> {
const formData = new FormData()
formData.append('File', file)
if (folder) {
formData.append('Folder', folder)
}
const res = await apiClient.post<ResponseData<string>>('/api/admin/upload/image', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return res.data.data
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

Some files were not shown because too many files have changed in this diff Show More