diff --git a/Backend/Directory.Packages.props b/Backend/Directory.Packages.props index 0d3de24..44a6402 100644 --- a/Backend/Directory.Packages.props +++ b/Backend/Directory.Packages.props @@ -2,7 +2,6 @@ true - 3.2.1 @@ -13,14 +12,12 @@ 1.0.5 1.1.2 - - @@ -39,7 +36,6 @@ - @@ -50,14 +46,12 @@ - - - + @@ -70,7 +64,6 @@ - @@ -98,7 +91,6 @@ - @@ -126,7 +118,6 @@ - @@ -145,8 +136,7 @@ - - + \ No newline at end of file diff --git a/Backend/dotnet-tools.json b/Backend/dotnet-tools.json new file mode 100644 index 0000000..bffb60c --- /dev/null +++ b/Backend/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.3", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/Admin.cs b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/Admin.cs new file mode 100644 index 0000000..dca47e3 --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/Admin.cs @@ -0,0 +1,117 @@ +using Fengling.Backend.Domain.DomainEvents; + +namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +/// +/// 管理员ID +/// +public partial record AdminId : IGuidStronglyTypedId; + +/// +/// 管理员聚合根 +/// +public class Admin : Entity, IAggregateRoot +{ + protected Admin() { } + + private Admin(string username, string passwordHash) + { + Username = username; + PasswordHash = passwordHash; + Status = AdminStatus.Active; + CreatedAt = DateTime.UtcNow; + + this.AddDomainEvent(new AdminCreatedDomainEvent(this)); + } + + /// + /// 用户名 + /// + public string Username { get; private set; } = string.Empty; + + /// + /// 密码哈希 + /// + public string PasswordHash { get; private set; } = string.Empty; + + /// + /// 管理员状态 + /// + public AdminStatus Status { get; private set; } + + /// + /// 最后登录时间 + /// + public DateTime? LastLoginAt { get; private set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; private set; } + + /// + /// 软删除标记 + /// + public Deleted Deleted { get; private set; } = new(); + + /// + /// 行版本 + /// + public RowVersion RowVersion { get; private set; } = new(0); + + /// + /// 创建管理员 + /// + 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); + } + + /// + /// 验证密码 + /// + public bool VerifyPassword(string password) + { + return PasswordHelper.VerifyPassword(password, PasswordHash); + } + + /// + /// 记录登录 + /// + public void RecordLogin() + { + LastLoginAt = DateTime.UtcNow; + this.AddDomainEvent(new AdminLoggedInDomainEvent(this)); + } + + /// + /// 禁用管理员 + /// + public void Disable() + { + if (Status == AdminStatus.Disabled) + return; + + Status = AdminStatus.Disabled; + this.AddDomainEvent(new AdminDisabledDomainEvent(this)); + } + + /// + /// 启用管理员 + /// + public void Enable() + { + if (Status == AdminStatus.Active) + return; + + Status = AdminStatus.Active; + this.AddDomainEvent(new AdminEnabledDomainEvent(this)); + } +} diff --git a/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/AdminStatus.cs b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/AdminStatus.cs new file mode 100644 index 0000000..f48fa2f --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/AdminStatus.cs @@ -0,0 +1,17 @@ +namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +/// +/// 管理员状态 +/// +public enum AdminStatus +{ + /// + /// 正常 + /// + Active = 1, + + /// + /// 禁用 + /// + Disabled = 2 +} diff --git a/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/PasswordHelper.cs b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/PasswordHelper.cs new file mode 100644 index 0000000..4cdb9b2 --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/AggregatesModel/AdminAggregate/PasswordHelper.cs @@ -0,0 +1,36 @@ +namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +/// +/// 密码加密工具类 +/// +public static class PasswordHelper +{ + /// + /// 加密密码 + /// + public static string HashPassword(string password) + { + if (string.IsNullOrWhiteSpace(password)) + throw new ArgumentException("密码不能为空", nameof(password)); + + return BCrypt.Net.BCrypt.HashPassword(password); + } + + /// + /// 验证密码 + /// + 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; + } + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Domain/AggregatesModel/CategoryAggregate/Category.cs b/Backend/src/Fengling.Backend.Domain/AggregatesModel/CategoryAggregate/Category.cs new file mode 100644 index 0000000..9040387 --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/AggregatesModel/CategoryAggregate/Category.cs @@ -0,0 +1,111 @@ +namespace Fengling.Backend.Domain.AggregatesModel.CategoryAggregate; + +/// +/// 品类ID +/// +public partial record CategoryId : IGuidStronglyTypedId; + +/// +/// 品类聚合根 +/// +public class Category : Entity, 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; + } + + /// + /// 品类名称 + /// + public string Name { get; private set; } = string.Empty; + + /// + /// 品类编码(唯一) + /// + public string Code { get; private set; } = string.Empty; + + /// + /// 描述 + /// + public string Description { get; private set; } = string.Empty; + + /// + /// 排序 + /// + public int SortOrder { get; private set; } + + /// + /// 是否激活 + /// + public bool IsActive { get; private set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; private set; } + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; private set; } + + public Deleted Deleted { get; private set; } = new(); + public RowVersion RowVersion { get; private set; } = new(0); + + /// + /// 更新品类信息 + /// + 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; + } + + /// + /// 激活 + /// + public void Activate() + { + if (!IsActive) + { + IsActive = true; + UpdatedAt = DateTime.UtcNow; + } + } + + /// + /// 停用 + /// + public void Deactivate() + { + if (IsActive) + { + IsActive = false; + UpdatedAt = DateTime.UtcNow; + } + } +} diff --git a/Backend/src/Fengling.Backend.Domain/AggregatesModel/ProductAggregate/Product.cs b/Backend/src/Fengling.Backend.Domain/AggregatesModel/ProductAggregate/Product.cs new file mode 100644 index 0000000..d6b8bea --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/AggregatesModel/ProductAggregate/Product.cs @@ -0,0 +1,116 @@ +namespace Fengling.Backend.Domain.AggregatesModel.ProductAggregate; + +/// +/// 产品ID +/// +public partial record ProductId : IGuidStronglyTypedId; + +/// +/// 产品聚合根 +/// +public class Product : Entity, 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; + } + + /// + /// 产品名称 + /// + public string Name { get; private set; } = string.Empty; + + /// + /// 品类ID + /// + public Guid CategoryId { get; private set; } + + /// + /// 品类名称 + /// + public string CategoryName { get; private set; } = string.Empty; + + /// + /// 描述 + /// + public string Description { get; private set; } = string.Empty; + + /// + /// 是否激活 + /// + public bool IsActive { get; private set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; private set; } + + /// + /// 更新时间 + /// + public DateTime UpdatedAt { get; private set; } + + public Deleted Deleted { get; private set; } = new(); + public RowVersion RowVersion { get; private set; } = new(0); + + /// + /// 更新产品信息 + /// + 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; + } + + /// + /// 激活 + /// + public void Activate() + { + if (!IsActive) + { + IsActive = true; + UpdatedAt = DateTime.UtcNow; + } + } + + /// + /// 停用 + /// + public void Deactivate() + { + if (IsActive) + { + IsActive = false; + UpdatedAt = DateTime.UtcNow; + } + } +} diff --git a/Backend/src/Fengling.Backend.Domain/DomainEvents/AdminDomainEvents.cs b/Backend/src/Fengling.Backend.Domain/DomainEvents/AdminDomainEvents.cs new file mode 100644 index 0000000..24efbfb --- /dev/null +++ b/Backend/src/Fengling.Backend.Domain/DomainEvents/AdminDomainEvents.cs @@ -0,0 +1,23 @@ +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +namespace Fengling.Backend.Domain.DomainEvents; + +/// +/// 管理员创建领域事件 +/// +public record AdminCreatedDomainEvent(Admin Admin) : IDomainEvent; + +/// +/// 管理员登录领域事件 +/// +public record AdminLoggedInDomainEvent(Admin Admin) : IDomainEvent; + +/// +/// 管理员禁用领域事件 +/// +public record AdminDisabledDomainEvent(Admin Admin) : IDomainEvent; + +/// +/// 管理员启用领域事件 +/// +public record AdminEnabledDomainEvent(Admin Admin) : IDomainEvent; diff --git a/Backend/src/Fengling.Backend.Domain/Fengling.Backend.Domain.csproj b/Backend/src/Fengling.Backend.Domain/Fengling.Backend.Domain.csproj index 1ab15ae..b04e537 100644 --- a/Backend/src/Fengling.Backend.Domain/Fengling.Backend.Domain.csproj +++ b/Backend/src/Fengling.Backend.Domain/Fengling.Backend.Domain.csproj @@ -7,6 +7,7 @@ true + diff --git a/Backend/src/Fengling.Backend.Infrastructure/ApplicationDbContext.cs b/Backend/src/Fengling.Backend.Infrastructure/ApplicationDbContext.cs index ebc34a4..85ebb21 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/ApplicationDbContext.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/ApplicationDbContext.cs @@ -1,12 +1,15 @@ using MediatR; using Microsoft.EntityFrameworkCore; using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence; +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate; using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate; using Fengling.Backend.Domain.AggregatesModel.GiftAggregate; using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate; +using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate; +using Fengling.Backend.Domain.AggregatesModel.ProductAggregate; namespace Fengling.Backend.Infrastructure; @@ -14,6 +17,9 @@ public partial class ApplicationDbContext(DbContextOptions : AppDbContextBase(options, mediator) , ISqliteCapDataStorage { + // 管理员聚合 + public DbSet Admins => Set(); + // 会员聚合 public DbSet Members => Set(); public DbSet PointsTransactions => Set(); @@ -30,6 +36,12 @@ public partial class ApplicationDbContext(DbContextOptions // 兑换订单聚合 public DbSet RedemptionOrders => Set(); + // 品类聚合 + public DbSet Categories => Set(); + + // 产品聚合 + public DbSet Products => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { if (modelBuilder is null) diff --git a/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/AdminEntityTypeConfiguration.cs b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/AdminEntityTypeConfiguration.cs new file mode 100644 index 0000000..f729be5 --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/AdminEntityTypeConfiguration.cs @@ -0,0 +1,49 @@ +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +namespace Fengling.Backend.Infrastructure.EntityConfigurations; + +/// +/// 管理员实体配置 +/// +public class AdminEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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"); + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs new file mode 100644 index 0000000..7fd2dbd --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/CategoryEntityTypeConfiguration.cs @@ -0,0 +1,51 @@ +using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate; + +namespace Fengling.Backend.Infrastructure.EntityConfigurations; + +public class CategoryEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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"); + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/PointsTransactionEntityTypeConfiguration.cs b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/PointsTransactionEntityTypeConfiguration.cs index 2a94a8b..daebedb 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/PointsTransactionEntityTypeConfiguration.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/PointsTransactionEntityTypeConfiguration.cs @@ -48,7 +48,7 @@ public class PointsTransactionEntityTypeConfiguration : IEntityTypeConfiguration // 索引 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.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt"); } diff --git a/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs new file mode 100644 index 0000000..94521b2 --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/EntityConfigurations/ProductEntityTypeConfiguration.cs @@ -0,0 +1,50 @@ +using Fengling.Backend.Domain.AggregatesModel.ProductAggregate; + +namespace Fengling.Backend.Infrastructure.EntityConfigurations; + +public class ProductEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder 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"); + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.Designer.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.Designer.cs similarity index 85% rename from Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.Designer.cs rename to Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.Designer.cs index 3754614..f7fad15 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.Designer.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Fengling.Backend.Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20260211044819_Init")] + [Migration("20260211061437_Init")] partial class Init { /// @@ -355,7 +355,6 @@ namespace Fengling.Backend.Infrastructure.Migrations .HasDatabaseName("IX_PointsTransactions_MemberId"); b.HasIndex("RelatedId") - .IsUnique() .HasDatabaseName("IX_PointsTransactions_RelatedId"); b.HasIndex("Type") @@ -449,6 +448,112 @@ namespace Fengling.Backend.Infrastructure.Migrations b.ToTable("RedemptionOrders", (string)null); }); + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Instance") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastLockTime") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("CAPLock", (string)null); + }); + + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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 => diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.cs similarity index 80% rename from Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.cs rename to Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.cs index f2ddc9d..19d2a1d 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211044819_Init.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211061437_Init.cs @@ -11,6 +11,58 @@ namespace Fengling.Backend.Infrastructure.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.CreateTable( + name: "CAPLock", + columns: table => new + { + Key = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Instance = table.Column(type: "TEXT", maxLength: 256, nullable: true), + LastLockTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CAPLock", x => x.Key); + }); + + migrationBuilder.CreateTable( + name: "CAPPublishedMessage", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Version = table.Column(type: "TEXT", maxLength: 20, nullable: true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Content = table.Column(type: "TEXT", nullable: true), + Retries = table.Column(type: "INTEGER", nullable: true), + Added = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: true), + StatusName = table.Column(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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Version = table.Column(type: "TEXT", maxLength: 20, nullable: true), + Name = table.Column(type: "TEXT", maxLength: 400, nullable: false), + Group = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Content = table.Column(type: "TEXT", nullable: true), + Retries = table.Column(type: "INTEGER", nullable: true), + Added = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: true), + StatusName = table.Column(type: "TEXT", maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Gifts", columns: table => new @@ -160,6 +212,26 @@ namespace Fengling.Backend.Infrastructure.Migrations 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( name: "IX_Gifts_IsOnShelf", table: "Gifts", @@ -235,8 +307,7 @@ namespace Fengling.Backend.Infrastructure.Migrations migrationBuilder.CreateIndex( name: "IX_PointsTransactions_RelatedId", table: "PointsTransactions", - column: "RelatedId", - unique: true); + column: "RelatedId"); migrationBuilder.CreateIndex( name: "IX_PointsTransactions_Type", @@ -268,6 +339,15 @@ namespace Fengling.Backend.Infrastructure.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "CAPLock"); + + migrationBuilder.DropTable( + name: "CAPPublishedMessage"); + + migrationBuilder.DropTable( + name: "CAPReceivedMessage"); + migrationBuilder.DropTable( name: "Gifts"); diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.Designer.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.Designer.cs new file mode 100644 index 0000000..f19be80 --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.Designer.cs @@ -0,0 +1,753 @@ +// +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 + { + /// + 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("Id") + .HasColumnType("TEXT") + .HasComment("管理员ID"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("LastLoginAt") + .HasColumnType("TEXT") + .HasComment("最后登录时间"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComment("密码哈希"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("管理员状态(1=Active,2=Disabled)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("礼品ID"); + + b.Property("AvailableStock") + .HasColumnType("INTEGER") + .HasComment("可用库存"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("图片URL"); + + b.Property("IsOnShelf") + .HasColumnType("INTEGER") + .HasComment("是否上架"); + + b.Property("LimitPerMember") + .HasColumnType("INTEGER") + .HasComment("每人限兑数量"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("礼品名称"); + + b.Property("RequiredPoints") + .HasColumnType("INTEGER") + .HasComment("所需积分"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasComment("排序"); + + b.Property("TotalStock") + .HasColumnType("INTEGER") + .HasComment("总库存"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("营销码ID"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("批次号"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("营销码"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT") + .HasComment("过期时间"); + + b.Property("IsUsed") + .HasColumnType("INTEGER") + .HasComment("是否已使用"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UsedAt") + .HasColumnType("TEXT") + .HasComment("使用时间"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("AvailablePoints") + .HasColumnType("INTEGER") + .HasComment("可用积分"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("昵称"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("密码(已加密)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasComment("手机号"); + + b.Property("RegisteredAt") + .HasColumnType("TEXT") + .HasComment("注册时间"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("状态(1:正常,2:禁用)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("积分规则ID"); + + b.Property("BonusMultiplier") + .HasColumnType("TEXT") + .HasComment("奖励倍数"); + + b.Property("CategoryId") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT") + .HasComment("生效结束时间"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("MemberLevelCode") + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasComment("会员等级编码"); + + b.Property("PointsValue") + .HasColumnType("INTEGER") + .HasComment("积分值"); + + b.Property("ProductId") + .HasColumnType("TEXT") + .HasComment("产品ID"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("RuleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("规则名称"); + + b.Property("RuleType") + .HasColumnType("INTEGER") + .HasComment("规则类型(1:产品,2:时间,3:会员等级)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("积分流水ID"); + + b.Property("Amount") + .HasColumnType("INTEGER") + .HasComment("积分数量"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT") + .HasComment("过期时间"); + + b.Property("MemberId") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasComment("原因描述"); + + b.Property("RelatedId") + .HasColumnType("TEXT") + .HasComment("关联ID"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("来源"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("兑换订单ID"); + + b.Property("CancelReason") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("取消原因"); + + b.Property("ConsumedPoints") + .HasColumnType("INTEGER") + .HasComment("消耗积分"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("GiftId") + .HasColumnType("TEXT") + .HasComment("礼品ID"); + + b.Property("GiftName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("礼品名称"); + + b.Property("GiftType") + .HasColumnType("INTEGER") + .HasComment("礼品类型"); + + b.Property("MemberId") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("订单号"); + + b.Property("Quantity") + .HasColumnType("INTEGER") + .HasComment("数量"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)"); + + b.Property("TrackingNo") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("物流单号"); + + b.Property("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("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Instance") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastLockTime") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("CAPLock", (string)null); + }); + + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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("MarketingCodeId") + .HasColumnType("TEXT"); + + b1.Property("CategoryId") + .HasColumnType("TEXT") + .HasColumnName("CategoryId") + .HasComment("品类ID"); + + b1.Property("CategoryName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("CategoryName") + .HasComment("品类名称"); + + b1.Property("ProductId") + .HasColumnType("TEXT") + .HasColumnName("ProductId") + .HasComment("产品ID"); + + b1.Property("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("MemberId") + .HasColumnType("TEXT"); + + b1.Property("BonusRate") + .HasColumnType("TEXT") + .HasColumnName("BonusRate") + .HasComment("积分奖励倍率"); + + b1.Property("LevelCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("LevelCode") + .HasComment("等级编码"); + + b1.Property("LevelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("LevelName") + .HasComment("等级名称"); + + b1.Property("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("RedemptionOrderId") + .HasColumnType("TEXT"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("City") + .HasComment("市"); + + b1.Property("DetailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("DetailAddress") + .HasComment("详细地址"); + + b1.Property("District") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("District") + .HasComment("区/县"); + + b1.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("ReceiverPhone") + .HasComment("联系电话"); + + b1.Property("Province") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("Province") + .HasComment("省"); + + b1.Property("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 + } + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.cs new file mode 100644 index 0000000..39d8f9d --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fengling.Backend.Infrastructure.Migrations +{ + /// + public partial class AddAdminAggregate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Admins", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, comment: "管理员ID"), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: false, comment: "用户名"), + PasswordHash = table.Column(type: "TEXT", maxLength: 255, nullable: false, comment: "密码哈希"), + Status = table.Column(type: "INTEGER", nullable: false, comment: "管理员状态(1=Active,2=Disabled)"), + LastLoginAt = table.Column(type: "TEXT", nullable: true, comment: "最后登录时间"), + CreatedAt = table.Column(type: "TEXT", nullable: false, comment: "创建时间"), + Deleted = table.Column(type: "INTEGER", nullable: false), + RowVersion = table.Column(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); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Admins"); + } + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.Designer.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.Designer.cs new file mode 100644 index 0000000..94ae82a --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.Designer.cs @@ -0,0 +1,873 @@ +// +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 + { + /// + 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("Id") + .HasColumnType("TEXT") + .HasComment("管理员ID"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("LastLoginAt") + .HasColumnType("TEXT") + .HasComment("最后登录时间"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComment("密码哈希"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("管理员状态(1=Active,2=Disabled)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("品类编码"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("品类名称"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasComment("排序"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("礼品ID"); + + b.Property("AvailableStock") + .HasColumnType("INTEGER") + .HasComment("可用库存"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("ImageUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("图片URL"); + + b.Property("IsOnShelf") + .HasColumnType("INTEGER") + .HasComment("是否上架"); + + b.Property("LimitPerMember") + .HasColumnType("INTEGER") + .HasComment("每人限兑数量"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("礼品名称"); + + b.Property("RequiredPoints") + .HasColumnType("INTEGER") + .HasComment("所需积分"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasComment("排序"); + + b.Property("TotalStock") + .HasColumnType("INTEGER") + .HasComment("总库存"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("营销码ID"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("批次号"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("营销码"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT") + .HasComment("过期时间"); + + b.Property("IsUsed") + .HasColumnType("INTEGER") + .HasComment("是否已使用"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UsedAt") + .HasColumnType("TEXT") + .HasComment("使用时间"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("AvailablePoints") + .HasColumnType("INTEGER") + .HasComment("可用积分"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("昵称"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("密码(已加密)"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasComment("手机号"); + + b.Property("RegisteredAt") + .HasColumnType("TEXT") + .HasComment("注册时间"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("状态(1:正常,2:禁用)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("积分规则ID"); + + b.Property("BonusMultiplier") + .HasColumnType("TEXT") + .HasComment("奖励倍数"); + + b.Property("CategoryId") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT") + .HasComment("生效结束时间"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("MemberLevelCode") + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasComment("会员等级编码"); + + b.Property("PointsValue") + .HasColumnType("INTEGER") + .HasComment("积分值"); + + b.Property("ProductId") + .HasColumnType("TEXT") + .HasComment("产品ID"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("RuleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("规则名称"); + + b.Property("RuleType") + .HasColumnType("INTEGER") + .HasComment("规则类型(1:产品,2:时间,3:会员等级)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("积分流水ID"); + + b.Property("Amount") + .HasColumnType("INTEGER") + .HasComment("积分数量"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("ExpiryDate") + .HasColumnType("TEXT") + .HasComment("过期时间"); + + b.Property("MemberId") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasComment("原因描述"); + + b.Property("RelatedId") + .HasColumnType("TEXT") + .HasComment("关联ID"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("来源"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("产品ID"); + + b.Property("CategoryId") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("品类名称"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("产品名称"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("兑换订单ID"); + + b.Property("CancelReason") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("取消原因"); + + b.Property("ConsumedPoints") + .HasColumnType("INTEGER") + .HasComment("消耗积分"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("GiftId") + .HasColumnType("TEXT") + .HasComment("礼品ID"); + + b.Property("GiftName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("礼品名称"); + + b.Property("GiftType") + .HasColumnType("INTEGER") + .HasComment("礼品类型"); + + b.Property("MemberId") + .HasColumnType("TEXT") + .HasComment("会员ID"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("订单号"); + + b.Property("Quantity") + .HasColumnType("INTEGER") + .HasComment("数量"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)"); + + b.Property("TrackingNo") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("物流单号"); + + b.Property("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("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Instance") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastLockTime") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("CAPLock", (string)null); + }); + + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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("MarketingCodeId") + .HasColumnType("TEXT"); + + b1.Property("CategoryId") + .HasColumnType("TEXT") + .HasColumnName("CategoryId") + .HasComment("品类ID"); + + b1.Property("CategoryName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("CategoryName") + .HasComment("品类名称"); + + b1.Property("ProductId") + .HasColumnType("TEXT") + .HasColumnName("ProductId") + .HasComment("产品ID"); + + b1.Property("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("MemberId") + .HasColumnType("TEXT"); + + b1.Property("BonusRate") + .HasColumnType("TEXT") + .HasColumnName("BonusRate") + .HasComment("积分奖励倍率"); + + b1.Property("LevelCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("LevelCode") + .HasComment("等级编码"); + + b1.Property("LevelName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("LevelName") + .HasComment("等级名称"); + + b1.Property("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("RedemptionOrderId") + .HasColumnType("TEXT"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("City") + .HasComment("市"); + + b1.Property("DetailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("DetailAddress") + .HasComment("详细地址"); + + b1.Property("District") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("District") + .HasComment("区/县"); + + b1.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("ReceiverPhone") + .HasComment("联系电话"); + + b1.Property("Province") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("Province") + .HasComment("省"); + + b1.Property("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 + } + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.cs new file mode 100644 index 0000000..6f90b00 --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211084650_AddProductAndCategoryAggregates.cs @@ -0,0 +1,91 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fengling.Backend.Infrastructure.Migrations +{ + /// + public partial class AddProductAndCategoryAggregates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, comment: "品类ID"), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"), + Code = table.Column(type: "TEXT", maxLength: 50, nullable: false, comment: "品类编码"), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"), + SortOrder = table.Column(type: "INTEGER", nullable: false, comment: "排序"), + IsActive = table.Column(type: "INTEGER", nullable: false, comment: "是否激活"), + CreatedAt = table.Column(type: "TEXT", nullable: false, comment: "创建时间"), + UpdatedAt = table.Column(type: "TEXT", nullable: false, comment: "更新时间"), + Deleted = table.Column(type: "INTEGER", nullable: false), + RowVersion = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false, comment: "产品ID"), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false, comment: "产品名称"), + CategoryId = table.Column(type: "TEXT", nullable: false, comment: "品类ID"), + CategoryName = table.Column(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"), + IsActive = table.Column(type: "INTEGER", nullable: false, comment: "是否激活"), + CreatedAt = table.Column(type: "TEXT", nullable: false, comment: "创建时间"), + UpdatedAt = table.Column(type: "TEXT", nullable: false, comment: "更新时间"), + Deleted = table.Column(type: "INTEGER", nullable: false), + RowVersion = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Products"); + } + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/Backend/src/Fengling.Backend.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index c88d28f..96bea0d 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,6 +17,117 @@ namespace Fengling.Backend.Infrastructure.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasComment("管理员ID"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("LastLoginAt") + .HasColumnType("TEXT") + .HasComment("最后登录时间"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComment("密码哈希"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER") + .HasComment("管理员状态(1=Active,2=Disabled)"); + + b.Property("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("Id") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasComment("品类编码"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("品类名称"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER") + .HasComment("排序"); + + b.Property("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("Id") @@ -352,7 +463,6 @@ namespace Fengling.Backend.Infrastructure.Migrations .HasDatabaseName("IX_PointsTransactions_MemberId"); b.HasIndex("RelatedId") - .IsUnique() .HasDatabaseName("IX_PointsTransactions_RelatedId"); b.HasIndex("Type") @@ -361,6 +471,64 @@ namespace Fengling.Backend.Infrastructure.Migrations b.ToTable("PointsTransactions", (string)null); }); + modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.ProductAggregate.Product", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasComment("产品ID"); + + b.Property("CategoryId") + .HasColumnType("TEXT") + .HasComment("品类ID"); + + b.Property("CategoryName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("品类名称"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasComment("创建时间"); + + b.Property("Deleted") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasComment("描述"); + + b.Property("IsActive") + .HasColumnType("INTEGER") + .HasComment("是否激活"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasComment("产品名称"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("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("Id") @@ -446,6 +614,112 @@ namespace Fengling.Backend.Infrastructure.Migrations b.ToTable("RedemptionOrders", (string)null); }); + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b => + { + b.Property("Key") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Instance") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastLockTime") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("CAPLock", (string)null); + }); + + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Added") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("TEXT"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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 => diff --git a/Backend/src/Fengling.Backend.Infrastructure/Repositories/AdminRepository.cs b/Backend/src/Fengling.Backend.Infrastructure/Repositories/AdminRepository.cs new file mode 100644 index 0000000..3f366a2 --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Repositories/AdminRepository.cs @@ -0,0 +1,48 @@ +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; + +namespace Fengling.Backend.Infrastructure.Repositories; + +/// +/// 管理员仓储接口 +/// +public interface IAdminRepository : IRepository +{ + /// + /// 通过用户名获取管理员 + /// + Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default); + + /// + /// 检查用户名是否存在 + /// + Task ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default); + + /// + /// 检查是否存在任何管理员 + /// + Task AnyAsync(CancellationToken cancellationToken = default); +} + +/// +/// 管理员仓储实现 +/// +public class AdminRepository(ApplicationDbContext context) + : RepositoryBase(context), IAdminRepository +{ + public async Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default) + { + return await DbContext.Admins + .FirstOrDefaultAsync(x => x.Username == username, cancellationToken); + } + + public async Task ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default) + { + return await DbContext.Admins + .AnyAsync(x => x.Username == username, cancellationToken); + } + + public async Task AnyAsync(CancellationToken cancellationToken = default) + { + return await DbContext.Admins.AnyAsync(cancellationToken); + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Repositories/CategoryRepository.cs b/Backend/src/Fengling.Backend.Infrastructure/Repositories/CategoryRepository.cs new file mode 100644 index 0000000..0ee10cb --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Repositories/CategoryRepository.cs @@ -0,0 +1,25 @@ +using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate; + +namespace Fengling.Backend.Infrastructure.Repositories; + +public interface ICategoryRepository : IRepository +{ + Task GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default); + Task GetByCodeAsync(string code, CancellationToken cancellationToken = default); +} + +public class CategoryRepository(ApplicationDbContext context) + : RepositoryBase(context), ICategoryRepository +{ + public async Task GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default) + { + return await DbContext.Categories + .FirstOrDefaultAsync(x => x.Id == categoryId, cancellationToken); + } + + public async Task GetByCodeAsync(string code, CancellationToken cancellationToken = default) + { + return await DbContext.Categories + .FirstOrDefaultAsync(x => x.Code == code, cancellationToken); + } +} diff --git a/Backend/src/Fengling.Backend.Infrastructure/Repositories/PointsRuleRepository.cs b/Backend/src/Fengling.Backend.Infrastructure/Repositories/PointsRuleRepository.cs index b7eff08..0f213ce 100644 --- a/Backend/src/Fengling.Backend.Infrastructure/Repositories/PointsRuleRepository.cs +++ b/Backend/src/Fengling.Backend.Infrastructure/Repositories/PointsRuleRepository.cs @@ -26,7 +26,8 @@ public interface IPointsRuleRepository : IRepository string? memberLevelCode, DateTime startDate, DateTime? endDate, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default, + PointsRuleId? excludeRuleId = null); } /// @@ -57,10 +58,17 @@ public class PointsRuleRepository(ApplicationDbContext context) string? memberLevelCode, DateTime startDate, DateTime? endDate, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + PointsRuleId? excludeRuleId = null) { var query = DbContext.PointsRules.AsQueryable(); + // 排除特定规则 + if (excludeRuleId != null) + { + query = query.Where(x => x.Id != excludeRuleId); + } + // 检查维度是否完全一致 if (productId.HasValue) query = query.Where(x => x.ProductId == productId); @@ -78,8 +86,9 @@ public class PointsRuleRepository(ApplicationDbContext context) query = query.Where(x => x.MemberLevelCode == null); // 检查时间重叠 + var effectiveEndDate = endDate ?? DateTime.MaxValue; query = query.Where(x => - x.StartDate <= (endDate ?? DateTime.MaxValue) && + x.StartDate <= effectiveEndDate && (x.EndDate == null || x.EndDate >= startDate)); return await query.AnyAsync(cancellationToken); diff --git a/Backend/src/Fengling.Backend.Infrastructure/Repositories/ProductRepository.cs b/Backend/src/Fengling.Backend.Infrastructure/Repositories/ProductRepository.cs new file mode 100644 index 0000000..f85353b --- /dev/null +++ b/Backend/src/Fengling.Backend.Infrastructure/Repositories/ProductRepository.cs @@ -0,0 +1,25 @@ +using Fengling.Backend.Domain.AggregatesModel.ProductAggregate; + +namespace Fengling.Backend.Infrastructure.Repositories; + +public interface IProductRepository : IRepository +{ + Task GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default); + Task GetByNameAsync(string name, CancellationToken cancellationToken = default); +} + +public class ProductRepository(ApplicationDbContext context) + : RepositoryBase(context), IProductRepository +{ + public async Task GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default) + { + return await DbContext.Products + .FirstOrDefaultAsync(x => x.Id == productId, cancellationToken); + } + + public async Task GetByNameAsync(string name, CancellationToken cancellationToken = default) + { + return await DbContext.Products + .FirstOrDefaultAsync(x => x.Name == name, cancellationToken); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/AdminLoginCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/AdminLoginCommand.cs new file mode 100644 index 0000000..647af79 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/AdminLoginCommand.cs @@ -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; + +/// +/// 管理员登录命令 +/// +public record AdminLoginCommand(string Username, string Password) : ICommand; + +/// +/// 管理员登录响应 +/// +public record AdminLoginResponse(AdminId AdminId, string Username, string Token, DateTime ExpiresAt); + +/// +/// 管理员登录命令验证器 +/// +public class AdminLoginCommandValidator : AbstractValidator +{ + public AdminLoginCommandValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("用户名不能为空") + .MaximumLength(50).WithMessage("用户名长度不能超过50个字符"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空"); + } +} + +/// +/// 管理员登录命令处理器 +/// +public class AdminLoginCommandHandler( + IAdminRepository adminRepository, + IConfiguration configuration) + : ICommandHandler +{ + public async Task 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() + ?? 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); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/InitializeDefaultAdminCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/InitializeDefaultAdminCommand.cs new file mode 100644 index 0000000..e133a0a --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/AdminAuth/InitializeDefaultAdminCommand.cs @@ -0,0 +1,39 @@ +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.AdminAuth; + +/// +/// 初始化默认管理员命令 +/// +public record InitializeDefaultAdminCommand : ICommand; + +/// +/// 初始化默认管理员命令处理器 +/// +public class InitializeDefaultAdminCommandHandler( + IAdminRepository adminRepository, + IConfiguration configuration, + ILogger logger) + : ICommandHandler +{ + public async Task Handle(InitializeDefaultAdminCommand command, CancellationToken cancellationToken) + { + // 检查是否已存在管理员 + if (await adminRepository.AnyAsync(cancellationToken)) + { + logger.LogInformation("管理员账号已存在,跳过初始化"); + return; + } + + // 从配置读取默认管理员信息 + var username = configuration.GetValue("AppConfiguration:DefaultAdmin:Username") ?? "admin"; + var password = configuration.GetValue("AppConfiguration:DefaultAdmin:Password") ?? "Admin@123"; + + // 创建默认管理员 + var admin = Admin.Create(username, password); + await adminRepository.AddAsync(admin, cancellationToken); + + logger.LogInformation("默认管理员账号已初始化, 用户名: {Username}", username); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/Categories/CategoryCommands.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/Categories/CategoryCommands.cs new file mode 100644 index 0000000..16ac85a --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/Categories/CategoryCommands.cs @@ -0,0 +1,99 @@ +using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.Categories; + +/// +/// 创建品类命令 +/// +public record CreateCategoryCommand( + string Name, + string Code, + string? Description = null, + int SortOrder = 0) : ICommand; + +public class CreateCategoryCommandValidator : AbstractValidator +{ + 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 +{ + public async Task 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; + } +} + +/// +/// 更新品类命令 +/// +public record UpdateCategoryCommand( + CategoryId CategoryId, + string? Name = null, + string? Description = null, + int? SortOrder = null) : ICommand; + +public class UpdateCategoryCommandValidator : AbstractValidator +{ + 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 +{ + public async Task 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(); + } +} + +/// +/// 删除品类命令 +/// +public record DeleteCategoryCommand(CategoryId CategoryId) : ICommand; + +public class DeleteCategoryCommandHandler(ICategoryRepository categoryRepository) + : ICommandHandler +{ + public async Task 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(); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/Gifts/GiftCommands.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/Gifts/GiftCommands.cs index 4ce7008..67de681 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Commands/Gifts/GiftCommands.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/Gifts/GiftCommands.cs @@ -8,7 +8,7 @@ namespace Fengling.Backend.Web.Application.Commands.Gifts; /// public record CreateGiftCommand( string Name, - int Type, + GiftType Type, string Description, string ImageUrl, int RequiredPoints, @@ -20,7 +20,7 @@ public class CreateGiftCommandValidator : AbstractValidator public CreateGiftCommandValidator() { 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.ImageUrl).NotEmpty().MaximumLength(500); RuleFor(x => x.RequiredPoints).GreaterThan(0); @@ -33,11 +33,9 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository) { public async Task Handle(CreateGiftCommand request, CancellationToken cancellationToken) { - var giftType = (GiftType)request.Type; - var gift = new Gift( request.Name, - giftType, + request.Type, request.Description, request.ImageUrl, request.RequiredPoints, @@ -54,7 +52,7 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository) /// 更新礼品命令 /// public record UpdateGiftCommand( - Guid GiftId, + GiftId GiftId, string? Name = null, string? Description = null, string? ImageUrl = null, @@ -78,8 +76,7 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository) { public async Task Handle(UpdateGiftCommand request, CancellationToken cancellationToken) { - var giftId = new GiftId(request.GiftId); - var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken); + var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken); if (gift == null) throw new KnownException("礼品不存在"); @@ -100,15 +97,14 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository) /// /// 上架礼品命令 /// -public record PutOnShelfCommand(Guid GiftId) : ICommand; +public record PutOnShelfCommand(GiftId GiftId) : ICommand; public class PutOnShelfCommandHandler(IGiftRepository giftRepository) : ICommandHandler { public async Task Handle(PutOnShelfCommand request, CancellationToken cancellationToken) { - var giftId = new GiftId(request.GiftId); - var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken); + var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken); if (gift == null) throw new KnownException("礼品不存在"); @@ -123,15 +119,14 @@ public class PutOnShelfCommandHandler(IGiftRepository giftRepository) /// /// 下架礼品命令 /// -public record PutOffShelfCommand(Guid GiftId) : ICommand; +public record PutOffShelfCommand(GiftId GiftId) : ICommand; public class PutOffShelfCommandHandler(IGiftRepository giftRepository) : ICommandHandler { public async Task Handle(PutOffShelfCommand request, CancellationToken cancellationToken) { - var giftId = new GiftId(request.GiftId); - var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken); + var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken); if (gift == null) throw new KnownException("礼品不存在"); @@ -146,7 +141,7 @@ public class PutOffShelfCommandHandler(IGiftRepository giftRepository) /// /// 增加库存命令 /// -public record AddGiftStockCommand(Guid GiftId, int Quantity) : ICommand; +public record AddGiftStockCommand(GiftId GiftId, int Quantity) : ICommand; public class AddGiftStockCommandValidator : AbstractValidator { @@ -162,8 +157,7 @@ public class AddGiftStockCommandHandler(IGiftRepository giftRepository) { public async Task Handle(AddGiftStockCommand request, CancellationToken cancellationToken) { - var giftId = new GiftId(request.GiftId); - var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken); + var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken); if (gift == null) throw new KnownException("礼品不存在"); diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/Members/LoginMemberCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/Members/LoginMemberCommand.cs index f926c07..69e171b 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Commands/Members/LoginMemberCommand.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/Members/LoginMemberCommand.cs @@ -1,5 +1,10 @@ using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; 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; @@ -32,7 +37,8 @@ public class LoginMemberCommandValidator : AbstractValidator /// 会员登录命令处理器 /// public class LoginMemberCommandHandler( - IMemberRepository memberRepository) + IMemberRepository memberRepository, + IConfiguration configuration) : ICommandHandler { public async Task Handle(LoginMemberCommand command, CancellationToken cancellationToken) @@ -50,8 +56,8 @@ public class LoginMemberCommandHandler( if (member.Status == MemberStatus.Disabled) throw new KnownException("该账号已被禁用"); - // 生成Token(这里简化处理) - var token = GenerateToken(member.Id); + // 生成JWT Token + var token = GenerateJwtToken(member.Id, member.Phone); return new LoginMemberResponse(member.Id, token); } @@ -61,9 +67,37 @@ public class LoginMemberCommandHandler( return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password)); } - private static string GenerateToken(MemberId memberId) + private string GenerateJwtToken(MemberId memberId, string phone) { - // TODO: 实际项目中应使用JWT - return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"Member:{memberId}:{DateTime.UtcNow:O}")); + var appConfig = configuration.GetSection("AppConfiguration").Get() + ?? 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); } } diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/ActivatePointsRuleCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/ActivatePointsRuleCommand.cs new file mode 100644 index 0000000..6e06656 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/ActivatePointsRuleCommand.cs @@ -0,0 +1,41 @@ +using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.PointsRules; + +/// +/// 激活积分规则命令 +/// +public record ActivatePointsRuleCommand(PointsRuleId RuleId) : ICommand; + +/// +/// 激活积分规则命令验证器 +/// +public class ActivatePointsRuleCommandValidator : AbstractValidator +{ + public ActivatePointsRuleCommandValidator() + { + RuleFor(x => x.RuleId) + .NotNull().WithMessage("规则ID不能为空"); + } +} + +/// +/// 激活积分规则命令处理器 +/// +public class ActivatePointsRuleCommandHandler( + IPointsRuleRepository pointsRuleRepository) + : ICommandHandler +{ + 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); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/CreatePointsRuleCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/CreatePointsRuleCommand.cs index e1db12e..0f25c5d 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/CreatePointsRuleCommand.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/CreatePointsRuleCommand.cs @@ -55,7 +55,8 @@ public class CreatePointsRuleCommandHandler( command.MemberLevelCode, command.StartDate, command.EndDate, - cancellationToken)) + cancellationToken, + null)) // 新创建的规则,不需要排除任何规则 { throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则"); } diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/DeactivatePointsRuleCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/DeactivatePointsRuleCommand.cs new file mode 100644 index 0000000..2b45a7a --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/DeactivatePointsRuleCommand.cs @@ -0,0 +1,41 @@ +using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.PointsRules; + +/// +/// 停用积分规则命令 +/// +public record DeactivatePointsRuleCommand(PointsRuleId RuleId) : ICommand; + +/// +/// 停用积分规则命令验证器 +/// +public class DeactivatePointsRuleCommandValidator : AbstractValidator +{ + public DeactivatePointsRuleCommandValidator() + { + RuleFor(x => x.RuleId) + .NotNull().WithMessage("规则ID不能为空"); + } +} + +/// +/// 停用积分规则命令处理器 +/// +public class DeactivatePointsRuleCommandHandler( + IPointsRuleRepository pointsRuleRepository) + : ICommandHandler +{ + 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); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/UpdatePointsRuleCommand.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/UpdatePointsRuleCommand.cs new file mode 100644 index 0000000..2402ed0 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/PointsRules/UpdatePointsRuleCommand.cs @@ -0,0 +1,89 @@ +using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.PointsRules; + +/// +/// 更新积分规则命令 +/// +public record UpdatePointsRuleCommand( + PointsRuleId RuleId, + string? RuleName = null, + int? PointsValue = null, + decimal? BonusMultiplier = null, + DateTime? StartDate = null, + DateTime? EndDate = null) : ICommand; + +/// +/// 更新积分规则命令验证器 +/// +public class UpdatePointsRuleCommandValidator : AbstractValidator +{ + 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); + } +} + +/// +/// 更新积分规则命令处理器 +/// +public class UpdatePointsRuleCommandHandler( + IPointsRuleRepository pointsRuleRepository) + : ICommandHandler +{ + 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); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/Products/ProductCommands.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/Products/ProductCommands.cs new file mode 100644 index 0000000..78d377b --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/Products/ProductCommands.cs @@ -0,0 +1,98 @@ +using Fengling.Backend.Domain.AggregatesModel.ProductAggregate; +using Fengling.Backend.Infrastructure.Repositories; + +namespace Fengling.Backend.Web.Application.Commands.Products; + +/// +/// 创建产品命令 +/// +public record CreateProductCommand( + string Name, + Guid CategoryId, + string CategoryName, + string? Description = null) : ICommand; + +public class CreateProductCommandValidator : AbstractValidator +{ + 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 +{ + public async Task 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; + } +} + +/// +/// 更新产品命令 +/// +public record UpdateProductCommand( + ProductId ProductId, + string? Name = null, + Guid? CategoryId = null, + string? CategoryName = null, + string? Description = null) : ICommand; + +public class UpdateProductCommandValidator : AbstractValidator +{ + 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 +{ + public async Task 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(); + } +} + +/// +/// 删除产品命令 +/// +public record DeleteProductCommand(ProductId ProductId) : ICommand; + +public class DeleteProductCommandHandler(IProductRepository productRepository) + : ICommandHandler +{ + public async Task 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(); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Commands/RedemptionOrders/RedemptionOrderCommands.cs b/Backend/src/Fengling.Backend.Web/Application/Commands/RedemptionOrders/RedemptionOrderCommands.cs index 46cb9dc..168c6a6 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Commands/RedemptionOrders/RedemptionOrderCommands.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Commands/RedemptionOrders/RedemptionOrderCommands.cs @@ -9,12 +9,12 @@ namespace Fengling.Backend.Web.Application.Commands.RedemptionOrders; /// 创建兑换订单命令 /// public record CreateRedemptionOrderCommand( - Guid MemberId, - Guid GiftId, + MemberId MemberId, + GiftId GiftId, int Quantity, - AddressDto? ShippingAddress = null) : ICommand; + CreateRedemptionOrderAddressDto? ShippingAddress = null) : ICommand; -public record AddressDto( +public record CreateRedemptionOrderAddressDto( string ReceiverName, string Phone, string Province, @@ -30,8 +30,7 @@ public class CreateRedemptionOrderCommandHandler( public async Task Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken) { // 1. 获取礼品信息 - var giftId = new GiftId(request.GiftId); - var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken); + var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken); if (gift == null) throw new KnownException("礼品不存在"); @@ -46,15 +45,14 @@ public class CreateRedemptionOrderCommandHandler( if (gift.LimitPerMember.HasValue) { var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync( - request.MemberId, request.GiftId, cancellationToken); + request.MemberId.Id, request.GiftId.Id, cancellationToken); if (redeemedCount + request.Quantity > gift.LimitPerMember.Value) throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个"); } // 4. 获取会员信息并检查积分 - var memberId = new MemberId(request.MemberId); - var member = await memberRepository.GetAsync(memberId, cancellationToken); + var member = await memberRepository.GetAsync(request.MemberId, cancellationToken); if (member == null) throw new KnownException("会员不存在"); @@ -89,8 +87,8 @@ public class CreateRedemptionOrderCommandHandler( // 8. 创建订单 var order = new RedemptionOrder( orderNo, - request.MemberId, - request.GiftId, + request.MemberId.Id, + request.GiftId.Id, gift.Name, (int)gift.Type, request.Quantity, @@ -106,15 +104,14 @@ public class CreateRedemptionOrderCommandHandler( /// /// 标记订单为已发货命令 /// -public record MarkOrderAsDispatchedCommand(Guid OrderId, string? TrackingNo = null) : ICommand; +public record MarkOrderAsDispatchedCommand(RedemptionOrderId OrderId, string? TrackingNo = null) : ICommand; public class MarkOrderAsDispatchedCommandHandler( IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler { public async Task Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken) { - var orderId = new RedemptionOrderId(request.OrderId); - var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken); + var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken); if (order == null) throw new KnownException("订单不存在"); @@ -129,15 +126,14 @@ public class MarkOrderAsDispatchedCommandHandler( /// /// 完成订单命令 /// -public record CompleteOrderCommand(Guid OrderId) : ICommand; +public record CompleteOrderCommand(RedemptionOrderId OrderId) : ICommand; public class CompleteOrderCommandHandler( IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler { public async Task Handle(CompleteOrderCommand request, CancellationToken cancellationToken) { - var orderId = new RedemptionOrderId(request.OrderId); - var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken); + var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken); if (order == null) throw new KnownException("订单不存在"); @@ -152,15 +148,14 @@ public class CompleteOrderCommandHandler( /// /// 取消订单命令 /// -public record CancelOrderCommand(Guid OrderId, string Reason) : ICommand; +public record CancelOrderCommand(RedemptionOrderId OrderId, string Reason) : ICommand; public class CancelOrderCommandHandler( IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler { public async Task Handle(CancelOrderCommand request, CancellationToken cancellationToken) { - var orderId = new RedemptionOrderId(request.OrderId); - var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken); + var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken); if (order == null) throw new KnownException("订单不存在"); diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/AdminAuth/GetCurrentAdminQuery.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/AdminAuth/GetCurrentAdminQuery.cs new file mode 100644 index 0000000..1c16008 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/AdminAuth/GetCurrentAdminQuery.cs @@ -0,0 +1,46 @@ +using Fengling.Backend.Domain.AggregatesModel.AdminAggregate; +using Fengling.Backend.Infrastructure; + +namespace Fengling.Backend.Web.Application.Queries.AdminAuth; + +/// +/// 获取当前管理员查询 +/// +public record GetCurrentAdminQuery(AdminId AdminId) : IQuery; + +/// +/// 管理员DTO +/// +public record AdminDto( + AdminId AdminId, + string Username, + string Status, + DateTime? LastLoginAt, + DateTime CreatedAt); + +/// +/// 获取当前管理员查询处理器 +/// +public class GetCurrentAdminQueryHandler(ApplicationDbContext context) + : IQueryHandler +{ + public async Task 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; + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/Categories/CategoryQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/Categories/CategoryQueries.cs new file mode 100644 index 0000000..09380d7 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/Categories/CategoryQueries.cs @@ -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; } +} + +/// +/// 查询品类列表 +/// +public record GetCategoriesQuery(bool? IsActive = null) : IQuery>; + +public class GetCategoriesQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler> +{ + public async Task> 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; + } +} + +/// +/// 查询品类详情 +/// +public record GetCategoryByIdQuery(Guid CategoryId) : IQuery; + +public class GetCategoryByIdQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler +{ + public async Task 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; + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/Gifts/GiftQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/Gifts/GiftQueries.cs index 6a79c22..8a46c5f 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Queries/Gifts/GiftQueries.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/Gifts/GiftQueries.cs @@ -1,3 +1,4 @@ +using Fengling.Backend.Domain.AggregatesModel.GiftAggregate; using Fengling.Backend.Infrastructure; namespace Fengling.Backend.Web.Application.Queries.Gifts; @@ -66,14 +67,14 @@ public class GetGiftsQueryHandler(ApplicationDbContext dbContext) : IQueryHandle /// /// 礼品详情查询 /// -public record GetGiftByIdQuery(Guid GiftId) : IQuery; +public record GetGiftByIdQuery(GiftId GiftId) : IQuery; public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler { public async Task Handle(GetGiftByIdQuery request, CancellationToken cancellationToken) { var gift = await dbContext.Gifts - .Where(x => x.Id.Id == request.GiftId) + .Where(x => x.Id == request.GiftId) .Select(x => new GiftDto { Id = x.Id.Id, diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/MarketingCodes/MarketingCodeQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/MarketingCodes/MarketingCodeQueries.cs new file mode 100644 index 0000000..1443e47 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/MarketingCodes/MarketingCodeQueries.cs @@ -0,0 +1,148 @@ +using Fengling.Backend.Infrastructure; + +namespace Fengling.Backend.Web.Application.Queries.MarketingCodes; + +/// +/// 营销码DTO +/// +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; } +} + +/// +/// 批次信息DTO +/// +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; } +} + +/// +/// 查询营销码列表 +/// +public record GetMarketingCodesQuery( + string? BatchNo = null, + Guid? ProductId = null, + bool? IsUsed = null, + DateTime? StartDate = null, + DateTime? EndDate = null) : IQuery>; + +public class GetMarketingCodesQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler> +{ + public async Task> 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; + } +} + +/// +/// 查询所有批次列表 +/// +public record GetMarketingCodeBatchesQuery : IQuery>; + +public class GetMarketingCodeBatchesQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler> +{ + public async Task> 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; + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/PointsRules/PointsRuleQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/PointsRules/PointsRuleQueries.cs new file mode 100644 index 0000000..f3192fa --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/PointsRules/PointsRuleQueries.cs @@ -0,0 +1,96 @@ +using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate; +using Fengling.Backend.Infrastructure; + +namespace Fengling.Backend.Web.Application.Queries.PointsRules; + +/// +/// 查询所有积分规则 +/// +public record GetAllPointsRulesQuery(bool? IsActive = null, int? RuleType = null) : IQuery>; + +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> +{ + public async Task> 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; + } +} + +/// +/// 根据ID查询积分规则详情 +/// +public record GetPointsRuleByIdQuery(PointsRuleId RuleId) : IQuery; + +public class GetPointsRuleByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler +{ + public async Task 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; + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/PointsTransactions/PointsTransactionQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/PointsTransactions/PointsTransactionQueries.cs new file mode 100644 index 0000000..a69c707 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/PointsTransactions/PointsTransactionQueries.cs @@ -0,0 +1,57 @@ +using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; +using Fengling.Backend.Infrastructure; + +namespace Fengling.Backend.Web.Application.Queries.PointsTransactions; + +/// +/// 获取会员积分流水查询 +/// +public record GetPointsTransactionsQuery(Guid MemberId, int? Type = null) : IQuery>; + +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> +{ + public async Task> 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; + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/Products/ProductQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/Products/ProductQueries.cs new file mode 100644 index 0000000..4b845f4 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/Products/ProductQueries.cs @@ -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; } +} + +/// +/// 查询产品列表 +/// +public record GetProductsQuery(Guid? CategoryId = null, bool? IsActive = null) : IQuery>; + +public class GetProductsQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler> +{ + public async Task> 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; + } +} + +/// +/// 查询产品详情 +/// +public record GetProductByIdQuery(Guid ProductId) : IQuery; + +public class GetProductByIdQueryHandler(ApplicationDbContext dbContext) + : IQueryHandler +{ + public async Task 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; + } +} diff --git a/Backend/src/Fengling.Backend.Web/Application/Queries/RedemptionOrders/RedemptionOrderQueries.cs b/Backend/src/Fengling.Backend.Web/Application/Queries/RedemptionOrders/RedemptionOrderQueries.cs index 0c336a1..7d6793a 100644 --- a/Backend/src/Fengling.Backend.Web/Application/Queries/RedemptionOrders/RedemptionOrderQueries.cs +++ b/Backend/src/Fengling.Backend.Web/Application/Queries/RedemptionOrders/RedemptionOrderQueries.cs @@ -17,7 +17,7 @@ public record RedemptionOrderDto public int GiftType { get; init; } public int Quantity { get; init; } public int ConsumedPoints { get; init; } - public AddressDto? ShippingAddress { get; init; } + public RedemptionOrderAddressDto? ShippingAddress { get; init; } public string? TrackingNo { get; init; } public int Status { get; init; } public string? CancelReason { get; init; } @@ -25,7 +25,7 @@ public record RedemptionOrderDto public DateTime UpdatedAt { get; init; } } -public record AddressDto +public record RedemptionOrderAddressDto { public string ReceiverName { get; init; } = string.Empty; public string Phone { get; init; } = string.Empty; @@ -64,7 +64,7 @@ public class GetRedemptionOrdersQueryHandler(ApplicationDbContext dbContext) GiftType = x.GiftType, Quantity = x.Quantity, ConsumedPoints = x.ConsumedPoints, - ShippingAddress = x.ShippingAddress == null ? null : new AddressDto + ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto { ReceiverName = x.ShippingAddress.ReceiverName, Phone = x.ShippingAddress.Phone, @@ -107,7 +107,7 @@ public class GetRedemptionOrderByIdQueryHandler(ApplicationDbContext dbContext) GiftType = x.GiftType, Quantity = x.Quantity, ConsumedPoints = x.ConsumedPoints, - ShippingAddress = x.ShippingAddress == null ? null : new AddressDto + ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto { ReceiverName = x.ShippingAddress.ReceiverName, Phone = x.ShippingAddress.Phone, diff --git a/Backend/src/Fengling.Backend.Web/Backend/src/Fengling.Backend.Web/wwwroot/uploads/.gitkeep b/Backend/src/Fengling.Backend.Web/Backend/src/Fengling.Backend.Web/wwwroot/uploads/.gitkeep new file mode 100644 index 0000000..3ebe65a --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Backend/src/Fengling.Backend.Web/wwwroot/uploads/.gitkeep @@ -0,0 +1 @@ +# Uploads Directory diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Categories/CategoryEndpoints.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Categories/CategoryEndpoints.cs new file mode 100644 index 0000000..bd963b3 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Categories/CategoryEndpoints.cs @@ -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>> +{ + 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> +{ + 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> +{ + 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 +{ + 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 +{ + public override async Task HandleAsync(DeleteCategoryCommand req, CancellationToken ct) + { + var result = await mediator.Send(req, ct); + await Send.OkAsync(result, ct); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/GetPointsRulesEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/GetPointsRulesEndpoint.cs new file mode 100644 index 0000000..6d3a720 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/GetPointsRulesEndpoint.cs @@ -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; + +/// +/// 获取积分规则列表端点 +/// +[Tags("Admin-PointsRules")] +[HttpGet("/api/admin/points-rules")] +[AllowAnonymous] +public class GetPointsRulesEndpoint(IMediator mediator) + : EndpointWithoutRequest>> +{ + public override async Task HandleAsync(CancellationToken ct) + { + var query = new GetAllPointsRulesQuery(); + var rules = await mediator.Send(query, ct); + await Send.OkAsync(rules.AsResponseData(), ct); + } +} + +/// +/// 根据ID获取积分规则详情端点 +/// +[Tags("Admin-PointsRules")] +[HttpGet("/api/admin/points-rules/{id}")] +[AllowAnonymous] +public class GetPointsRuleByIdEndpoint(IMediator mediator) + : Endpoint> +{ + public override async Task HandleAsync(EmptyRequest _, CancellationToken ct) + { + var id = Route("id"); + var query = new GetPointsRuleByIdQuery(id!); + var rule = await mediator.Send(query, ct); + await Send.OkAsync(rule.AsResponseData(), ct); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/MarketingCodes/MarketingCodeQueryEndpoints.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/MarketingCodes/MarketingCodeQueryEndpoints.cs new file mode 100644 index 0000000..0184572 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/MarketingCodes/MarketingCodeQueryEndpoints.cs @@ -0,0 +1,56 @@ +using FastEndpoints; +using Fengling.Backend.Web.Application.Queries.MarketingCodes; + +namespace Fengling.Backend.Web.Endpoints.Admin.MarketingCodes; + +/// +/// 查询营销码请求 +/// +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; } +} + +/// +/// 查询营销码列表端点 +/// +[Tags("Admin-MarketingCodes")] +[HttpGet("/api/admin/marketing-codes")] +[AllowAnonymous] +public class GetMarketingCodesEndpoint(IMediator mediator) + : Endpoint>> +{ + 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); + } +} + +/// +/// 查询营销码批次列表端点 +/// +[Tags("Admin-MarketingCodes")] +[HttpGet("/api/admin/marketing-codes/batches")] +[AllowAnonymous] +public class GetMarketingCodeBatchesEndpoint(IMediator mediator) + : EndpointWithoutRequest>> +{ + public override async Task HandleAsync(CancellationToken ct) + { + var query = new GetMarketingCodeBatchesQuery(); + var result = await mediator.Send(query, ct); + await Send.OkAsync(result.AsResponseData(), ct); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/PointsRuleStatusEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/PointsRuleStatusEndpoint.cs new file mode 100644 index 0000000..9fff727 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/PointsRuleStatusEndpoint.cs @@ -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; + +/// +/// 激活积分规则端点 +/// +[Tags("Admin-PointsRules")] +[HttpPost("/api/admin/points-rules/{id}/activate")] +[AllowAnonymous] +public class ActivatePointsRuleEndpoint(IMediator mediator) + : EndpointWithoutRequest> +{ + public override async Task HandleAsync(CancellationToken ct) + { + var id = Route("id"); + var ruleId = new PointsRuleId(id); + var command = new ActivatePointsRuleCommand(ruleId); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), ct); + } +} + +/// +/// 停用积分规则端点 +/// +[Tags("Admin-PointsRules")] +[HttpPost("/api/admin/points-rules/{id}/deactivate")] +[AllowAnonymous] +public class DeactivatePointsRuleEndpoint(IMediator mediator) + : EndpointWithoutRequest> +{ + public override async Task HandleAsync(CancellationToken ct) + { + var id = Route("id"); + var ruleId = new PointsRuleId(id); + var command = new DeactivatePointsRuleCommand(ruleId); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), ct); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Products/ProductEndpoints.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Products/ProductEndpoints.cs new file mode 100644 index 0000000..ac45bce --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/Products/ProductEndpoints.cs @@ -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>> +{ + 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> +{ + 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> +{ + 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 +{ + 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 +{ + public override async Task HandleAsync(DeleteProductCommand req, CancellationToken ct) + { + var result = await mediator.Send(req, ct); + await Send.OkAsync(result, ct); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UpdatePointsRuleEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UpdatePointsRuleEndpoint.cs new file mode 100644 index 0000000..d9c7ad9 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UpdatePointsRuleEndpoint.cs @@ -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; + +/// +/// 更新积分规则请求 +/// +public record UpdatePointsRuleRequest( + string? RuleName = null, + int? PointsValue = null, + decimal? BonusMultiplier = null, + DateTime? StartDate = null, + DateTime? EndDate = null); + +/// +/// 更新积分规则端点 +/// +[Tags("Admin-PointsRules")] +[HttpPut("/api/admin/points-rules/{id}")] +[AllowAnonymous] +public class UpdatePointsRuleEndpoint(IMediator mediator) + : Endpoint> +{ + public override async Task HandleAsync(UpdatePointsRuleRequest req, CancellationToken ct) + { + var id = Route("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); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UploadImageEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UploadImageEndpoint.cs new file mode 100644 index 0000000..08618fc --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Admin/UploadImageEndpoint.cs @@ -0,0 +1,31 @@ +using FastEndpoints; +using Fengling.Backend.Web.Services; + +namespace Fengling.Backend.Web.Endpoints.Admin; + +/// +/// 图片上传请求 +/// +public class UploadImageRequest +{ + public IFormFile File { get; set; } = null!; + public string? Folder { get; set; } +} + +/// +/// 图片上传端点 +/// +[Tags("Admin/Upload")] +[HttpPost("/api/admin/upload/image")] +[AllowAnonymous] +[AllowFileUploads] +public class UploadImageEndpoint(IFileStorageService fileStorageService) + : Endpoint> +{ + 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); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/AdminLoginEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/AdminLoginEndpoint.cs new file mode 100644 index 0000000..5dd0ec1 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/AdminLoginEndpoint.cs @@ -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; + +/// +/// 管理员登录请求 +/// +public record AdminLoginRequest(string Username, string Password); + +/// +/// 管理员登录响应 +/// +public record AdminLoginResponseDto(AdminId AdminId, string Username, string Token, DateTime ExpiresAt); + +/// +/// 管理员登录端点 +/// +[Tags("AdminAuth")] +[HttpPost("/api/admin/auth/login")] +[AllowAnonymous] +public class AdminLoginEndpoint(IMediator mediator) + : Endpoint> +{ + 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); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/GetCurrentAdminEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/GetCurrentAdminEndpoint.cs new file mode 100644 index 0000000..d1fa484 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/AdminAuth/GetCurrentAdminEndpoint.cs @@ -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; + +/// +/// 获取当前管理员端点 +/// +[Tags("AdminAuth")] +// [HttpGet("/api/admin/auth/me")] +public class GetCurrentAdminEndpoint(IMediator mediator) + : EndpointWithoutRequest> +{ + 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); + } +} diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Gifts/GiftEndpoints.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Gifts/GiftEndpoints.cs index 5d7f674..97d70f1 100644 --- a/Backend/src/Fengling.Backend.Web/Endpoints/Gifts/GiftEndpoints.cs +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Gifts/GiftEndpoints.cs @@ -1,6 +1,9 @@ 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.Queries.Gifts; +using System.Security.Claims; namespace Fengling.Backend.Web.Endpoints.Gifts; @@ -37,17 +40,71 @@ public class GetGiftDetailEndpoint(IMediator mediator) } } +/// +/// 兑换礼品请求 +/// +public record RedeemGiftRequest( + string GiftId, + int Quantity, + RedeemGiftAddressDto? ShippingAddress = null); + +/// +/// 收货地址 +/// +public record RedeemGiftAddressDto( + string ReceiverName, + string ReceiverPhone, + string Province, + string City, + string District, + string DetailAddress); + /// /// 兑换礼品端点(会员端) /// [Tags("Gifts")] [HttpPost("/api/gifts/redeem")] public class RedeemGiftEndpoint(IMediator mediator) - : Endpoint> + : Endpoint> { - 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); - await Send.OkAsync(orderId.Id.AsResponseData(), 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; + } + + 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); } } diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/MarketingCodes/UseMarketingCodeEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/MarketingCodes/UseMarketingCodeEndpoint.cs index d285da8..a142ffd 100644 --- a/Backend/src/Fengling.Backend.Web/Endpoints/MarketingCodes/UseMarketingCodeEndpoint.cs +++ b/Backend/src/Fengling.Backend.Web/Endpoints/MarketingCodes/UseMarketingCodeEndpoint.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using FastEndpoints; using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate; @@ -8,7 +9,7 @@ namespace Fengling.Backend.Web.Endpoints.MarketingCodes; /// /// 扫码请求 /// -public record UseMarketingCodeRequest(string Code, MemberId MemberId); +public record UseMarketingCodeRequest(string Code); /// /// 扫码响应 @@ -24,13 +25,22 @@ public record UseMarketingCodeEndpointResponse( /// [Tags("MarketingCodes")] [HttpPost("/api/marketing-codes/scan")] -[AllowAnonymous] public class UseMarketingCodeEndpoint(IMediator mediator) : Endpoint> { 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 response = new UseMarketingCodeEndpointResponse( diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Members/GetCurrentMemberEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Members/GetCurrentMemberEndpoint.cs new file mode 100644 index 0000000..3b89c7f --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Members/GetCurrentMemberEndpoint.cs @@ -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; + +/// +/// 获取当前会员信息响应 +/// +public record GetCurrentMemberResponse( + string Id, + string Phone, + string? Nickname, + int TotalPoints, + int AvailablePoints, + string Level, + string Status, + DateTime CreatedAt); + +/// +/// 获取当前会员信息端点 +/// +[Tags("Members")] +[HttpGet("/api/members/current")] +public class GetCurrentMemberEndpoint(IMemberRepository memberRepository) + : EndpointWithoutRequest> +{ + 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); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/Members/LogoutMemberEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/Members/LogoutMemberEndpoint.cs new file mode 100644 index 0000000..a8dc986 --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/Members/LogoutMemberEndpoint.cs @@ -0,0 +1,18 @@ +using FastEndpoints; + +namespace Fengling.Backend.Web.Endpoints.Members; + +/// +/// 退出登录端点 +/// +[Tags("Members")] +[HttpPost("/api/members/logout")] +public class LogoutMemberEndpoint : EndpointWithoutRequest +{ + public override async Task HandleAsync(CancellationToken ct) + { + // JWT Token 由客户端负责清除,服务端无需特殊处理 + // 这里可以添加黑名单Token逻辑(如果需要) + await Send.NoContentAsync(ct); + } +} \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/PointsTransactions/GetMyPointsTransactionsEndpoint.cs b/Backend/src/Fengling.Backend.Web/Endpoints/PointsTransactions/GetMyPointsTransactionsEndpoint.cs new file mode 100644 index 0000000..d693c8b --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Endpoints/PointsTransactions/GetMyPointsTransactionsEndpoint.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using FastEndpoints; +using Fengling.Backend.Web.Application.Queries.PointsTransactions; + +namespace Fengling.Backend.Web.Endpoints.PointsTransactions; + +/// +/// 获取我的积分流水记录 +/// +[Tags("PointsTransactions")] +[HttpGet("/api/points-transactions/my")] +public class GetMyPointsTransactionsEndpoint(IMediator mediator) + : Endpoint>> +{ + 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); diff --git a/Backend/src/Fengling.Backend.Web/Endpoints/RedemptionOrders/RedemptionOrderEndpoints.cs b/Backend/src/Fengling.Backend.Web/Endpoints/RedemptionOrders/RedemptionOrderEndpoints.cs index b45c4ef..c0a3111 100644 --- a/Backend/src/Fengling.Backend.Web/Endpoints/RedemptionOrders/RedemptionOrderEndpoints.cs +++ b/Backend/src/Fengling.Backend.Web/Endpoints/RedemptionOrders/RedemptionOrderEndpoints.cs @@ -1,5 +1,7 @@ using FastEndpoints; +using Fengling.Backend.Domain.AggregatesModel.MemberAggregate; using Fengling.Backend.Web.Application.Queries.RedemptionOrders; +using System.Security.Claims; namespace Fengling.Backend.Web.Endpoints.RedemptionOrders; @@ -13,15 +15,23 @@ public class GetMyRedemptionOrdersEndpoint(IMediator mediator) { public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct) { - // TODO: 从JWT Token中获取当前登录会员ID - // 暂时使用请求中的MemberId - var query = new GetRedemptionOrdersQuery(MemberId: req.MemberId, Status: req.Status); + // 从 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 = memberGuid; + var query = new GetRedemptionOrdersQuery(MemberId: memberId, Status: req.Status); var orders = await mediator.Send(query, ct); await Send.OkAsync(orders.AsResponseData(), ct); } } -public record GetMyRedemptionOrdersRequest(Guid MemberId, int? Status = null); +public record GetMyRedemptionOrdersRequest(int? Status = null); /// /// 获取订单详情端点(会员端) diff --git a/Backend/src/Fengling.Backend.Web/Program.cs b/Backend/src/Fengling.Backend.Web/Program.cs index f471700..72b2251 100644 --- a/Backend/src/Fengling.Backend.Web/Program.cs +++ b/Backend/src/Fengling.Backend.Web/Program.cs @@ -19,10 +19,13 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Refit; using NetCorePal.Extensions.CodeAnalysis; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; Log.Logger = new LoggerConfiguration() .Enrich.WithClientIp() - .WriteTo.Console(new JsonFormatter()) + .WriteTo.Console() .CreateLogger(); try { @@ -59,17 +62,34 @@ try // 配置JWT认证 builder.Services.Configure(builder.Configuration.GetSection("AppConfiguration")); - var appConfig = builder.Configuration.GetSection("AppConfiguration").Get() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" }; + var appConfig = builder.Configuration.GetSection("AppConfiguration").Get() + ?? new AppConfiguration + { + JwtIssuer = "FenglingBackend", + JwtAudience = "FenglingBackend", + Secret = "YourVerySecretKeyForJwtTokenGeneration12345!" + }; - builder.Services.AddAuthentication().AddJwtBearer(options => - { - options.RequireHttpsMetadata = false; - options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience; - options.TokenValidationParameters.ValidateAudience = true; - options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer; - options.TokenValidationParameters.ValidateIssuer = true; - }); - builder.Services.AddNetCorePalJwt().AddRedisStore(); + 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.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = appConfig.JwtIssuer, + ValidAudience = appConfig.JwtAudience, + IssuerSigningKey = key, + ClockSkew = TimeSpan.Zero + }; + }); #endregion @@ -143,6 +163,9 @@ try .AddKnownExceptionValidationBehavior() .AddUnitOfWorkBehaviors()); + // 文件存储服务 + builder.Services.AddSingleton(); + #region 多环境支持与服务注册发现 builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template") @@ -180,12 +203,16 @@ try var app = builder.Build(); - // 在非生产环境中执行数据库迁移(包括开发、测试、Staging等环境) + // 在非生产环境中执行数据库迁移(包括开发、测试、Staging等环境) if (!app.Environment.IsProduction()) { using var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(); + + // 初始化默认管理员账号 + var mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new Fengling.Backend.Web.Application.Commands.AdminAuth.InitializeDefaultAdminCommand()); } @@ -198,6 +225,15 @@ try } app.UseStaticFiles(); + app.UseCors(x => + { + x + .SetIsOriginAllowed(_=>true) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyMethod() + .Build(); + }); //app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); // Authentication 必须在 Authorization 之前 diff --git a/Backend/src/Fengling.Backend.Web/Services/IFileStorageService.cs b/Backend/src/Fengling.Backend.Web/Services/IFileStorageService.cs new file mode 100644 index 0000000..07165ab --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Services/IFileStorageService.cs @@ -0,0 +1,16 @@ +namespace Fengling.Backend.Web.Services; + +/// +/// 文件存储服务接口 +/// +public interface IFileStorageService +{ + /// + /// 上传图片 + /// + /// 图片文件 + /// 文件夹名称 + /// 取消令牌 + /// 图片URL + Task UploadImageAsync(IFormFile file, string folder, CancellationToken cancellationToken = default); +} diff --git a/Backend/src/Fengling.Backend.Web/Services/LocalFileStorageService.cs b/Backend/src/Fengling.Backend.Web/Services/LocalFileStorageService.cs new file mode 100644 index 0000000..e54ff2d --- /dev/null +++ b/Backend/src/Fengling.Backend.Web/Services/LocalFileStorageService.cs @@ -0,0 +1,46 @@ +namespace Fengling.Backend.Web.Services; + +/// +/// 本地文件存储服务实现 +/// +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 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("\\", "/")}"; + } +} diff --git a/Backend/src/Fengling.Backend.Web/appsettings.Development.json b/Backend/src/Fengling.Backend.Web/appsettings.Development.json index b995007..563d56a 100644 --- a/Backend/src/Fengling.Backend.Web/appsettings.Development.json +++ b/Backend/src/Fengling.Backend.Web/appsettings.Development.json @@ -7,7 +7,7 @@ }, "ConnectionStrings": { "SQLite": "Data Source=fengling.db", - "Redis": "81.68.223.70:6379" + "Redis": "81.68.223.70:16379,password=sl52788542" }, "Services": { "user": { diff --git a/Backend/src/Fengling.Backend.Web/appsettings.json b/Backend/src/Fengling.Backend.Web/appsettings.json index 7592754..6c71f6b 100644 --- a/Backend/src/Fengling.Backend.Web/appsettings.json +++ b/Backend/src/Fengling.Backend.Web/appsettings.json @@ -8,10 +8,10 @@ "AllowedHosts": "*", "ConnectionStrings": { "SQLite": "Data Source=fengling.db", - "Redis": "81.68.223.70:6379" + "Redis": "81.68.223.70:16379,password=sl52788542" }, "RedisStreams": { - "ConnectionString": "81.68.223.70:6379" + "ConnectionString": "81.68.223.70:16379,password=sl52788542" }, "Services": { "user": { @@ -24,5 +24,11 @@ "https://user-v2:8443" ] } + }, + "AppConfiguration": { + "Secret": "YourVerySecretKeyForJwtTokenGeneration12345!", + "TokenExpiryInMinutes": 1440, + "JwtIssuer": "FenglingBackend", + "JwtAudience": "FenglingBackend" } } \ No newline at end of file diff --git a/Backend/src/Fengling.Backend.Web/fengling.db b/Backend/src/Fengling.Backend.Web/fengling.db new file mode 100644 index 0000000..0de02ec Binary files /dev/null and b/Backend/src/Fengling.Backend.Web/fengling.db differ diff --git a/Backend/src/Fengling.Backend.Web/fengling.db-shm b/Backend/src/Fengling.Backend.Web/fengling.db-shm new file mode 100644 index 0000000..a6ce08a Binary files /dev/null and b/Backend/src/Fengling.Backend.Web/fengling.db-shm differ diff --git a/Backend/src/Fengling.Backend.Web/fengling.db-wal b/Backend/src/Fengling.Backend.Web/fengling.db-wal new file mode 100644 index 0000000..51c8fde Binary files /dev/null and b/Backend/src/Fengling.Backend.Web/fengling.db-wal differ diff --git a/Frontend/Fengling.Backend.Admin/.env.development b/Frontend/Fengling.Backend.Admin/.env.development new file mode 100644 index 0000000..c4d9e2b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/.env.development @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=http://localhost:5511 +VITE_APP_TITLE=Fengling 管理后台 diff --git a/Frontend/Fengling.Backend.Admin/.env.production b/Frontend/Fengling.Backend.Admin/.env.production new file mode 100644 index 0000000..32246e9 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/.env.production @@ -0,0 +1,2 @@ +VITE_API_BASE_URL= +VITE_APP_TITLE=Fengling 管理后台 diff --git a/Frontend/Fengling.Backend.Admin/.gitignore b/Frontend/Fengling.Backend.Admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/.gitignore @@ -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? diff --git a/Frontend/Fengling.Backend.Admin/.vscode/extensions.json b/Frontend/Fengling.Backend.Admin/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/Frontend/Fengling.Backend.Admin/README.md b/Frontend/Fengling.Backend.Admin/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/README.md @@ -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 ` + + diff --git a/Frontend/Fengling.Backend.Admin/package.json b/Frontend/Fengling.Backend.Admin/package.json new file mode 100644 index 0000000..f0317d1 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/package.json @@ -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" + } +} diff --git a/Frontend/Fengling.Backend.Admin/pnpm-lock.yaml b/Frontend/Fengling.Backend.Admin/pnpm-lock.yaml new file mode 100644 index 0000000..b01d51f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/pnpm-lock.yaml @@ -0,0 +1,1869 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tanstack/vue-table': + specifier: ^8.21.3 + version: 8.21.3(vue@3.5.28(typescript@5.9.3)) + '@vueuse/core': + specifier: ^14.2.1 + version: 14.2.1(vue@3.5.28(typescript@5.9.3)) + axios: + specifier: ^1.13.5 + version: 1.13.5 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-vue-next: + specifier: ^0.563.0 + version: 0.563.0(vue@3.5.28(typescript@5.9.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)) + reka-ui: + specifier: ^2.8.0 + version: 2.8.0(vue@3.5.28(typescript@5.9.3)) + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 + vue: + specifier: ^3.5.25 + version: 3.5.28(typescript@5.9.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.28(typescript@5.9.3)) + vue-sonner: + specifier: ^2.0.9 + version: 2.0.9 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)) + '@types/node': + specifier: ^24.10.1 + version: 24.10.13 + '@vitejs/plugin-vue': + specifier: ^6.0.2 + version: 6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.28(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)) + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + vue-tsc: + specifier: ^3.1.5 + version: 3.2.4(typescript@5.9.3) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@floating-ui/vue@1.1.10': + resolution: {integrity: sha512-vdf8f6rHnFPPLRsmL4p12wYl+Ux4mOJOkjzKEMYVnwdf7UFdvBtHlLvQyx8iKG5vhPRbDRgZxdtpmyigDPjzYg==} + + '@internationalized/date@3.11.0': + resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.18': + resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} + + '@tanstack/vue-table@8.21.3': + resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + + '@tanstack/vue-virtual@3.13.18': + resolution: {integrity: sha512-6pT8HdHtTU5Z+t906cGdCroUNA5wHjFXsNss9gwk7QAr1VNZtz9IQCs2Nhx0gABK48c+OocHl2As+TMg8+Hy4A==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.10.13': + resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@vitejs/plugin-vue@6.0.4': + resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.27': + resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} + + '@volar/source-map@2.4.27': + resolution: {integrity: sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==} + + '@volar/typescript@2.4.27': + resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} + + '@vue/compiler-core@3.5.28': + resolution: {integrity: sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==} + + '@vue/compiler-dom@3.5.28': + resolution: {integrity: sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==} + + '@vue/compiler-sfc@3.5.28': + resolution: {integrity: sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==} + + '@vue/compiler-ssr@3.5.28': + resolution: {integrity: sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/language-core@3.2.4': + resolution: {integrity: sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==} + + '@vue/reactivity@3.5.28': + resolution: {integrity: sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==} + + '@vue/runtime-core@3.5.28': + resolution: {integrity: sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==} + + '@vue/runtime-dom@3.5.28': + resolution: {integrity: sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==} + + '@vue/server-renderer@3.5.28': + resolution: {integrity: sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==} + peerDependencies: + vue: 3.5.28 + + '@vue/shared@3.5.28': + resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lucide-vue-next@0.563.0: + resolution: {integrity: sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA==} + peerDependencies: + vue: '>=3.0.1' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + reka-ui@2.8.0: + resolution: {integrity: sha512-N4JOyIrmDE7w2i06WytqcV2QICubtS2PsK5Uo8FIMAgmO13KhUAgAByP26cXjjm2oF/w7rTyRs8YaqtvaBT+SA==} + peerDependencies: + vue: '>= 3.2.0' + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-sonner@2.0.9: + resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==} + peerDependencies: + '@nuxt/kit': ^4.0.3 + '@nuxt/schema': ^4.0.3 + nuxt: ^4.0.3 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@nuxt/schema': + optional: true + nuxt: + optional: true + + vue-tsc@3.2.4: + resolution: {integrity: sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.28: + resolution: {integrity: sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@floating-ui/vue@1.1.10(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@floating-ui/dom': 1.7.5 + '@floating-ui/utils': 0.2.10 + vue-demi: 0.14.10(vue@3.5.28(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@internationalized/date@3.11.0': + dependencies: + '@swc/helpers': 0.5.18 + + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.18 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rolldown/pluginutils@1.0.0-rc.2': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.18': {} + + '@tanstack/vue-table@8.21.3(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@tanstack/table-core': 8.21.3 + vue: 3.5.28(typescript@5.9.3) + + '@tanstack/vue-virtual@3.13.18(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@tanstack/virtual-core': 3.13.18 + vue: 3.5.28(typescript@5.9.3) + + '@types/estree@1.0.8': {} + + '@types/node@24.10.13': + dependencies: + undici-types: 7.16.0 + + '@types/web-bluetooth@0.0.21': {} + + '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2) + vue: 3.5.28(typescript@5.9.3) + + '@volar/language-core@2.4.27': + dependencies: + '@volar/source-map': 2.4.27 + + '@volar/source-map@2.4.27': {} + + '@volar/typescript@2.4.27': + dependencies: + '@volar/language-core': 2.4.27 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.28': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.28 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.28': + dependencies: + '@vue/compiler-core': 3.5.28 + '@vue/shared': 3.5.28 + + '@vue/compiler-sfc@3.5.28': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.28 + '@vue/compiler-dom': 3.5.28 + '@vue/compiler-ssr': 3.5.28 + '@vue/shared': 3.5.28 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.28': + dependencies: + '@vue/compiler-dom': 3.5.28 + '@vue/shared': 3.5.28 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.2.4': + dependencies: + '@volar/language-core': 2.4.27 + '@vue/compiler-dom': 3.5.28 + '@vue/shared': 3.5.28 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.28': + dependencies: + '@vue/shared': 3.5.28 + + '@vue/runtime-core@3.5.28': + dependencies: + '@vue/reactivity': 3.5.28 + '@vue/shared': 3.5.28 + + '@vue/runtime-dom@3.5.28': + dependencies: + '@vue/reactivity': 3.5.28 + '@vue/runtime-core': 3.5.28 + '@vue/shared': 3.5.28 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.28(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.28 + '@vue/shared': 3.5.28 + vue: 3.5.28(typescript@5.9.3) + + '@vue/shared@3.5.28': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.28(typescript@5.9.3) + + '@vueuse/core@14.2.1(vue@3.5.28(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.28(typescript@5.9.3)) + vue: 3.5.28(typescript@5.9.3) + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.28(typescript@5.9.3))': + dependencies: + vue: 3.5.28(typescript@5.9.3) + + alien-signals@3.1.2: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + birpc@2.9.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.3: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + is-what@5.5.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lucide-vue-next@0.563.0(vue@3.5.28(typescript@5.9.3)): + dependencies: + vue: 3.5.28(typescript@5.9.3) + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mitt@3.0.1: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + ohash@2.0.11: {} + + path-browserify@1.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.28(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.28(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + reka-ui@2.8.0(vue@3.5.28(typescript@5.9.3)): + dependencies: + '@floating-ui/dom': 1.7.5 + '@floating-ui/vue': 1.1.10(vue@3.5.28(typescript@5.9.3)) + '@internationalized/date': 3.11.0 + '@internationalized/number': 3.6.5 + '@tanstack/vue-virtual': 3.13.18(vue@3.5.28(typescript@5.9.3)) + '@vueuse/core': 14.2.1(vue@3.5.28(typescript@5.9.3)) + '@vueuse/shared': 14.2.1(vue@3.5.28(typescript@5.9.3)) + aria-hidden: 1.2.6 + defu: 6.1.4 + ohash: 2.0.11 + vue: 3.5.28(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + + rfdc@1.4.1: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tailwind-merge@3.4.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.13 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + vscode-uri@3.1.0: {} + + vue-demi@0.14.10(vue@3.5.28(typescript@5.9.3)): + dependencies: + vue: 3.5.28(typescript@5.9.3) + + vue-router@4.6.4(vue@3.5.28(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.28(typescript@5.9.3) + + vue-sonner@2.0.9: {} + + vue-tsc@3.2.4(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.27 + '@vue/language-core': 3.2.4 + typescript: 5.9.3 + + vue@3.5.28(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.28 + '@vue/compiler-sfc': 3.5.28 + '@vue/runtime-dom': 3.5.28 + '@vue/server-renderer': 3.5.28(vue@3.5.28(typescript@5.9.3)) + '@vue/shared': 3.5.28 + optionalDependencies: + typescript: 5.9.3 diff --git a/Frontend/Fengling.Backend.Admin/public/vite.svg b/Frontend/Fengling.Backend.Admin/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Fengling.Backend.Admin/src/App.vue b/Frontend/Fengling.Backend.Admin/src/App.vue new file mode 100644 index 0000000..b4e3775 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/App.vue @@ -0,0 +1,8 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/api/admin-auth.ts b/Frontend/Fengling.Backend.Admin/src/api/admin-auth.ts new file mode 100644 index 0000000..92a6d30 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/admin-auth.ts @@ -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 => { + const res = await apiClient.post>('/api/admin/auth/login', data) + return res.data.data +} + +/** + * 获取当前管理员信息 + */ +export const getCurrentAdmin = async (): Promise => { + const res = await apiClient.get>('/api/admin/auth/me') + return res.data.data +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/categories.ts b/Frontend/Fengling.Backend.Admin/src/api/categories.ts new file mode 100644 index 0000000..563818f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/categories.ts @@ -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 { + const res = await apiClient.get>('/api/admin/categories') + return res.data.data +} + +export async function getCategoryById(id: string): Promise { + const res = await apiClient.get>(`/api/admin/categories/${id}`) + return res.data.data +} + +export async function createCategory(data: CreateCategoryRequest): Promise { + const res = await apiClient.post>('/api/admin/categories', data) + return res.data.data +} + +export async function updateCategory(id: string, data: UpdateCategoryRequest): Promise { + await apiClient.put(`/api/admin/categories/${id}`, { ...data, categoryId: id }) +} + +export async function deleteCategory(id: string): Promise { + await apiClient.delete(`/api/admin/categories/${id}`) +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/client.ts b/Frontend/Fengling.Backend.Admin/src/api/client.ts new file mode 100644 index 0000000..c0caef4 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/client.ts @@ -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 diff --git a/Frontend/Fengling.Backend.Admin/src/api/gifts.ts b/Frontend/Fengling.Backend.Admin/src/api/gifts.ts new file mode 100644 index 0000000..de6c666 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/gifts.ts @@ -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 { + const res = await apiClient.get>('/api/admin/gifts') + return res.data.data +} + +export async function getGiftById(id: string): Promise { + const res = await apiClient.get>(`/api/admin/gifts/${id}`) + return res.data.data +} + +export async function createGift(data: CreateGiftRequest): Promise { + const res = await apiClient.post>('/api/admin/gifts', data) + return res.data.data +} + +export async function updateGift(id: string, data: UpdateGiftRequest): Promise { + await apiClient.put(`/api/admin/gifts/${id}`, { ...data, giftId: id }) +} + +export async function putOnShelf(id: string): Promise { + await apiClient.post(`/api/admin/gifts/${id}/putonshelf`, {}) +} + +export async function putOffShelf(id: string): Promise { + await apiClient.post(`/api/admin/gifts/${id}/putoffshelf`, {}) +} + +export async function addGiftStock(id: string, quantity: number): Promise { + await apiClient.post(`/api/admin/gifts/${id}/addstock`, { giftId: id, quantity }) +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/marketing-codes.ts b/Frontend/Fengling.Backend.Admin/src/api/marketing-codes.ts new file mode 100644 index 0000000..1c1b9c7 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/marketing-codes.ts @@ -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 { + const res = await apiClient.post>('/api/admin/marketing-codes/generate', data) + return res.data.data +} + +export async function getMarketingCodes(params: GetMarketingCodesParams): Promise { + const res = await apiClient.get>('/api/admin/marketing-codes', { params }) + return res.data.data +} + +export async function getMarketingCodeBatches(): Promise { + const res = await apiClient.get>('/api/admin/marketing-codes/batches') + return res.data.data +} + diff --git a/Frontend/Fengling.Backend.Admin/src/api/members.ts b/Frontend/Fengling.Backend.Admin/src/api/members.ts new file mode 100644 index 0000000..1627507 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/members.ts @@ -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 { + const res = await apiClient.get>(`/api/members/${memberId}`) + return res.data.data +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/orders.ts b/Frontend/Fengling.Backend.Admin/src/api/orders.ts new file mode 100644 index 0000000..6a9e4c8 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/orders.ts @@ -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 { + const res = await apiClient.get>('/api/admin/redemption-orders') + return res.data.data +} + +export async function getOrderById(id: string): Promise { + const res = await apiClient.get>(`/api/admin/redemption-orders/${id}`) + return res.data.data +} + +export async function dispatchOrder(data: DispatchOrderRequest): Promise { + await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/dispatch`, data) +} + +export async function completeOrder(orderId: string): Promise { + await apiClient.post(`/api/admin/redemption-orders/${orderId}/complete`, { orderId }) +} + +export async function cancelOrder(data: CancelOrderRequest): Promise { + await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/cancel`, data) +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/points-rules.ts b/Frontend/Fengling.Backend.Admin/src/api/points-rules.ts new file mode 100644 index 0000000..c81f290 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/points-rules.ts @@ -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 { + const res = await apiClient.post>('/api/admin/points-rules', data) + return res.data.data +} + +// 获取积分规则列表 +export async function getPointsRules(): Promise { + const res = await apiClient.get>('/api/admin/points-rules') + return res.data.data +} + +// 根据ID获取积分规则 +export async function getPointsRuleById(id: string): Promise { + const res = await apiClient.get>(`/api/admin/points-rules/${id}`) + return res.data.data +} + +// 更新积分规则 +export async function updatePointsRule(id: string, data: UpdatePointsRuleRequest): Promise { + await apiClient.put>(`/api/admin/points-rules/${id}`, data) +} + +// 激活积分规则 +export async function activatePointsRule(id: string): Promise { + await apiClient.post>(`/api/admin/points-rules/${id}/activate`) +} + +// 停用积分规则 +export async function deactivatePointsRule(id: string): Promise { + await apiClient.post>(`/api/admin/points-rules/${id}/deactivate`) +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/products.ts b/Frontend/Fengling.Backend.Admin/src/api/products.ts new file mode 100644 index 0000000..0cb563f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/products.ts @@ -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 { + const res = await apiClient.get>('/api/admin/products') + return res.data.data +} + +export async function getProductById(id: string): Promise { + const res = await apiClient.get>(`/api/admin/products/${id}`) + return res.data.data +} + +export async function createProduct(data: CreateProductRequest): Promise { + const res = await apiClient.post>('/api/admin/products', data) + return res.data.data +} + +export async function updateProduct(id: string, data: UpdateProductRequest): Promise { + await apiClient.put(`/api/admin/products/${id}`, { ...data, productId: id }) +} + +export async function deleteProduct(id: string): Promise { + await apiClient.delete(`/api/admin/products/${id}`) +} diff --git a/Frontend/Fengling.Backend.Admin/src/api/upload.ts b/Frontend/Fengling.Backend.Admin/src/api/upload.ts new file mode 100644 index 0000000..7ed57e2 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/api/upload.ts @@ -0,0 +1,17 @@ +import apiClient from './client' +import type { ResponseData } from '@/types/api' + +export async function uploadImage(file: File, folder?: string): Promise { + const formData = new FormData() + formData.append('File', file) + if (folder) { + formData.append('Folder', folder) + } + + const res = await apiClient.post>('/api/admin/upload/image', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return res.data.data +} diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/admin-layout-sidebar.png b/Frontend/Fengling.Backend.Admin/src/assets/design/admin-layout-sidebar.png new file mode 100644 index 0000000..ab4d339 Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/admin-layout-sidebar.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/dashboard-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/dashboard-page.png new file mode 100644 index 0000000..f38b3da Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/dashboard-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/gift-form-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/gift-form-page.png new file mode 100644 index 0000000..e0f72cc Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/gift-form-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/gift-list-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/gift-list-page.png new file mode 100644 index 0000000..8f3fdaa Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/gift-list-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/login-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/login-page.png new file mode 100644 index 0000000..62e595f Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/login-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/marketing-codes-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/marketing-codes-page.png new file mode 100644 index 0000000..7a6ad31 Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/marketing-codes-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/member-search-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/member-search-page.png new file mode 100644 index 0000000..92ca3e4 Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/member-search-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/order-detail-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/order-detail-page.png new file mode 100644 index 0000000..59104ad Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/order-detail-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/order-list-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/order-list-page.png new file mode 100644 index 0000000..154266b Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/order-list-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/design/points-rules-page.png b/Frontend/Fengling.Backend.Admin/src/assets/design/points-rules-page.png new file mode 100644 index 0000000..2e52562 Binary files /dev/null and b/Frontend/Fengling.Backend.Admin/src/assets/design/points-rules-page.png differ diff --git a/Frontend/Fengling.Backend.Admin/src/assets/vue.svg b/Frontend/Fengling.Backend.Admin/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Fengling.Backend.Admin/src/components/CategorySelector.vue b/Frontend/Fengling.Backend.Admin/src/components/CategorySelector.vue new file mode 100644 index 0000000..4f2d94d --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/CategorySelector.vue @@ -0,0 +1,51 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ImageUpload.vue b/Frontend/Fengling.Backend.Admin/src/components/ImageUpload.vue new file mode 100644 index 0000000..f57b389 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ImageUpload.vue @@ -0,0 +1,119 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ProductSelector.vue b/Frontend/Fengling.Backend.Admin/src/components/ProductSelector.vue new file mode 100644 index 0000000..9a0cf80 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ProductSelector.vue @@ -0,0 +1,66 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/layout/AdminLayout.vue b/Frontend/Fengling.Backend.Admin/src/components/layout/AdminLayout.vue new file mode 100644 index 0000000..08e1be4 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/layout/AdminLayout.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/layout/Header.vue b/Frontend/Fengling.Backend.Admin/src/components/layout/Header.vue new file mode 100644 index 0000000..2276bd0 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/layout/Header.vue @@ -0,0 +1,42 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/layout/Sidebar.vue b/Frontend/Fengling.Backend.Admin/src/components/layout/Sidebar.vue new file mode 100644 index 0000000..3678058 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/layout/Sidebar.vue @@ -0,0 +1,72 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/Avatar.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/Avatar.vue new file mode 100644 index 0000000..bb7e669 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/Avatar.vue @@ -0,0 +1,18 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarFallback.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarFallback.vue new file mode 100644 index 0000000..16b588a --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarFallback.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarImage.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarImage.vue new file mode 100644 index 0000000..24a8166 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/AvatarImage.vue @@ -0,0 +1,16 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/index.ts new file mode 100644 index 0000000..cf0e003 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/avatar/index.ts @@ -0,0 +1,3 @@ +export { default as Avatar } from "./Avatar.vue" +export { default as AvatarFallback } from "./AvatarFallback.vue" +export { default as AvatarImage } from "./AvatarImage.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/badge/Badge.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..d894dfe --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/badge/Badge.vue @@ -0,0 +1,26 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/badge/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/badge/index.ts new file mode 100644 index 0000000..bbc0dfa --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/badge/index.ts @@ -0,0 +1,26 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Badge } from "./Badge.vue" + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) +export type BadgeVariants = VariantProps diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/Breadcrumb.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/Breadcrumb.vue new file mode 100644 index 0000000..c5be5f0 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/Breadcrumb.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue new file mode 100644 index 0000000..2a35182 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue @@ -0,0 +1,23 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbItem.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbItem.vue new file mode 100644 index 0000000..e3dce68 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbItem.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbLink.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbLink.vue new file mode 100644 index 0000000..5d96381 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbLink.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbList.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbList.vue new file mode 100644 index 0000000..fc62811 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbList.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbPage.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbPage.vue new file mode 100644 index 0000000..b429b20 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbPage.vue @@ -0,0 +1,20 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbSeparator.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbSeparator.vue new file mode 100644 index 0000000..f0fc894 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/BreadcrumbSeparator.vue @@ -0,0 +1,22 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..f4eafdc --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/breadcrumb/index.ts @@ -0,0 +1,7 @@ +export { default as Breadcrumb } from "./Breadcrumb.vue" +export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue" +export { default as BreadcrumbItem } from "./BreadcrumbItem.vue" +export { default as BreadcrumbLink } from "./BreadcrumbLink.vue" +export { default as BreadcrumbList } from "./BreadcrumbList.vue" +export { default as BreadcrumbPage } from "./BreadcrumbPage.vue" +export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/button/Button.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/button/Button.vue new file mode 100644 index 0000000..374320b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/button/Button.vue @@ -0,0 +1,29 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/button/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/button/index.ts new file mode 100644 index 0000000..26e2c55 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/button/index.ts @@ -0,0 +1,38 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Button } from "./Button.vue" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + "default": "h-9 px-4 py-2 has-[>svg]:px-3", + "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + "lg": "h-10 rounded-md px-6 has-[>svg]:px-4", + "icon": "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/Card.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/Card.vue new file mode 100644 index 0000000..f5a0707 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardAction.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardAction.vue new file mode 100644 index 0000000..c91638b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..dfbc552 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardDescription.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..71c1b8d --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardFooter.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..9e3739e --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardHeader.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..4fe4da4 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardTitle.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..5f479e7 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/card/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/card/index.ts new file mode 100644 index 0000000..1627758 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/Dialog.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..ade5260 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogClose.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..c5fae04 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..7f86b47 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,53 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogDescription.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..f52e655 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogFooter.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..0a936e6 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogHeader.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..bfc3c64 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogOverlay.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogOverlay.vue new file mode 100644 index 0000000..7790077 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogOverlay.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogScrollContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..f2475db --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,59 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTitle.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..860f01a --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,23 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..49667e9 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..6768b09 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dialog/index.ts @@ -0,0 +1,10 @@ +export { default as Dialog } from "./Dialog.vue" +export { default as DialogClose } from "./DialogClose.vue" +export { default as DialogContent } from "./DialogContent.vue" +export { default as DialogDescription } from "./DialogDescription.vue" +export { default as DialogFooter } from "./DialogFooter.vue" +export { default as DialogHeader } from "./DialogHeader.vue" +export { default as DialogOverlay } from "./DialogOverlay.vue" +export { default as DialogScrollContent } from "./DialogScrollContent.vue" +export { default as DialogTitle } from "./DialogTitle.vue" +export { default as DialogTrigger } from "./DialogTrigger.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenu.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..e1c9ee3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..1253078 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..7c43014 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,39 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..da634ec --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..f56cae3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..8bca83c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..fe82cad --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..e03c40c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..1b936c3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,23 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..60be75c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..7472e77 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,18 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..d7c6b08 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..1683aaf --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..75cd747 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..955fe3a --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from "./DropdownMenu.vue" + +export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue" +export { default as DropdownMenuContent } from "./DropdownMenuContent.vue" +export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue" +export { default as DropdownMenuItem } from "./DropdownMenuItem.vue" +export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue" +export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue" +export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue" +export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue" +export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue" +export { default as DropdownMenuSub } from "./DropdownMenuSub.vue" +export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue" +export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue" +export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue" +export { DropdownMenuPortal } from "reka-ui" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/input/Input.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/input/Input.vue new file mode 100644 index 0000000..e5135c1 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/input/Input.vue @@ -0,0 +1,33 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/input/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/input/index.ts new file mode 100644 index 0000000..9976b86 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/label/Label.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/label/Label.vue new file mode 100644 index 0000000..ee63970 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/label/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/label/index.ts new file mode 100644 index 0000000..036e35c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/popover/Popover.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/Popover.vue new file mode 100644 index 0000000..4efdb98 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/Popover.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverAnchor.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverAnchor.vue new file mode 100644 index 0000000..49e01db --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverAnchor.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverContent.vue new file mode 100644 index 0000000..cf1e55c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverContent.vue @@ -0,0 +1,45 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverTrigger.vue new file mode 100644 index 0000000..fd3b497 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/PopoverTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/popover/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/index.ts new file mode 100644 index 0000000..66edf89 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/popover/index.ts @@ -0,0 +1,4 @@ +export { default as Popover } from "./Popover.vue" +export { default as PopoverAnchor } from "./PopoverAnchor.vue" +export { default as PopoverContent } from "./PopoverContent.vue" +export { default as PopoverTrigger } from "./PopoverTrigger.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollArea.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollArea.vue new file mode 100644 index 0000000..6112caa --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollArea.vue @@ -0,0 +1,33 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollBar.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollBar.vue new file mode 100644 index 0000000..a0b6f9b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/ScrollBar.vue @@ -0,0 +1,32 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..c416759 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/scroll-area/index.ts @@ -0,0 +1,2 @@ +export { default as ScrollArea } from "./ScrollArea.vue" +export { default as ScrollBar } from "./ScrollBar.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/Select.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/Select.vue new file mode 100644 index 0000000..c94bbe8 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/Select.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..adf04ec --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectContent.vue @@ -0,0 +1,51 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectGroup.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..e981c6c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItem.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..9371764 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItemText.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..b6700b1 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectLabel.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..5b6650c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollDownButton.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..7dc7670 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollUpButton.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..07fe87e --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectSeparator.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..4b5c885 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..667908b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,33 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectValue.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..d5ce58b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/select/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/select/index.ts new file mode 100644 index 0000000..96eae60 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as Select } from "./Select.vue" +export { default as SelectContent } from "./SelectContent.vue" +export { default as SelectGroup } from "./SelectGroup.vue" +export { default as SelectItem } from "./SelectItem.vue" +export { default as SelectItemText } from "./SelectItemText.vue" +export { default as SelectLabel } from "./SelectLabel.vue" +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue" +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue" +export { default as SelectSeparator } from "./SelectSeparator.vue" +export { default as SelectTrigger } from "./SelectTrigger.vue" +export { default as SelectValue } from "./SelectValue.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/separator/Separator.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/separator/Separator.vue new file mode 100644 index 0000000..78d60ec --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/separator/Separator.vue @@ -0,0 +1,29 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/separator/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/separator/index.ts new file mode 100644 index 0000000..4407287 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from "./Separator.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/Sheet.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/Sheet.vue new file mode 100644 index 0000000..8522f84 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/Sheet.vue @@ -0,0 +1,19 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetClose.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetClose.vue new file mode 100644 index 0000000..39a942c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetClose.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetContent.vue new file mode 100644 index 0000000..e0c4b8f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetContent.vue @@ -0,0 +1,62 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetDescription.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetDescription.vue new file mode 100644 index 0000000..6c8ba0a --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetDescription.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetFooter.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetFooter.vue new file mode 100644 index 0000000..5fcf751 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetFooter.vue @@ -0,0 +1,16 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetHeader.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetHeader.vue new file mode 100644 index 0000000..b6305ab --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetHeader.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetOverlay.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetOverlay.vue new file mode 100644 index 0000000..220452a --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetOverlay.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTitle.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTitle.vue new file mode 100644 index 0000000..889ae54 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTitle.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTrigger.vue new file mode 100644 index 0000000..41b121d --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/SheetTrigger.vue @@ -0,0 +1,15 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/index.ts new file mode 100644 index 0000000..7c70e5d --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sheet/index.ts @@ -0,0 +1,8 @@ +export { default as Sheet } from "./Sheet.vue" +export { default as SheetClose } from "./SheetClose.vue" +export { default as SheetContent } from "./SheetContent.vue" +export { default as SheetDescription } from "./SheetDescription.vue" +export { default as SheetFooter } from "./SheetFooter.vue" +export { default as SheetHeader } from "./SheetHeader.vue" +export { default as SheetTitle } from "./SheetTitle.vue" +export { default as SheetTrigger } from "./SheetTrigger.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/Sonner.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/Sonner.vue new file mode 100644 index 0000000..6830896 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/Sonner.vue @@ -0,0 +1,42 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/index.ts new file mode 100644 index 0000000..6673112 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./Sonner.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/switch/Switch.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..2e725ed --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/switch/Switch.vue @@ -0,0 +1,38 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/switch/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/switch/index.ts new file mode 100644 index 0000000..cc081f3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from "./Switch.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/Table.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/Table.vue new file mode 100644 index 0000000..0d0cd9b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableBody.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..d14a2d3 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCaption.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..3630084 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCell.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..d6e9ed2 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableEmpty.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..9519328 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,34 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableFooter.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..29e0ce9 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHead.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..f83efe5 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHeader.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..b4ab5cf --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableRow.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..8f1d172 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/table/index.ts new file mode 100644 index 0000000..3be308b --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableEmpty } from "./TableEmpty.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/table/utils.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/table/utils.ts new file mode 100644 index 0000000..3d4fd12 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/table/utils.ts @@ -0,0 +1,10 @@ +import type { Updater } from "@tanstack/vue-table" + +import type { Ref } from "vue" +import { isFunction } from "@tanstack/vue-table" + +export function valueUpdater(updaterOrValue: Updater, ref: Ref) { + ref.value = isFunction(updaterOrValue) + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/Tabs.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/Tabs.vue new file mode 100644 index 0000000..d260a15 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/Tabs.vue @@ -0,0 +1,24 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsContent.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsContent.vue new file mode 100644 index 0000000..3186ee8 --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsContent.vue @@ -0,0 +1,21 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsList.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsList.vue new file mode 100644 index 0000000..a64a2da --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsList.vue @@ -0,0 +1,24 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsTrigger.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsTrigger.vue new file mode 100644 index 0000000..45e424f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/TabsTrigger.vue @@ -0,0 +1,26 @@ + + + diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/index.ts b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/index.ts new file mode 100644 index 0000000..7f99b7f --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/tabs/index.ts @@ -0,0 +1,4 @@ +export { default as Tabs } from "./Tabs.vue" +export { default as TabsContent } from "./TabsContent.vue" +export { default as TabsList } from "./TabsList.vue" +export { default as TabsTrigger } from "./TabsTrigger.vue" diff --git a/Frontend/Fengling.Backend.Admin/src/components/ui/textarea/Textarea.vue b/Frontend/Fengling.Backend.Admin/src/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..790f10c --- /dev/null +++ b/Frontend/Fengling.Backend.Admin/src/components/ui/textarea/Textarea.vue @@ -0,0 +1,28 @@ + + +