feat: 实现完整的前后端功能
- 后端新增管理员、商品、分类聚合模型 - 实现积分规则、礼品、订单、会员等完整功能 - 添加管理员认证和权限管理 - 完善数据库迁移和实体配置 - 前端管理后台实现登录、仪表盘、积分规则、礼品、订单、会员等页面 - 集成shadcn-vue UI组件库 - 添加前后端功能文档和截图
@ -2,7 +2,6 @@
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Third-party package versions -->
|
||||
<NetCorePalVersion>3.2.1</NetCorePalVersion>
|
||||
@ -13,14 +12,12 @@
|
||||
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
|
||||
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
|
||||
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
|
||||
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
|
||||
|
||||
<!-- Database providers - framework specific versions -->
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
|
||||
@ -39,7 +36,6 @@
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
|
||||
|
||||
<!-- CAP packages for .NET 9.0+ -->
|
||||
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
|
||||
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
|
||||
@ -50,14 +46,12 @@
|
||||
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
|
||||
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
|
||||
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
|
||||
|
||||
<!-- FastEndpoints -->
|
||||
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
|
||||
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
|
||||
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
|
||||
|
||||
|
||||
<!-- Other packages -->
|
||||
<PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
|
||||
@ -70,7 +64,6 @@
|
||||
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
|
||||
<!-- Aspire packages -->
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
|
||||
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
|
||||
@ -98,7 +91,6 @@
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
|
||||
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
|
||||
|
||||
<!-- NetCorePal packages -->
|
||||
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
|
||||
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
|
||||
@ -126,7 +118,6 @@
|
||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
|
||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
|
||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
|
||||
|
||||
<!-- Testing packages -->
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
|
||||
@ -145,8 +136,7 @@
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
|
||||
|
||||
<!-- Code analysis -->
|
||||
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
13
Backend/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
using Fengling.Backend.Domain.DomainEvents;
|
||||
|
||||
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员ID
|
||||
/// </summary>
|
||||
public partial record AdminId : IGuidStronglyTypedId;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员聚合根
|
||||
/// </summary>
|
||||
public class Admin : Entity<AdminId>, IAggregateRoot
|
||||
{
|
||||
protected Admin() { }
|
||||
|
||||
private Admin(string username, string passwordHash)
|
||||
{
|
||||
Username = username;
|
||||
PasswordHash = passwordHash;
|
||||
Status = AdminStatus.Active;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
|
||||
this.AddDomainEvent(new AdminCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string Username { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码哈希
|
||||
/// </summary>
|
||||
public string PasswordHash { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员状态
|
||||
/// </summary>
|
||||
public AdminStatus Status { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后登录时间
|
||||
/// </summary>
|
||||
public DateTime? LastLoginAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 软删除标记
|
||||
/// </summary>
|
||||
public Deleted Deleted { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 行版本
|
||||
/// </summary>
|
||||
public RowVersion RowVersion { get; private set; } = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// 创建管理员
|
||||
/// </summary>
|
||||
public static Admin Create(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new KnownException("用户名不能为空");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new KnownException("密码不能为空");
|
||||
|
||||
var passwordHash = PasswordHelper.HashPassword(password);
|
||||
return new Admin(username, passwordHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证密码
|
||||
/// </summary>
|
||||
public bool VerifyPassword(string password)
|
||||
{
|
||||
return PasswordHelper.VerifyPassword(password, PasswordHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录登录
|
||||
/// </summary>
|
||||
public void RecordLogin()
|
||||
{
|
||||
LastLoginAt = DateTime.UtcNow;
|
||||
this.AddDomainEvent(new AdminLoggedInDomainEvent(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用管理员
|
||||
/// </summary>
|
||||
public void Disable()
|
||||
{
|
||||
if (Status == AdminStatus.Disabled)
|
||||
return;
|
||||
|
||||
Status = AdminStatus.Disabled;
|
||||
this.AddDomainEvent(new AdminDisabledDomainEvent(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用管理员
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (Status == AdminStatus.Active)
|
||||
return;
|
||||
|
||||
Status = AdminStatus.Active;
|
||||
this.AddDomainEvent(new AdminEnabledDomainEvent(this));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员状态
|
||||
/// </summary>
|
||||
public enum AdminStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 正常
|
||||
/// </summary>
|
||||
Active = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 禁用
|
||||
/// </summary>
|
||||
Disabled = 2
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
namespace Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 密码加密工具类
|
||||
/// </summary>
|
||||
public static class PasswordHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 加密密码
|
||||
/// </summary>
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentException("密码不能为空", nameof(password));
|
||||
|
||||
return BCrypt.Net.BCrypt.HashPassword(password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证密码
|
||||
/// </summary>
|
||||
public static bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return BCrypt.Net.BCrypt.Verify(password, hash);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
namespace Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 品类ID
|
||||
/// </summary>
|
||||
public partial record CategoryId : IGuidStronglyTypedId;
|
||||
|
||||
/// <summary>
|
||||
/// 品类聚合根
|
||||
/// </summary>
|
||||
public class Category : Entity<CategoryId>, IAggregateRoot
|
||||
{
|
||||
protected Category() { }
|
||||
|
||||
public Category(
|
||||
string name,
|
||||
string code,
|
||||
string? description = null,
|
||||
int sortOrder = 0)
|
||||
{
|
||||
Name = name;
|
||||
Code = code;
|
||||
Description = description ?? string.Empty;
|
||||
SortOrder = sortOrder;
|
||||
IsActive = true;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 品类名称
|
||||
/// </summary>
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 品类编码(唯一)
|
||||
/// </summary>
|
||||
public string Code { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
public int SortOrder { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否激活
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; private set; }
|
||||
|
||||
public Deleted Deleted { get; private set; } = new();
|
||||
public RowVersion RowVersion { get; private set; } = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// 更新品类信息
|
||||
/// </summary>
|
||||
public void UpdateInfo(
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
int? sortOrder = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
Name = name;
|
||||
|
||||
if (description != null)
|
||||
Description = description;
|
||||
|
||||
if (sortOrder.HasValue)
|
||||
SortOrder = sortOrder.Value;
|
||||
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
IsActive = true;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用
|
||||
/// </summary>
|
||||
public void Deactivate()
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
IsActive = false;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
namespace Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// 产品ID
|
||||
/// </summary>
|
||||
public partial record ProductId : IGuidStronglyTypedId;
|
||||
|
||||
/// <summary>
|
||||
/// 产品聚合根
|
||||
/// </summary>
|
||||
public class Product : Entity<ProductId>, IAggregateRoot
|
||||
{
|
||||
protected Product() { }
|
||||
|
||||
public Product(
|
||||
string name,
|
||||
Guid categoryId,
|
||||
string categoryName,
|
||||
string? description = null)
|
||||
{
|
||||
Name = name;
|
||||
CategoryId = categoryId;
|
||||
CategoryName = categoryName;
|
||||
Description = description ?? string.Empty;
|
||||
IsActive = true;
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产品名称
|
||||
/// </summary>
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 品类ID
|
||||
/// </summary>
|
||||
public Guid CategoryId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 品类名称
|
||||
/// </summary>
|
||||
public string CategoryName { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否激活
|
||||
/// </summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; private set; }
|
||||
|
||||
public Deleted Deleted { get; private set; } = new();
|
||||
public RowVersion RowVersion { get; private set; } = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// 更新产品信息
|
||||
/// </summary>
|
||||
public void UpdateInfo(
|
||||
string? name = null,
|
||||
Guid? categoryId = null,
|
||||
string? categoryName = null,
|
||||
string? description = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
Name = name;
|
||||
|
||||
if (categoryId.HasValue && categoryId.Value != Guid.Empty)
|
||||
{
|
||||
CategoryId = categoryId.Value;
|
||||
if (!string.IsNullOrWhiteSpace(categoryName))
|
||||
CategoryName = categoryName;
|
||||
}
|
||||
|
||||
if (description != null)
|
||||
Description = description;
|
||||
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
IsActive = true;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用
|
||||
/// </summary>
|
||||
public void Deactivate()
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
IsActive = false;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
namespace Fengling.Backend.Domain.DomainEvents;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员创建领域事件
|
||||
/// </summary>
|
||||
public record AdminCreatedDomainEvent(Admin Admin) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录领域事件
|
||||
/// </summary>
|
||||
public record AdminLoggedInDomainEvent(Admin Admin) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员禁用领域事件
|
||||
/// </summary>
|
||||
public record AdminDisabledDomainEvent(Admin Admin) : IDomainEvent;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员启用领域事件
|
||||
/// </summary>
|
||||
public record AdminEnabledDomainEvent(Admin Admin) : IDomainEvent;
|
||||
@ -7,6 +7,7 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" />
|
||||
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
|
||||
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
|
||||
<PackageReference Include="NetCorePal.Extensions.Primitives" />
|
||||
|
||||
@ -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<ApplicationDbContext>
|
||||
: AppDbContextBase(options, mediator)
|
||||
, ISqliteCapDataStorage
|
||||
{
|
||||
// 管理员聚合
|
||||
public DbSet<Admin> Admins => Set<Admin>();
|
||||
|
||||
// 会员聚合
|
||||
public DbSet<Member> Members => Set<Member>();
|
||||
public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>();
|
||||
@ -30,6 +36,12 @@ public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
|
||||
// 兑换订单聚合
|
||||
public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>();
|
||||
|
||||
// 品类聚合
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
|
||||
// 产品聚合
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
if (modelBuilder is null)
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员实体配置
|
||||
/// </summary>
|
||||
public class AdminEntityTypeConfiguration : IEntityTypeConfiguration<Admin>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Admin> builder)
|
||||
{
|
||||
builder.ToTable("Admins");
|
||||
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.UseGuidVersion7ValueGenerator()
|
||||
.HasComment("管理员ID");
|
||||
|
||||
builder.Property(x => x.Username)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasComment("用户名");
|
||||
|
||||
builder.Property(x => x.PasswordHash)
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasComment("密码哈希");
|
||||
|
||||
builder.Property(x => x.Status)
|
||||
.IsRequired()
|
||||
.HasComment("管理员状态(1=Active,2=Disabled)");
|
||||
|
||||
builder.Property(x => x.LastLoginAt)
|
||||
.HasComment("最后登录时间");
|
||||
|
||||
builder.Property(x => x.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasComment("创建时间");
|
||||
|
||||
// 索引
|
||||
builder.HasIndex(x => x.Username)
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Admins_Username");
|
||||
|
||||
builder.HasIndex(x => x.Status)
|
||||
.HasDatabaseName("IX_Admins_Status");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
|
||||
|
||||
public class CategoryEntityTypeConfiguration : IEntityTypeConfiguration<Category>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Category> builder)
|
||||
{
|
||||
builder.ToTable("Categories");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.UseGuidVersion7ValueGenerator()
|
||||
.HasComment("品类ID");
|
||||
|
||||
builder.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasComment("品类名称");
|
||||
|
||||
builder.Property(x => x.Code)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasComment("品类编码");
|
||||
|
||||
builder.Property(x => x.Description)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasComment("描述");
|
||||
|
||||
builder.Property(x => x.SortOrder)
|
||||
.IsRequired()
|
||||
.HasComment("排序");
|
||||
|
||||
builder.Property(x => x.IsActive)
|
||||
.IsRequired()
|
||||
.HasComment("是否激活");
|
||||
|
||||
builder.Property(x => x.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasComment("创建时间");
|
||||
|
||||
builder.Property(x => x.UpdatedAt)
|
||||
.IsRequired()
|
||||
.HasComment("更新时间");
|
||||
|
||||
builder.HasIndex(x => x.Code).IsUnique().HasDatabaseName("IX_Categories_Code");
|
||||
builder.HasIndex(x => x.SortOrder).HasDatabaseName("IX_Categories_SortOrder");
|
||||
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_Categories_IsActive");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.EntityConfigurations;
|
||||
|
||||
public class ProductEntityTypeConfiguration : IEntityTypeConfiguration<Product>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Product> builder)
|
||||
{
|
||||
builder.ToTable("Products");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.Id)
|
||||
.UseGuidVersion7ValueGenerator()
|
||||
.HasComment("产品ID");
|
||||
|
||||
builder.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasComment("产品名称");
|
||||
|
||||
builder.Property(x => x.CategoryId)
|
||||
.IsRequired()
|
||||
.HasComment("品类ID");
|
||||
|
||||
builder.Property(x => x.CategoryName)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasComment("品类名称");
|
||||
|
||||
builder.Property(x => x.Description)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasComment("描述");
|
||||
|
||||
builder.Property(x => x.IsActive)
|
||||
.IsRequired()
|
||||
.HasComment("是否激活");
|
||||
|
||||
builder.Property(x => x.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasComment("创建时间");
|
||||
|
||||
builder.Property(x => x.UpdatedAt)
|
||||
.IsRequired()
|
||||
.HasComment("更新时间");
|
||||
|
||||
builder.HasIndex(x => x.CategoryId).HasDatabaseName("IX_Products_CategoryId");
|
||||
builder.HasIndex(x => x.IsActive).HasDatabaseName("IX_Products_IsActive");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@ -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<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Instance")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLockTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("CAPLock", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||
|
||||
b.ToTable("CAPPublishedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||
|
||||
b.ToTable("CAPReceivedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||
@ -11,6 +11,58 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CAPLock",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||
Instance = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
LastLockTime = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CAPLock", x => x.Key);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CAPPublishedMessage",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Retries = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
StatusName = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CAPReceivedMessage",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||
Group = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Retries = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
StatusName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
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
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CAPLock");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CAPPublishedMessage");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CAPReceivedMessage");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Gifts");
|
||||
|
||||
753
Backend/src/Fengling.Backend.Infrastructure/Migrations/20260211074433_AddAdminAggregate.Designer.cs
generated
Normal file
@ -0,0 +1,753 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Fengling.Backend.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260211074433_AddAdminAggregate")]
|
||||
partial class AddAdminAggregate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("管理员ID");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("最后登录时间");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("密码哈希");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("管理员状态(1=Active,2=Disabled)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("用户名");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_Admins_Status");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Admins_Username");
|
||||
|
||||
b.ToTable("Admins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品ID");
|
||||
|
||||
b.Property<int>("AvailableStock")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("可用库存");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("图片URL");
|
||||
|
||||
b.Property<bool>("IsOnShelf")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否上架");
|
||||
|
||||
b.Property<int?>("LimitPerMember")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("每人限兑数量");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品名称");
|
||||
|
||||
b.Property<int>("RequiredPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("所需积分");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("排序");
|
||||
|
||||
b.Property<int>("TotalStock")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("总库存");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsOnShelf")
|
||||
.HasDatabaseName("IX_Gifts_IsOnShelf");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("IX_Gifts_SortOrder");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("IX_Gifts_Type");
|
||||
|
||||
b.ToTable("Gifts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("营销码ID");
|
||||
|
||||
b.Property<string>("BatchNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("批次号");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("营销码");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiryDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("过期时间");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否已使用");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("使用时间");
|
||||
|
||||
b.Property<Guid?>("UsedByMemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("使用者会员ID");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BatchNo")
|
||||
.HasDatabaseName("IX_MarketingCodes_BatchNo");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_MarketingCodes_Code");
|
||||
|
||||
b.HasIndex("IsUsed")
|
||||
.HasDatabaseName("IX_MarketingCodes_IsUsed");
|
||||
|
||||
b.ToTable("MarketingCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<int>("AvailablePoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("可用积分");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("昵称");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("密码(已加密)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("手机号");
|
||||
|
||||
b.Property<DateTime>("RegisteredAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("注册时间");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("状态(1:正常,2:禁用)");
|
||||
|
||||
b.Property<int>("TotalPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("累计总积分");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Members_Phone");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_Members_Status");
|
||||
|
||||
b.ToTable("Members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("积分规则ID");
|
||||
|
||||
b.Property<decimal>("BonusMultiplier")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("奖励倍数");
|
||||
|
||||
b.Property<Guid?>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("生效结束时间");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("MemberLevelCode")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员等级编码");
|
||||
|
||||
b.Property<int>("PointsValue")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("积分值");
|
||||
|
||||
b.Property<Guid?>("ProductId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("规则名称");
|
||||
|
||||
b.Property<int>("RuleType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("生效开始时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_PointsRules_IsActive");
|
||||
|
||||
b.HasIndex("MemberLevelCode")
|
||||
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
|
||||
|
||||
b.HasIndex("ProductId")
|
||||
.HasDatabaseName("IX_PointsRules_ProductId");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.HasDatabaseName("IX_PointsRules_DateRange");
|
||||
|
||||
b.ToTable("PointsRules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("积分流水ID");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("积分数量");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiryDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("过期时间");
|
||||
|
||||
b.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("原因描述");
|
||||
|
||||
b.Property<Guid>("RelatedId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("关联ID");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("来源");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
||||
|
||||
b.HasIndex("RelatedId")
|
||||
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("IX_PointsTransactions_Type");
|
||||
|
||||
b.ToTable("PointsTransactions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("兑换订单ID");
|
||||
|
||||
b.Property<string>("CancelReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("取消原因");
|
||||
|
||||
b.Property<int>("ConsumedPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("消耗积分");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("GiftId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品ID");
|
||||
|
||||
b.Property<string>("GiftName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品名称");
|
||||
|
||||
b.Property<int>("GiftType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("礼品类型");
|
||||
|
||||
b.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<string>("OrderNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("订单号");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("数量");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
|
||||
|
||||
b.Property<string>("TrackingNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("物流单号");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("IX_RedemptionOrders_MemberId");
|
||||
|
||||
b.HasIndex("OrderNo")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_RedemptionOrders_Status");
|
||||
|
||||
b.ToTable("RedemptionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Instance")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLockTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("CAPLock", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||
|
||||
b.ToTable("CAPPublishedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||
|
||||
b.ToTable("CAPReceivedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MarketingCodeId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<Guid?>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("CategoryId")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b1.Property<string>("CategoryName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("CategoryName")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b1.Property<Guid>("ProductId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ProductId")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b1.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ProductName")
|
||||
.HasComment("产品名称");
|
||||
|
||||
b1.HasKey("MarketingCodeId");
|
||||
|
||||
b1.ToTable("MarketingCodes");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MarketingCodeId");
|
||||
});
|
||||
|
||||
b.Navigation("ProductInfo")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<decimal>("BonusRate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("BonusRate")
|
||||
.HasComment("积分奖励倍率");
|
||||
|
||||
b1.Property<string>("LevelCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LevelCode")
|
||||
.HasComment("等级编码");
|
||||
|
||||
b1.Property<string>("LevelName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LevelName")
|
||||
.HasComment("等级名称");
|
||||
|
||||
b1.Property<int>("RequiredPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("RequiredPoints")
|
||||
.HasComment("所需积分");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("Members");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId");
|
||||
});
|
||||
|
||||
b.Navigation("Level")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("RedemptionOrderId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("City")
|
||||
.HasComment("市");
|
||||
|
||||
b1.Property<string>("DetailAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("DetailAddress")
|
||||
.HasComment("详细地址");
|
||||
|
||||
b1.Property<string>("District")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("District")
|
||||
.HasComment("区/县");
|
||||
|
||||
b1.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ReceiverPhone")
|
||||
.HasComment("联系电话");
|
||||
|
||||
b1.Property<string>("Province")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Province")
|
||||
.HasComment("省");
|
||||
|
||||
b1.Property<string>("ReceiverName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ReceiverName")
|
||||
.HasComment("收货人姓名");
|
||||
|
||||
b1.HasKey("RedemptionOrderId");
|
||||
|
||||
b1.ToTable("RedemptionOrders");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("RedemptionOrderId");
|
||||
});
|
||||
|
||||
b.Navigation("ShippingAddress");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAdminAggregate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Admins",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "管理员ID"),
|
||||
Username = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "用户名"),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false, comment: "密码哈希"),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false, comment: "管理员状态(1=Active,2=Disabled)"),
|
||||
LastLoginAt = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "最后登录时间"),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
|
||||
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Admins", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Admins_Status",
|
||||
table: "Admins",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Admins_Username",
|
||||
table: "Admins",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Admins");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,873 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Fengling.Backend.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260211084650_AddProductAndCategoryAggregates")]
|
||||
partial class AddProductAndCategoryAggregates
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("管理员ID");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("最后登录时间");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("密码哈希");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("管理员状态(1=Active,2=Disabled)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("用户名");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_Admins_Status");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Admins_Username");
|
||||
|
||||
b.ToTable("Admins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.CategoryAggregate.Category", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类编码");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("排序");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Categories_Code");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_Categories_IsActive");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("IX_Categories_SortOrder");
|
||||
|
||||
b.ToTable("Categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品ID");
|
||||
|
||||
b.Property<int>("AvailableStock")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("可用库存");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("图片URL");
|
||||
|
||||
b.Property<bool>("IsOnShelf")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否上架");
|
||||
|
||||
b.Property<int?>("LimitPerMember")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("每人限兑数量");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品名称");
|
||||
|
||||
b.Property<int>("RequiredPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("所需积分");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("排序");
|
||||
|
||||
b.Property<int>("TotalStock")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("总库存");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("礼品类型(1:实物,2:虚拟,3:自有产品)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsOnShelf")
|
||||
.HasDatabaseName("IX_Gifts_IsOnShelf");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("IX_Gifts_SortOrder");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("IX_Gifts_Type");
|
||||
|
||||
b.ToTable("Gifts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("营销码ID");
|
||||
|
||||
b.Property<string>("BatchNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("批次号");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("营销码");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiryDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("过期时间");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否已使用");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("使用时间");
|
||||
|
||||
b.Property<Guid?>("UsedByMemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("使用者会员ID");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BatchNo")
|
||||
.HasDatabaseName("IX_MarketingCodes_BatchNo");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_MarketingCodes_Code");
|
||||
|
||||
b.HasIndex("IsUsed")
|
||||
.HasDatabaseName("IX_MarketingCodes_IsUsed");
|
||||
|
||||
b.ToTable("MarketingCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<int>("AvailablePoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("可用积分");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("昵称");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("密码(已加密)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("手机号");
|
||||
|
||||
b.Property<DateTime>("RegisteredAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("注册时间");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("状态(1:正常,2:禁用)");
|
||||
|
||||
b.Property<int>("TotalPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("累计总积分");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Members_Phone");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_Members_Status");
|
||||
|
||||
b.ToTable("Members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate.PointsRule", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("积分规则ID");
|
||||
|
||||
b.Property<decimal>("BonusMultiplier")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("奖励倍数");
|
||||
|
||||
b.Property<Guid?>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("生效结束时间");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("MemberLevelCode")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员等级编码");
|
||||
|
||||
b.Property<int>("PointsValue")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("积分值");
|
||||
|
||||
b.Property<Guid?>("ProductId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("规则名称");
|
||||
|
||||
b.Property<int>("RuleType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("规则类型(1:产品,2:时间,3:会员等级)");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("生效开始时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_PointsRules_IsActive");
|
||||
|
||||
b.HasIndex("MemberLevelCode")
|
||||
.HasDatabaseName("IX_PointsRules_MemberLevelCode");
|
||||
|
||||
b.HasIndex("ProductId")
|
||||
.HasDatabaseName("IX_PointsRules_ProductId");
|
||||
|
||||
b.HasIndex("StartDate", "EndDate")
|
||||
.HasDatabaseName("IX_PointsRules_DateRange");
|
||||
|
||||
b.ToTable("PointsRules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate.PointsTransaction", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("积分流水ID");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("积分数量");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ExpiryDate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("过期时间");
|
||||
|
||||
b.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("原因描述");
|
||||
|
||||
b.Property<Guid>("RelatedId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("关联ID");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("来源");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("交易类型(1:获得,2:消费,3:过期,4:退还)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_PointsTransactions_CreatedAt");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
||||
|
||||
b.HasIndex("RelatedId")
|
||||
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("IX_PointsTransactions_Type");
|
||||
|
||||
b.ToTable("PointsTransactions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.ProductAggregate.Product", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<string>("CategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品名称");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId")
|
||||
.HasDatabaseName("IX_Products_CategoryId");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_Products_IsActive");
|
||||
|
||||
b.ToTable("Products", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("兑换订单ID");
|
||||
|
||||
b.Property<string>("CancelReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("取消原因");
|
||||
|
||||
b.Property<int>("ConsumedPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("消耗积分");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("GiftId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品ID");
|
||||
|
||||
b.Property<string>("GiftName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("礼品名称");
|
||||
|
||||
b.Property<int>("GiftType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("礼品类型");
|
||||
|
||||
b.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("会员ID");
|
||||
|
||||
b.Property<string>("OrderNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("订单号");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("数量");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("订单状态(1:待处理,2:已发货,3:已送达,4:已完成,5:已取消)");
|
||||
|
||||
b.Property<string>("TrackingNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("物流单号");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("IX_RedemptionOrders_CreatedAt");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("IX_RedemptionOrders_MemberId");
|
||||
|
||||
b.HasIndex("OrderNo")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_RedemptionOrders_OrderNo");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_RedemptionOrders_Status");
|
||||
|
||||
b.ToTable("RedemptionOrders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Instance")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLockTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("CAPLock", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||
|
||||
b.ToTable("CAPPublishedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||
|
||||
b.ToTable("CAPReceivedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MarketingCodeId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<Guid?>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("CategoryId")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b1.Property<string>("CategoryName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("CategoryName")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b1.Property<Guid>("ProductId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ProductId")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b1.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ProductName")
|
||||
.HasComment("产品名称");
|
||||
|
||||
b1.HasKey("MarketingCodeId");
|
||||
|
||||
b1.ToTable("MarketingCodes");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MarketingCodeId");
|
||||
});
|
||||
|
||||
b.Navigation("ProductInfo")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.Member", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MemberAggregate.MemberLevel", "Level", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("MemberId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<decimal>("BonusRate")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("BonusRate")
|
||||
.HasComment("积分奖励倍率");
|
||||
|
||||
b1.Property<string>("LevelCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LevelCode")
|
||||
.HasComment("等级编码");
|
||||
|
||||
b1.Property<string>("LevelName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LevelName")
|
||||
.HasComment("等级名称");
|
||||
|
||||
b1.Property<int>("RequiredPoints")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("RequiredPoints")
|
||||
.HasComment("所需积分");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("Members");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId");
|
||||
});
|
||||
|
||||
b.Navigation("Level")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.Address", "ShippingAddress", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("RedemptionOrderId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("City")
|
||||
.HasComment("市");
|
||||
|
||||
b1.Property<string>("DetailAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("DetailAddress")
|
||||
.HasComment("详细地址");
|
||||
|
||||
b1.Property<string>("District")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("District")
|
||||
.HasComment("区/县");
|
||||
|
||||
b1.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ReceiverPhone")
|
||||
.HasComment("联系电话");
|
||||
|
||||
b1.Property<string>("Province")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Province")
|
||||
.HasComment("省");
|
||||
|
||||
b1.Property<string>("ReceiverName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ReceiverName")
|
||||
.HasComment("收货人姓名");
|
||||
|
||||
b1.HasKey("RedemptionOrderId");
|
||||
|
||||
b1.ToTable("RedemptionOrders");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("RedemptionOrderId");
|
||||
});
|
||||
|
||||
b.Navigation("ShippingAddress");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddProductAndCategoryAggregates : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "品类ID"),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"),
|
||||
Code = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false, comment: "品类编码"),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false, comment: "排序"),
|
||||
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否激活"),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
|
||||
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Products",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false, comment: "产品ID"),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "产品名称"),
|
||||
CategoryId = table.Column<Guid>(type: "TEXT", nullable: false, comment: "品类ID"),
|
||||
CategoryName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false, comment: "品类名称"),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false, comment: "描述"),
|
||||
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, comment: "是否激活"),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "创建时间"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "更新时间"),
|
||||
Deleted = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RowVersion = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Products", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_Code",
|
||||
table: "Categories",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_IsActive",
|
||||
table: "Categories",
|
||||
column: "IsActive");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Categories_SortOrder",
|
||||
table: "Categories",
|
||||
column: "SortOrder");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Products_CategoryId",
|
||||
table: "Products",
|
||||
column: "CategoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Products_IsActive",
|
||||
table: "Products",
|
||||
column: "IsActive");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Products");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("管理员ID");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("最后登录时间");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("密码哈希");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("管理员状态(1=Active,2=Disabled)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("用户名");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("IX_Admins_Status");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Admins_Username");
|
||||
|
||||
b.ToTable("Admins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.CategoryAggregate.Category", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类编码");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("排序");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("IX_Categories_Code");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_Categories_IsActive");
|
||||
|
||||
b.HasIndex("SortOrder")
|
||||
.HasDatabaseName("IX_Categories_SortOrder");
|
||||
|
||||
b.ToTable("Categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -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<Guid>("Id")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品ID");
|
||||
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类ID");
|
||||
|
||||
b.Property<string>("CategoryName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("品类名称");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("创建时间");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("描述");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("是否激活");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("产品名称");
|
||||
|
||||
b.Property<int>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("更新时间");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId")
|
||||
.HasDatabaseName("IX_Products_CategoryId");
|
||||
|
||||
b.HasIndex("IsActive")
|
||||
.HasDatabaseName("IX_Products_IsActive");
|
||||
|
||||
b.ToTable("Products", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -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<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Instance")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLockTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("CAPLock", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||
|
||||
b.ToTable("CAPPublishedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Added")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Group")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(400)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Retries")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StatusName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||
|
||||
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||
|
||||
b.ToTable("CAPReceivedMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||
{
|
||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员仓储接口
|
||||
/// </summary>
|
||||
public interface IAdminRepository : IRepository<Admin, AdminId>
|
||||
{
|
||||
/// <summary>
|
||||
/// 通过用户名获取管理员
|
||||
/// </summary>
|
||||
Task<Admin?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 检查用户名是否存在
|
||||
/// </summary>
|
||||
Task<bool> ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否存在任何管理员
|
||||
/// </summary>
|
||||
Task<bool> AnyAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员仓储实现
|
||||
/// </summary>
|
||||
public class AdminRepository(ApplicationDbContext context)
|
||||
: RepositoryBase<Admin, AdminId, ApplicationDbContext>(context), IAdminRepository
|
||||
{
|
||||
public async Task<Admin?> GetByUsernameAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Admins
|
||||
.FirstOrDefaultAsync(x => x.Username == username, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByUsernameAsync(string username, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Admins
|
||||
.AnyAsync(x => x.Username == username, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> AnyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Admins.AnyAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
public interface ICategoryRepository : IRepository<Category, CategoryId>
|
||||
{
|
||||
Task<Category?> GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default);
|
||||
Task<Category?> GetByCodeAsync(string code, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class CategoryRepository(ApplicationDbContext context)
|
||||
: RepositoryBase<Category, CategoryId, ApplicationDbContext>(context), ICategoryRepository
|
||||
{
|
||||
public async Task<Category?> GetByIdAsync(CategoryId categoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Categories
|
||||
.FirstOrDefaultAsync(x => x.Id == categoryId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Category?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Categories
|
||||
.FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,8 @@ public interface IPointsRuleRepository : IRepository<PointsRule, PointsRuleId>
|
||||
string? memberLevelCode,
|
||||
DateTime startDate,
|
||||
DateTime? endDate,
|
||||
CancellationToken cancellationToken = default);
|
||||
CancellationToken cancellationToken = default,
|
||||
PointsRuleId? excludeRuleId = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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);
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
|
||||
|
||||
namespace Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
public interface IProductRepository : IRepository<Product, ProductId>
|
||||
{
|
||||
Task<Product?> GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default);
|
||||
Task<Product?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class ProductRepository(ApplicationDbContext context)
|
||||
: RepositoryBase<Product, ProductId, ApplicationDbContext>(context), IProductRepository
|
||||
{
|
||||
public async Task<Product?> GetByIdAsync(ProductId productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Products
|
||||
.FirstOrDefaultAsync(x => x.Id == productId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Product?> GetByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DbContext.Products
|
||||
.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.AdminAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录命令
|
||||
/// </summary>
|
||||
public record AdminLoginCommand(string Username, string Password) : ICommand<AdminLoginResponse>;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录响应
|
||||
/// </summary>
|
||||
public record AdminLoginResponse(AdminId AdminId, string Username, string Token, DateTime ExpiresAt);
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录命令验证器
|
||||
/// </summary>
|
||||
public class AdminLoginCommandValidator : AbstractValidator<AdminLoginCommand>
|
||||
{
|
||||
public AdminLoginCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("用户名不能为空")
|
||||
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("密码不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录命令处理器
|
||||
/// </summary>
|
||||
public class AdminLoginCommandHandler(
|
||||
IAdminRepository adminRepository,
|
||||
IConfiguration configuration)
|
||||
: ICommandHandler<AdminLoginCommand, AdminLoginResponse>
|
||||
{
|
||||
public async Task<AdminLoginResponse> Handle(AdminLoginCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 通过用户名查询管理员
|
||||
var admin = await adminRepository.GetByUsernameAsync(command.Username, cancellationToken);
|
||||
if (admin is null)
|
||||
{
|
||||
throw new KnownException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 2. 检查状态
|
||||
if (admin.Status == AdminStatus.Disabled)
|
||||
{
|
||||
throw new KnownException("账号已被禁用");
|
||||
}
|
||||
|
||||
// 3. 验证密码
|
||||
if (!admin.VerifyPassword(command.Password))
|
||||
{
|
||||
throw new KnownException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 4. 记录登录时间
|
||||
admin.RecordLogin();
|
||||
|
||||
// 5. 生成 JWT Token
|
||||
var token = GenerateJwtToken(admin.Id, admin.Username);
|
||||
var expiresAt = DateTime.UtcNow.AddHours(24);
|
||||
|
||||
return new AdminLoginResponse(admin.Id, admin.Username, token, expiresAt);
|
||||
}
|
||||
|
||||
private string GenerateJwtToken(AdminId adminId, string username)
|
||||
{
|
||||
var appConfig = configuration.GetSection("AppConfiguration").Get<Utils.AppConfiguration>()
|
||||
?? new Utils.AppConfiguration
|
||||
{
|
||||
JwtIssuer = "FenglingBackend",
|
||||
JwtAudience = "FenglingBackend",
|
||||
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!"
|
||||
};
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, adminId.ToString()),
|
||||
new Claim(ClaimTypes.Name, username),
|
||||
new Claim(ClaimTypes.Role, "Admin"),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
// 使用对称密钥
|
||||
var secret = appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!";
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
|
||||
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: appConfig.JwtIssuer,
|
||||
audience: appConfig.JwtAudience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(24),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.AdminAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化默认管理员命令
|
||||
/// </summary>
|
||||
public record InitializeDefaultAdminCommand : ICommand;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化默认管理员命令处理器
|
||||
/// </summary>
|
||||
public class InitializeDefaultAdminCommandHandler(
|
||||
IAdminRepository adminRepository,
|
||||
IConfiguration configuration,
|
||||
ILogger<InitializeDefaultAdminCommandHandler> logger)
|
||||
: ICommandHandler<InitializeDefaultAdminCommand>
|
||||
{
|
||||
public async Task Handle(InitializeDefaultAdminCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
// 检查是否已存在管理员
|
||||
if (await adminRepository.AnyAsync(cancellationToken))
|
||||
{
|
||||
logger.LogInformation("管理员账号已存在,跳过初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从配置读取默认管理员信息
|
||||
var username = configuration.GetValue<string>("AppConfiguration:DefaultAdmin:Username") ?? "admin";
|
||||
var password = configuration.GetValue<string>("AppConfiguration:DefaultAdmin:Password") ?? "Admin@123";
|
||||
|
||||
// 创建默认管理员
|
||||
var admin = Admin.Create(username, password);
|
||||
await adminRepository.AddAsync(admin, cancellationToken);
|
||||
|
||||
logger.LogInformation("默认管理员账号已初始化, 用户名: {Username}", username);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.Categories;
|
||||
|
||||
/// <summary>
|
||||
/// 创建品类命令
|
||||
/// </summary>
|
||||
public record CreateCategoryCommand(
|
||||
string Name,
|
||||
string Code,
|
||||
string? Description = null,
|
||||
int SortOrder = 0) : ICommand<CategoryId>;
|
||||
|
||||
public class CreateCategoryCommandValidator : AbstractValidator<CreateCategoryCommand>
|
||||
{
|
||||
public CreateCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateCategoryCommandHandler(ICategoryRepository categoryRepository)
|
||||
: ICommandHandler<CreateCategoryCommand, CategoryId>
|
||||
{
|
||||
public async Task<CategoryId> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var existingCategory = await categoryRepository.GetByCodeAsync(request.Code, cancellationToken);
|
||||
if (existingCategory != null)
|
||||
throw new KnownException("品类编码已存在");
|
||||
|
||||
var category = new Category(
|
||||
request.Name,
|
||||
request.Code,
|
||||
request.Description,
|
||||
request.SortOrder);
|
||||
|
||||
await categoryRepository.AddAsync(category, cancellationToken);
|
||||
return category.Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新品类命令
|
||||
/// </summary>
|
||||
public record UpdateCategoryCommand(
|
||||
CategoryId CategoryId,
|
||||
string? Name = null,
|
||||
string? Description = null,
|
||||
int? SortOrder = null) : ICommand<ResponseData>;
|
||||
|
||||
public class UpdateCategoryCommandValidator : AbstractValidator<UpdateCategoryCommand>
|
||||
{
|
||||
public UpdateCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.CategoryId).NotEmpty();
|
||||
RuleFor(x => x.Name).MaximumLength(100);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateCategoryCommandHandler(ICategoryRepository categoryRepository)
|
||||
: ICommandHandler<UpdateCategoryCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> Handle(UpdateCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(request.CategoryId, cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
throw new KnownException("品类不存在");
|
||||
|
||||
category.UpdateInfo(request.Name, request.Description, request.SortOrder);
|
||||
await categoryRepository.UpdateAsync(category, cancellationToken);
|
||||
|
||||
return new ResponseData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除品类命令
|
||||
/// </summary>
|
||||
public record DeleteCategoryCommand(CategoryId CategoryId) : ICommand<ResponseData>;
|
||||
|
||||
public class DeleteCategoryCommandHandler(ICategoryRepository categoryRepository)
|
||||
: ICommandHandler<DeleteCategoryCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> Handle(DeleteCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(request.CategoryId, cancellationToken);
|
||||
|
||||
if (category == null)
|
||||
throw new KnownException("品类不存在");
|
||||
|
||||
await categoryRepository.DeleteAsync(category);
|
||||
return new ResponseData();
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ namespace Fengling.Backend.Web.Application.Commands.Gifts;
|
||||
/// </summary>
|
||||
public record CreateGiftCommand(
|
||||
string Name,
|
||||
int Type,
|
||||
GiftType Type,
|
||||
string Description,
|
||||
string ImageUrl,
|
||||
int RequiredPoints,
|
||||
@ -20,7 +20,7 @@ public class CreateGiftCommandValidator : AbstractValidator<CreateGiftCommand>
|
||||
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<GiftId> 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)
|
||||
/// 更新礼品命令
|
||||
/// </summary>
|
||||
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<ResponseData> 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)
|
||||
/// <summary>
|
||||
/// 上架礼品命令
|
||||
/// </summary>
|
||||
public record PutOnShelfCommand(Guid GiftId) : ICommand<ResponseData>;
|
||||
public record PutOnShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
|
||||
|
||||
public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
|
||||
: ICommandHandler<PutOnShelfCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> 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)
|
||||
/// <summary>
|
||||
/// 下架礼品命令
|
||||
/// </summary>
|
||||
public record PutOffShelfCommand(Guid GiftId) : ICommand<ResponseData>;
|
||||
public record PutOffShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
|
||||
|
||||
public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
|
||||
: ICommandHandler<PutOffShelfCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> 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)
|
||||
/// <summary>
|
||||
/// 增加库存命令
|
||||
/// </summary>
|
||||
public record AddGiftStockCommand(Guid GiftId, int Quantity) : ICommand<ResponseData>;
|
||||
public record AddGiftStockCommand(GiftId GiftId, int Quantity) : ICommand<ResponseData>;
|
||||
|
||||
public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand>
|
||||
{
|
||||
@ -162,8 +157,7 @@ public class AddGiftStockCommandHandler(IGiftRepository giftRepository)
|
||||
{
|
||||
public async Task<ResponseData> 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("礼品不存在");
|
||||
|
||||
@ -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<LoginMemberCommand>
|
||||
/// 会员登录命令处理器
|
||||
/// </summary>
|
||||
public class LoginMemberCommandHandler(
|
||||
IMemberRepository memberRepository)
|
||||
IMemberRepository memberRepository,
|
||||
IConfiguration configuration)
|
||||
: ICommandHandler<LoginMemberCommand, LoginMemberResponse>
|
||||
{
|
||||
public async Task<LoginMemberResponse> 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<AppConfiguration>()
|
||||
?? 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
|
||||
|
||||
/// <summary>
|
||||
/// 激活积分规则命令
|
||||
/// </summary>
|
||||
public record ActivatePointsRuleCommand(PointsRuleId RuleId) : ICommand;
|
||||
|
||||
/// <summary>
|
||||
/// 激活积分规则命令验证器
|
||||
/// </summary>
|
||||
public class ActivatePointsRuleCommandValidator : AbstractValidator<ActivatePointsRuleCommand>
|
||||
{
|
||||
public ActivatePointsRuleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RuleId)
|
||||
.NotNull().WithMessage("规则ID不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活积分规则命令处理器
|
||||
/// </summary>
|
||||
public class ActivatePointsRuleCommandHandler(
|
||||
IPointsRuleRepository pointsRuleRepository)
|
||||
: ICommandHandler<ActivatePointsRuleCommand>
|
||||
{
|
||||
public async Task Handle(ActivatePointsRuleCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
|
||||
if (rule == null)
|
||||
{
|
||||
throw new KnownException("积分规则不存在");
|
||||
}
|
||||
|
||||
rule.Activate();
|
||||
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -55,7 +55,8 @@ public class CreatePointsRuleCommandHandler(
|
||||
command.MemberLevelCode,
|
||||
command.StartDate,
|
||||
command.EndDate,
|
||||
cancellationToken))
|
||||
cancellationToken,
|
||||
null)) // 新创建的规则,不需要排除任何规则
|
||||
{
|
||||
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
|
||||
}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
|
||||
|
||||
/// <summary>
|
||||
/// 停用积分规则命令
|
||||
/// </summary>
|
||||
public record DeactivatePointsRuleCommand(PointsRuleId RuleId) : ICommand;
|
||||
|
||||
/// <summary>
|
||||
/// 停用积分规则命令验证器
|
||||
/// </summary>
|
||||
public class DeactivatePointsRuleCommandValidator : AbstractValidator<DeactivatePointsRuleCommand>
|
||||
{
|
||||
public DeactivatePointsRuleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RuleId)
|
||||
.NotNull().WithMessage("规则ID不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用积分规则命令处理器
|
||||
/// </summary>
|
||||
public class DeactivatePointsRuleCommandHandler(
|
||||
IPointsRuleRepository pointsRuleRepository)
|
||||
: ICommandHandler<DeactivatePointsRuleCommand>
|
||||
{
|
||||
public async Task Handle(DeactivatePointsRuleCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
|
||||
if (rule == null)
|
||||
{
|
||||
throw new KnownException("积分规则不存在");
|
||||
}
|
||||
|
||||
rule.Deactivate();
|
||||
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.PointsRules;
|
||||
|
||||
/// <summary>
|
||||
/// 更新积分规则命令
|
||||
/// </summary>
|
||||
public record UpdatePointsRuleCommand(
|
||||
PointsRuleId RuleId,
|
||||
string? RuleName = null,
|
||||
int? PointsValue = null,
|
||||
decimal? BonusMultiplier = null,
|
||||
DateTime? StartDate = null,
|
||||
DateTime? EndDate = null) : ICommand;
|
||||
|
||||
/// <summary>
|
||||
/// 更新积分规则命令验证器
|
||||
/// </summary>
|
||||
public class UpdatePointsRuleCommandValidator : AbstractValidator<UpdatePointsRuleCommand>
|
||||
{
|
||||
public UpdatePointsRuleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.RuleId)
|
||||
.NotNull().WithMessage("规则ID不能为空");
|
||||
|
||||
RuleFor(x => x.RuleName)
|
||||
.MaximumLength(100).WithMessage("规则名称最多100个字符")
|
||||
.When(x => !string.IsNullOrEmpty(x.RuleName));
|
||||
|
||||
RuleFor(x => x.PointsValue)
|
||||
.GreaterThan(0).WithMessage("积分值必须大于0")
|
||||
.When(x => x.PointsValue.HasValue);
|
||||
|
||||
RuleFor(x => x.BonusMultiplier)
|
||||
.GreaterThan(0).WithMessage("奖励倍数必须大于0")
|
||||
.When(x => x.BonusMultiplier.HasValue);
|
||||
|
||||
RuleFor(x => x.StartDate)
|
||||
.LessThan(x => x.EndDate).WithMessage("开始时间必须早于结束时间")
|
||||
.When(x => x.StartDate.HasValue && x.EndDate.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新积分规则命令处理器
|
||||
/// </summary>
|
||||
public class UpdatePointsRuleCommandHandler(
|
||||
IPointsRuleRepository pointsRuleRepository)
|
||||
: ICommandHandler<UpdatePointsRuleCommand>
|
||||
{
|
||||
public async Task Handle(UpdatePointsRuleCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
var rule = await pointsRuleRepository.GetAsync(command.RuleId, cancellationToken);
|
||||
if (rule == null)
|
||||
{
|
||||
throw new KnownException("积分规则不存在");
|
||||
}
|
||||
|
||||
// 如果有修改时间范围,检查是否存在冲突的规则
|
||||
if (command.StartDate.HasValue || command.EndDate.HasValue)
|
||||
{
|
||||
var newStartDate = command.StartDate ?? rule.StartDate;
|
||||
var newEndDate = command.EndDate ?? rule.EndDate;
|
||||
|
||||
if (await pointsRuleRepository.HasConflictingRuleAsync(
|
||||
rule.ProductId,
|
||||
rule.CategoryId,
|
||||
rule.MemberLevelCode,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
cancellationToken,
|
||||
rule.Id))
|
||||
{
|
||||
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新规则
|
||||
rule.Update(
|
||||
command.RuleName,
|
||||
command.PointsValue,
|
||||
command.BonusMultiplier,
|
||||
command.StartDate,
|
||||
command.EndDate);
|
||||
|
||||
await pointsRuleRepository.UpdateAsync(rule, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Commands.Products;
|
||||
|
||||
/// <summary>
|
||||
/// 创建产品命令
|
||||
/// </summary>
|
||||
public record CreateProductCommand(
|
||||
string Name,
|
||||
Guid CategoryId,
|
||||
string CategoryName,
|
||||
string? Description = null) : ICommand<ProductId>;
|
||||
|
||||
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
|
||||
{
|
||||
public CreateProductCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.CategoryId).NotEmpty();
|
||||
RuleFor(x => x.CategoryName).NotEmpty().MaximumLength(100);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateProductCommandHandler(IProductRepository productRepository)
|
||||
: ICommandHandler<CreateProductCommand, ProductId>
|
||||
{
|
||||
public async Task<ProductId> Handle(CreateProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = new Product(
|
||||
request.Name,
|
||||
request.CategoryId,
|
||||
request.CategoryName,
|
||||
request.Description);
|
||||
|
||||
await productRepository.AddAsync(product, cancellationToken);
|
||||
return product.Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新产品命令
|
||||
/// </summary>
|
||||
public record UpdateProductCommand(
|
||||
ProductId ProductId,
|
||||
string? Name = null,
|
||||
Guid? CategoryId = null,
|
||||
string? CategoryName = null,
|
||||
string? Description = null) : ICommand<ResponseData>;
|
||||
|
||||
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
|
||||
{
|
||||
public UpdateProductCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProductId).NotEmpty();
|
||||
RuleFor(x => x.Name).MaximumLength(100);
|
||||
RuleFor(x => x.CategoryName).MaximumLength(100);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateProductCommandHandler(IProductRepository productRepository)
|
||||
: ICommandHandler<UpdateProductCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = await productRepository.GetByIdAsync(request.ProductId, cancellationToken);
|
||||
|
||||
if (product == null)
|
||||
throw new KnownException("产品不存在");
|
||||
|
||||
product.UpdateInfo(request.Name, request.CategoryId, request.CategoryName, request.Description);
|
||||
await productRepository.UpdateAsync(product, cancellationToken);
|
||||
|
||||
return new ResponseData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除产品命令
|
||||
/// </summary>
|
||||
public record DeleteProductCommand(ProductId ProductId) : ICommand<ResponseData>;
|
||||
|
||||
public class DeleteProductCommandHandler(IProductRepository productRepository)
|
||||
: ICommandHandler<DeleteProductCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = await productRepository.GetByIdAsync(request.ProductId, cancellationToken);
|
||||
|
||||
if (product == null)
|
||||
throw new KnownException("产品不存在");
|
||||
|
||||
await productRepository.DeleteAsync(product);
|
||||
return new ResponseData();
|
||||
}
|
||||
}
|
||||
@ -9,12 +9,12 @@ namespace Fengling.Backend.Web.Application.Commands.RedemptionOrders;
|
||||
/// 创建兑换订单命令
|
||||
/// </summary>
|
||||
public record CreateRedemptionOrderCommand(
|
||||
Guid MemberId,
|
||||
Guid GiftId,
|
||||
MemberId MemberId,
|
||||
GiftId GiftId,
|
||||
int Quantity,
|
||||
AddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
|
||||
CreateRedemptionOrderAddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
|
||||
|
||||
public record AddressDto(
|
||||
public record CreateRedemptionOrderAddressDto(
|
||||
string ReceiverName,
|
||||
string Phone,
|
||||
string Province,
|
||||
@ -30,8 +30,7 @@ public class CreateRedemptionOrderCommandHandler(
|
||||
public async Task<RedemptionOrderId> 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(
|
||||
/// <summary>
|
||||
/// 标记订单为已发货命令
|
||||
/// </summary>
|
||||
public record MarkOrderAsDispatchedCommand(Guid OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
|
||||
public record MarkOrderAsDispatchedCommand(RedemptionOrderId OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
|
||||
|
||||
public class MarkOrderAsDispatchedCommandHandler(
|
||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> 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(
|
||||
/// <summary>
|
||||
/// 完成订单命令
|
||||
/// </summary>
|
||||
public record CompleteOrderCommand(Guid OrderId) : ICommand<ResponseData>;
|
||||
public record CompleteOrderCommand(RedemptionOrderId OrderId) : ICommand<ResponseData>;
|
||||
|
||||
public class CompleteOrderCommandHandler(
|
||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> 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(
|
||||
/// <summary>
|
||||
/// 取消订单命令
|
||||
/// </summary>
|
||||
public record CancelOrderCommand(Guid OrderId, string Reason) : ICommand<ResponseData>;
|
||||
public record CancelOrderCommand(RedemptionOrderId OrderId, string Reason) : ICommand<ResponseData>;
|
||||
|
||||
public class CancelOrderCommandHandler(
|
||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData>
|
||||
{
|
||||
public async Task<ResponseData> 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("订单不存在");
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.AdminAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前管理员查询
|
||||
/// </summary>
|
||||
public record GetCurrentAdminQuery(AdminId AdminId) : IQuery<AdminDto>;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员DTO
|
||||
/// </summary>
|
||||
public record AdminDto(
|
||||
AdminId AdminId,
|
||||
string Username,
|
||||
string Status,
|
||||
DateTime? LastLoginAt,
|
||||
DateTime CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前管理员查询处理器
|
||||
/// </summary>
|
||||
public class GetCurrentAdminQueryHandler(ApplicationDbContext context)
|
||||
: IQueryHandler<GetCurrentAdminQuery, AdminDto>
|
||||
{
|
||||
public async Task<AdminDto> Handle(GetCurrentAdminQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var admin = await context.Admins
|
||||
.Where(x => x.Id == request.AdminId)
|
||||
.Select(x => new AdminDto(
|
||||
x.Id,
|
||||
x.Username,
|
||||
x.Status == AdminStatus.Active ? "Active" : "Disabled",
|
||||
x.LastLoginAt,
|
||||
x.CreatedAt))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (admin is null)
|
||||
{
|
||||
throw new KnownException($"未找到管理员,AdminId = {request.AdminId}");
|
||||
}
|
||||
|
||||
return admin;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.Categories;
|
||||
|
||||
public record CategoryDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Code { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public int SortOrder { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询品类列表
|
||||
/// </summary>
|
||||
public record GetCategoriesQuery(bool? IsActive = null) : IQuery<List<CategoryDto>>;
|
||||
|
||||
public class GetCategoriesQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetCategoriesQuery, List<CategoryDto>>
|
||||
{
|
||||
public async Task<List<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.Categories.AsQueryable();
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsActive == request.IsActive.Value);
|
||||
}
|
||||
|
||||
var categories = await query
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Name)
|
||||
.Select(x => new CategoryDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
Name = x.Name,
|
||||
Code = x.Code,
|
||||
Description = x.Description,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询品类详情
|
||||
/// </summary>
|
||||
public record GetCategoryByIdQuery(Guid CategoryId) : IQuery<CategoryDto?>;
|
||||
|
||||
public class GetCategoryByIdQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetCategoryByIdQuery, CategoryDto?>
|
||||
{
|
||||
public async Task<CategoryDto?> Handle(GetCategoryByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = await dbContext.Categories
|
||||
.Where(x => x.Id.Id == request.CategoryId)
|
||||
.Select(x => new CategoryDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
Name = x.Name,
|
||||
Code = x.Code,
|
||||
Description = x.Description,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return category;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// 礼品详情查询
|
||||
/// </summary>
|
||||
public record GetGiftByIdQuery(Guid GiftId) : IQuery<GiftDto?>;
|
||||
public record GetGiftByIdQuery(GiftId GiftId) : IQuery<GiftDto?>;
|
||||
|
||||
public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?>
|
||||
{
|
||||
public async Task<GiftDto?> 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,
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.MarketingCodes;
|
||||
|
||||
/// <summary>
|
||||
/// 营销码DTO
|
||||
/// </summary>
|
||||
public record MarketingCodeDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Code { get; init; } = string.Empty;
|
||||
public string BatchNo { get; init; } = string.Empty;
|
||||
public Guid ProductId { get; init; }
|
||||
public string ProductName { get; init; } = string.Empty;
|
||||
public Guid? CategoryId { get; init; }
|
||||
public string? CategoryName { get; init; }
|
||||
public bool IsUsed { get; init; }
|
||||
public Guid? UsedByMemberId { get; init; }
|
||||
public DateTime? UsedAt { get; init; }
|
||||
public DateTime? ExpiryDate { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批次信息DTO
|
||||
/// </summary>
|
||||
public record MarketingCodeBatchDto
|
||||
{
|
||||
public string BatchNo { get; init; } = string.Empty;
|
||||
public Guid ProductId { get; init; }
|
||||
public string ProductName { get; init; } = string.Empty;
|
||||
public Guid? CategoryId { get; init; }
|
||||
public string? CategoryName { get; init; }
|
||||
public int TotalCount { get; init; }
|
||||
public int UsedCount { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime? ExpiryDate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询营销码列表
|
||||
/// </summary>
|
||||
public record GetMarketingCodesQuery(
|
||||
string? BatchNo = null,
|
||||
Guid? ProductId = null,
|
||||
bool? IsUsed = null,
|
||||
DateTime? StartDate = null,
|
||||
DateTime? EndDate = null) : IQuery<List<MarketingCodeDto>>;
|
||||
|
||||
public class GetMarketingCodesQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetMarketingCodesQuery, List<MarketingCodeDto>>
|
||||
{
|
||||
public async Task<List<MarketingCodeDto>> Handle(GetMarketingCodesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.MarketingCodes.AsQueryable();
|
||||
|
||||
// 按批次号筛选
|
||||
if (!string.IsNullOrWhiteSpace(request.BatchNo))
|
||||
{
|
||||
query = query.Where(x => x.BatchNo == request.BatchNo);
|
||||
}
|
||||
|
||||
// 按产品ID筛选
|
||||
if (request.ProductId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.ProductInfo.ProductId == request.ProductId.Value);
|
||||
}
|
||||
|
||||
// 按使用状态筛选
|
||||
if (request.IsUsed.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsUsed == request.IsUsed.Value);
|
||||
}
|
||||
|
||||
// 按创建时间范围筛选
|
||||
if (request.StartDate.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CreatedAt >= request.StartDate.Value);
|
||||
}
|
||||
|
||||
if (request.EndDate.HasValue)
|
||||
{
|
||||
// EndDate包含当天整天
|
||||
var endOfDay = request.EndDate.Value.Date.AddDays(1);
|
||||
query = query.Where(x => x.CreatedAt < endOfDay);
|
||||
}
|
||||
|
||||
var marketingCodes = await query
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(x => new MarketingCodeDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
Code = x.Code,
|
||||
BatchNo = x.BatchNo,
|
||||
ProductId = x.ProductInfo.ProductId,
|
||||
ProductName = x.ProductInfo.ProductName,
|
||||
CategoryId = x.ProductInfo.CategoryId,
|
||||
CategoryName = x.ProductInfo.CategoryName,
|
||||
IsUsed = x.IsUsed,
|
||||
UsedByMemberId = x.UsedByMemberId,
|
||||
UsedAt = x.UsedAt,
|
||||
ExpiryDate = x.ExpiryDate,
|
||||
CreatedAt = x.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return marketingCodes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询所有批次列表
|
||||
/// </summary>
|
||||
public record GetMarketingCodeBatchesQuery : IQuery<List<MarketingCodeBatchDto>>;
|
||||
|
||||
public class GetMarketingCodeBatchesQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetMarketingCodeBatchesQuery, List<MarketingCodeBatchDto>>
|
||||
{
|
||||
public async Task<List<MarketingCodeBatchDto>> Handle(GetMarketingCodeBatchesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var batches = await dbContext.MarketingCodes
|
||||
.GroupBy(x => new
|
||||
{
|
||||
x.BatchNo,
|
||||
x.ProductInfo.ProductId,
|
||||
x.ProductInfo.ProductName,
|
||||
x.ProductInfo.CategoryId,
|
||||
x.ProductInfo.CategoryName,
|
||||
x.ExpiryDate
|
||||
})
|
||||
.Select(g => new MarketingCodeBatchDto
|
||||
{
|
||||
BatchNo = g.Key.BatchNo,
|
||||
ProductId = g.Key.ProductId,
|
||||
ProductName = g.Key.ProductName,
|
||||
CategoryId = g.Key.CategoryId,
|
||||
CategoryName = g.Key.CategoryName,
|
||||
TotalCount = g.Count(),
|
||||
UsedCount = g.Count(x => x.IsUsed),
|
||||
CreatedAt = g.Min(x => x.CreatedAt),
|
||||
ExpiryDate = g.Key.ExpiryDate
|
||||
})
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return batches;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.PointsRules;
|
||||
|
||||
/// <summary>
|
||||
/// 查询所有积分规则
|
||||
/// </summary>
|
||||
public record GetAllPointsRulesQuery(bool? IsActive = null, int? RuleType = null) : IQuery<List<PointsRuleDto>>;
|
||||
|
||||
public record PointsRuleDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string RuleName { get; init; } = string.Empty;
|
||||
public int RuleType { get; init; }
|
||||
public int PointsValue { get; init; }
|
||||
public decimal BonusMultiplier { get; init; }
|
||||
public DateTime StartDate { get; init; }
|
||||
public DateTime? EndDate { get; init; }
|
||||
public Guid? ProductId { get; init; }
|
||||
public Guid? CategoryId { get; init; }
|
||||
public string? MemberLevelCode { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
public class GetAllPointsRulesQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetAllPointsRulesQuery, List<PointsRuleDto>>
|
||||
{
|
||||
public async Task<List<PointsRuleDto>> Handle(GetAllPointsRulesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.PointsRules.AsQueryable();
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsActive == request.IsActive.Value);
|
||||
}
|
||||
|
||||
if (request.RuleType.HasValue)
|
||||
{
|
||||
query = query.Where(x => (int)x.RuleType == request.RuleType.Value);
|
||||
}
|
||||
|
||||
var rules = await query
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(x => new PointsRuleDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
RuleName = x.RuleName,
|
||||
RuleType = (int)x.RuleType,
|
||||
PointsValue = x.PointsValue,
|
||||
BonusMultiplier = x.BonusMultiplier,
|
||||
StartDate = x.StartDate,
|
||||
EndDate = x.EndDate,
|
||||
ProductId = x.ProductId,
|
||||
CategoryId = x.CategoryId,
|
||||
MemberLevelCode = x.MemberLevelCode,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID查询积分规则详情
|
||||
/// </summary>
|
||||
public record GetPointsRuleByIdQuery(PointsRuleId RuleId) : IQuery<PointsRuleDto?>;
|
||||
|
||||
public class GetPointsRuleByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetPointsRuleByIdQuery, PointsRuleDto?>
|
||||
{
|
||||
public async Task<PointsRuleDto?> Handle(GetPointsRuleByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var rule = await dbContext.PointsRules
|
||||
.Where(x => x.Id == request.RuleId)
|
||||
.Select(x => new PointsRuleDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
RuleName = x.RuleName,
|
||||
RuleType = (int)x.RuleType,
|
||||
PointsValue = x.PointsValue,
|
||||
BonusMultiplier = x.BonusMultiplier,
|
||||
StartDate = x.StartDate,
|
||||
EndDate = x.EndDate,
|
||||
ProductId = x.ProductId,
|
||||
CategoryId = x.CategoryId,
|
||||
MemberLevelCode = x.MemberLevelCode,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.PointsTransactions;
|
||||
|
||||
/// <summary>
|
||||
/// 获取会员积分流水查询
|
||||
/// </summary>
|
||||
public record GetPointsTransactionsQuery(Guid MemberId, int? Type = null) : IQuery<List<PointsTransactionDto>>;
|
||||
|
||||
public record PointsTransactionDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid MemberId { get; init; }
|
||||
public int Type { get; init; }
|
||||
public int Amount { get; init; }
|
||||
public string Source { get; init; } = string.Empty;
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public Guid RelatedId { get; init; }
|
||||
public DateTime? ExpiryDate { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
public class GetPointsTransactionsQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetPointsTransactionsQuery, List<PointsTransactionDto>>
|
||||
{
|
||||
public async Task<List<PointsTransactionDto>> Handle(GetPointsTransactionsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.PointsTransactions.AsQueryable();
|
||||
|
||||
query = query.Where(x => x.MemberId == new MemberId(request.MemberId));
|
||||
|
||||
if (request.Type.HasValue)
|
||||
{
|
||||
query = query.Where(x => (int)x.Type == request.Type.Value);
|
||||
}
|
||||
|
||||
var transactions = await query
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(x => new PointsTransactionDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
MemberId = x.MemberId.Id,
|
||||
Type = (int)x.Type,
|
||||
Amount = x.Amount,
|
||||
Source = x.Source,
|
||||
Reason = x.Reason,
|
||||
RelatedId = x.RelatedId,
|
||||
ExpiryDate = x.ExpiryDate,
|
||||
CreatedAt = x.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
using Fengling.Backend.Infrastructure;
|
||||
|
||||
namespace Fengling.Backend.Web.Application.Queries.Products;
|
||||
|
||||
public record ProductDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public Guid CategoryId { get; init; }
|
||||
public string CategoryName { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public bool IsActive { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public DateTime UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询产品列表
|
||||
/// </summary>
|
||||
public record GetProductsQuery(Guid? CategoryId = null, bool? IsActive = null) : IQuery<List<ProductDto>>;
|
||||
|
||||
public class GetProductsQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetProductsQuery, List<ProductDto>>
|
||||
{
|
||||
public async Task<List<ProductDto>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = dbContext.Products.AsQueryable();
|
||||
|
||||
if (request.CategoryId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CategoryId == request.CategoryId.Value);
|
||||
}
|
||||
|
||||
if (request.IsActive.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.IsActive == request.IsActive.Value);
|
||||
}
|
||||
|
||||
var products = await query
|
||||
.OrderBy(x => x.Name)
|
||||
.Select(x => new ProductDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
Name = x.Name,
|
||||
CategoryId = x.CategoryId,
|
||||
CategoryName = x.CategoryName,
|
||||
Description = x.Description,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return products;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询产品详情
|
||||
/// </summary>
|
||||
public record GetProductByIdQuery(Guid ProductId) : IQuery<ProductDto?>;
|
||||
|
||||
public class GetProductByIdQueryHandler(ApplicationDbContext dbContext)
|
||||
: IQueryHandler<GetProductByIdQuery, ProductDto?>
|
||||
{
|
||||
public async Task<ProductDto?> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = await dbContext.Products
|
||||
.Where(x => x.Id.Id == request.ProductId)
|
||||
.Select(x => new ProductDto
|
||||
{
|
||||
Id = x.Id.Id,
|
||||
Name = x.Name,
|
||||
CategoryId = x.CategoryId,
|
||||
CategoryName = x.CategoryName,
|
||||
Description = x.Description,
|
||||
IsActive = x.IsActive,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return product;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
# Uploads Directory
|
||||
@ -0,0 +1,71 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Web.Application.Commands.Categories;
|
||||
using Fengling.Backend.Web.Application.Queries.Categories;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin.Categories;
|
||||
|
||||
[Tags("Admin/Categories")]
|
||||
[HttpGet("/api/admin/categories")]
|
||||
[AllowAnonymous]
|
||||
public class GetCategoriesEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<List<CategoryDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var query = new GetCategoriesQuery();
|
||||
var result = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Categories")]
|
||||
[HttpGet("/api/admin/categories/{CategoryId}")]
|
||||
[AllowAnonymous]
|
||||
public class GetCategoryByIdEndpoint(IMediator mediator)
|
||||
: Endpoint<GetCategoryByIdQuery, ResponseData<CategoryDto?>>
|
||||
{
|
||||
public override async Task HandleAsync(GetCategoryByIdQuery req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Categories")]
|
||||
[HttpPost("/api/admin/categories")]
|
||||
[AllowAnonymous]
|
||||
public class CreateCategoryEndpoint(IMediator mediator)
|
||||
: Endpoint<CreateCategoryCommand, ResponseData<Guid>>
|
||||
{
|
||||
public override async Task HandleAsync(CreateCategoryCommand req, CancellationToken ct)
|
||||
{
|
||||
var categoryId = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(categoryId.Id.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Categories")]
|
||||
[HttpPut("/api/admin/categories/{CategoryId}")]
|
||||
[AllowAnonymous]
|
||||
public class UpdateCategoryEndpoint(IMediator mediator)
|
||||
: Endpoint<UpdateCategoryCommand, ResponseData>
|
||||
{
|
||||
public override async Task HandleAsync(UpdateCategoryCommand req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Categories")]
|
||||
[HttpDelete("/api/admin/categories/{CategoryId}")]
|
||||
[AllowAnonymous]
|
||||
public class DeleteCategoryEndpoint(IMediator mediator)
|
||||
: Endpoint<DeleteCategoryCommand, ResponseData>
|
||||
{
|
||||
public override async Task HandleAsync(DeleteCategoryCommand req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Web.Application.Queries.PointsRules;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 获取积分规则列表端点
|
||||
/// </summary>
|
||||
[Tags("Admin-PointsRules")]
|
||||
[HttpGet("/api/admin/points-rules")]
|
||||
[AllowAnonymous]
|
||||
public class GetPointsRulesEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<List<PointsRuleDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var query = new GetAllPointsRulesQuery();
|
||||
var rules = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(rules.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取积分规则详情端点
|
||||
/// </summary>
|
||||
[Tags("Admin-PointsRules")]
|
||||
[HttpGet("/api/admin/points-rules/{id}")]
|
||||
[AllowAnonymous]
|
||||
public class GetPointsRuleByIdEndpoint(IMediator mediator)
|
||||
: Endpoint<EmptyRequest, ResponseData<PointsRuleDto?>>
|
||||
{
|
||||
public override async Task HandleAsync(EmptyRequest _, CancellationToken ct)
|
||||
{
|
||||
var id = Route<PointsRuleId>("id");
|
||||
var query = new GetPointsRuleByIdQuery(id!);
|
||||
var rule = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(rule.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Web.Application.Queries.MarketingCodes;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin.MarketingCodes;
|
||||
|
||||
/// <summary>
|
||||
/// 查询营销码请求
|
||||
/// </summary>
|
||||
public record GetMarketingCodesRequest
|
||||
{
|
||||
public string? BatchNo { get; init; }
|
||||
public Guid? ProductId { get; init; }
|
||||
public bool? IsUsed { get; init; }
|
||||
public DateTime? StartDate { get; init; }
|
||||
public DateTime? EndDate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询营销码列表端点
|
||||
/// </summary>
|
||||
[Tags("Admin-MarketingCodes")]
|
||||
[HttpGet("/api/admin/marketing-codes")]
|
||||
[AllowAnonymous]
|
||||
public class GetMarketingCodesEndpoint(IMediator mediator)
|
||||
: Endpoint<GetMarketingCodesRequest, ResponseData<List<MarketingCodeDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(GetMarketingCodesRequest req, CancellationToken ct)
|
||||
{
|
||||
var query = new GetMarketingCodesQuery(
|
||||
req.BatchNo,
|
||||
req.ProductId,
|
||||
req.IsUsed,
|
||||
req.StartDate,
|
||||
req.EndDate);
|
||||
|
||||
var result = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询营销码批次列表端点
|
||||
/// </summary>
|
||||
[Tags("Admin-MarketingCodes")]
|
||||
[HttpGet("/api/admin/marketing-codes/batches")]
|
||||
[AllowAnonymous]
|
||||
public class GetMarketingCodeBatchesEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<List<MarketingCodeBatchDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var query = new GetMarketingCodeBatchesQuery();
|
||||
var result = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Web.Application.Commands.PointsRules;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 激活积分规则端点
|
||||
/// </summary>
|
||||
[Tags("Admin-PointsRules")]
|
||||
[HttpPost("/api/admin/points-rules/{id}/activate")]
|
||||
[AllowAnonymous]
|
||||
public class ActivatePointsRuleEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<bool>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var id = Route<Guid>("id");
|
||||
var ruleId = new PointsRuleId(id);
|
||||
var command = new ActivatePointsRuleCommand(ruleId);
|
||||
await mediator.Send(command, ct);
|
||||
await Send.OkAsync(true.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用积分规则端点
|
||||
/// </summary>
|
||||
[Tags("Admin-PointsRules")]
|
||||
[HttpPost("/api/admin/points-rules/{id}/deactivate")]
|
||||
[AllowAnonymous]
|
||||
public class DeactivatePointsRuleEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<bool>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var id = Route<Guid>("id");
|
||||
var ruleId = new PointsRuleId(id);
|
||||
var command = new DeactivatePointsRuleCommand(ruleId);
|
||||
await mediator.Send(command, ct);
|
||||
await Send.OkAsync(true.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Web.Application.Commands.Products;
|
||||
using Fengling.Backend.Web.Application.Queries.Products;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin.Products;
|
||||
|
||||
[Tags("Admin/Products")]
|
||||
[HttpGet("/api/admin/products")]
|
||||
[AllowAnonymous]
|
||||
public class GetProductsEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<List<ProductDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var query = new GetProductsQuery();
|
||||
var result = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Products")]
|
||||
[HttpGet("/api/admin/products/{ProductId}")]
|
||||
[AllowAnonymous]
|
||||
public class GetProductByIdEndpoint(IMediator mediator)
|
||||
: Endpoint<GetProductByIdQuery, ResponseData<ProductDto?>>
|
||||
{
|
||||
public override async Task HandleAsync(GetProductByIdQuery req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Products")]
|
||||
[HttpPost("/api/admin/products")]
|
||||
[AllowAnonymous]
|
||||
public class CreateProductEndpoint(IMediator mediator)
|
||||
: Endpoint<CreateProductCommand, ResponseData<Guid>>
|
||||
{
|
||||
public override async Task HandleAsync(CreateProductCommand req, CancellationToken ct)
|
||||
{
|
||||
var productId = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(productId.Id.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Products")]
|
||||
[HttpPut("/api/admin/products/{ProductId}")]
|
||||
[AllowAnonymous]
|
||||
public class UpdateProductEndpoint(IMediator mediator)
|
||||
: Endpoint<UpdateProductCommand, ResponseData>
|
||||
{
|
||||
public override async Task HandleAsync(UpdateProductCommand req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
|
||||
[Tags("Admin/Products")]
|
||||
[HttpDelete("/api/admin/products/{ProductId}")]
|
||||
[AllowAnonymous]
|
||||
public class DeleteProductEndpoint(IMediator mediator)
|
||||
: Endpoint<DeleteProductCommand, ResponseData>
|
||||
{
|
||||
public override async Task HandleAsync(DeleteProductCommand req, CancellationToken ct)
|
||||
{
|
||||
var result = await mediator.Send(req, ct);
|
||||
await Send.OkAsync(result, ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||
using Fengling.Backend.Web.Application.Commands.PointsRules;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 更新积分规则请求
|
||||
/// </summary>
|
||||
public record UpdatePointsRuleRequest(
|
||||
string? RuleName = null,
|
||||
int? PointsValue = null,
|
||||
decimal? BonusMultiplier = null,
|
||||
DateTime? StartDate = null,
|
||||
DateTime? EndDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// 更新积分规则端点
|
||||
/// </summary>
|
||||
[Tags("Admin-PointsRules")]
|
||||
[HttpPut("/api/admin/points-rules/{id}")]
|
||||
[AllowAnonymous]
|
||||
public class UpdatePointsRuleEndpoint(IMediator mediator)
|
||||
: Endpoint<UpdatePointsRuleRequest, ResponseData<bool>>
|
||||
{
|
||||
public override async Task HandleAsync(UpdatePointsRuleRequest req, CancellationToken ct)
|
||||
{
|
||||
var id = Route<Guid>("id");
|
||||
var ruleId = new PointsRuleId(id);
|
||||
|
||||
var command = new UpdatePointsRuleCommand(
|
||||
ruleId,
|
||||
req.RuleName,
|
||||
req.PointsValue,
|
||||
req.BonusMultiplier,
|
||||
req.StartDate,
|
||||
req.EndDate);
|
||||
|
||||
await mediator.Send(command, ct);
|
||||
await Send.OkAsync(true.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Web.Services;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 图片上传请求
|
||||
/// </summary>
|
||||
public class UploadImageRequest
|
||||
{
|
||||
public IFormFile File { get; set; } = null!;
|
||||
public string? Folder { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图片上传端点
|
||||
/// </summary>
|
||||
[Tags("Admin/Upload")]
|
||||
[HttpPost("/api/admin/upload/image")]
|
||||
[AllowAnonymous]
|
||||
[AllowFileUploads]
|
||||
public class UploadImageEndpoint(IFileStorageService fileStorageService)
|
||||
: Endpoint<UploadImageRequest, ResponseData<string>>
|
||||
{
|
||||
public override async Task HandleAsync(UploadImageRequest req, CancellationToken ct)
|
||||
{
|
||||
var folder = string.IsNullOrWhiteSpace(req.Folder) ? "common" : req.Folder;
|
||||
var url = await fileStorageService.UploadImageAsync(req.File, folder, ct);
|
||||
await Send.OkAsync(url.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
using Fengling.Backend.Web.Application.Commands.AdminAuth;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.AdminAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录请求
|
||||
/// </summary>
|
||||
public record AdminLoginRequest(string Username, string Password);
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录响应
|
||||
/// </summary>
|
||||
public record AdminLoginResponseDto(AdminId AdminId, string Username, string Token, DateTime ExpiresAt);
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录端点
|
||||
/// </summary>
|
||||
[Tags("AdminAuth")]
|
||||
[HttpPost("/api/admin/auth/login")]
|
||||
[AllowAnonymous]
|
||||
public class AdminLoginEndpoint(IMediator mediator)
|
||||
: Endpoint<AdminLoginRequest, ResponseData<AdminLoginResponseDto>>
|
||||
{
|
||||
public override async Task HandleAsync(AdminLoginRequest req, CancellationToken ct)
|
||||
{
|
||||
var command = new AdminLoginCommand(req.Username, req.Password);
|
||||
var response = await mediator.Send(command, ct);
|
||||
|
||||
var dto = new AdminLoginResponseDto(
|
||||
response.AdminId,
|
||||
response.Username,
|
||||
response.Token,
|
||||
response.ExpiresAt);
|
||||
|
||||
await Send.OkAsync(dto.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||
using Fengling.Backend.Web.Application.Queries.AdminAuth;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.AdminAuth;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前管理员端点
|
||||
/// </summary>
|
||||
[Tags("AdminAuth")]
|
||||
// [HttpGet("/api/admin/auth/me")]
|
||||
public class GetCurrentAdminEndpoint(IMediator mediator)
|
||||
: EndpointWithoutRequest<ResponseData<AdminDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/admin/auth/me");
|
||||
Tags("AdminAuth");
|
||||
Description(x => x.WithTags("AdminAuth"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
// 从 JWT Claims 中提取 AdminId
|
||||
var adminIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(adminIdClaim) || !Guid.TryParse(adminIdClaim, out var adminGuid))
|
||||
{
|
||||
await Send.UnauthorizedAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var adminId = new AdminId(adminGuid);
|
||||
var query = new GetCurrentAdminQuery(adminId);
|
||||
var admin = await mediator.Send(query, ct);
|
||||
|
||||
await Send.OkAsync(admin.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兑换礼品请求
|
||||
/// </summary>
|
||||
public record RedeemGiftRequest(
|
||||
string GiftId,
|
||||
int Quantity,
|
||||
RedeemGiftAddressDto? ShippingAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 收货地址
|
||||
/// </summary>
|
||||
public record RedeemGiftAddressDto(
|
||||
string ReceiverName,
|
||||
string ReceiverPhone,
|
||||
string Province,
|
||||
string City,
|
||||
string District,
|
||||
string DetailAddress);
|
||||
|
||||
/// <summary>
|
||||
/// 兑换礼品端点(会员端)
|
||||
/// </summary>
|
||||
[Tags("Gifts")]
|
||||
[HttpPost("/api/gifts/redeem")]
|
||||
public class RedeemGiftEndpoint(IMediator mediator)
|
||||
: Endpoint<CreateRedemptionOrderCommand, ResponseData<Guid>>
|
||||
: Endpoint<RedeemGiftRequest, ResponseData<string>>
|
||||
{
|
||||
public override async Task HandleAsync(CreateRedemptionOrderCommand req, CancellationToken ct)
|
||||
public override async Task HandleAsync(RedeemGiftRequest req, CancellationToken ct)
|
||||
{
|
||||
var orderId = await mediator.Send(req, ct);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
/// <summary>
|
||||
/// 扫码请求
|
||||
/// </summary>
|
||||
public record UseMarketingCodeRequest(string Code, MemberId MemberId);
|
||||
public record UseMarketingCodeRequest(string Code);
|
||||
|
||||
/// <summary>
|
||||
/// 扫码响应
|
||||
@ -24,13 +25,22 @@ public record UseMarketingCodeEndpointResponse(
|
||||
/// </summary>
|
||||
[Tags("MarketingCodes")]
|
||||
[HttpPost("/api/marketing-codes/scan")]
|
||||
[AllowAnonymous]
|
||||
public class UseMarketingCodeEndpoint(IMediator mediator)
|
||||
: Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>>
|
||||
{
|
||||
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(
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||
using Fengling.Backend.Infrastructure.Repositories;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Members;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前会员信息响应
|
||||
/// </summary>
|
||||
public record GetCurrentMemberResponse(
|
||||
string Id,
|
||||
string Phone,
|
||||
string? Nickname,
|
||||
int TotalPoints,
|
||||
int AvailablePoints,
|
||||
string Level,
|
||||
string Status,
|
||||
DateTime CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前会员信息端点
|
||||
/// </summary>
|
||||
[Tags("Members")]
|
||||
[HttpGet("/api/members/current")]
|
||||
public class GetCurrentMemberEndpoint(IMemberRepository memberRepository)
|
||||
: EndpointWithoutRequest<ResponseData<GetCurrentMemberResponse>>
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
// 从 JWT Claims 中提取 MemberId
|
||||
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
|
||||
{
|
||||
await Send.UnauthorizedAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var memberId = new MemberId(memberGuid);
|
||||
var member = await memberRepository.GetAsync(memberId, ct);
|
||||
|
||||
if (member == null)
|
||||
{
|
||||
await Send.NotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = new GetCurrentMemberResponse(
|
||||
member.Id.ToString(),
|
||||
member.Phone,
|
||||
member.Nickname,
|
||||
member.TotalPoints,
|
||||
member.AvailablePoints,
|
||||
member.Level.LevelName,
|
||||
member.Status.ToString(),
|
||||
member.RegisteredAt
|
||||
);
|
||||
|
||||
await Send.OkAsync(response.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.Members;
|
||||
|
||||
/// <summary>
|
||||
/// 退出登录端点
|
||||
/// </summary>
|
||||
[Tags("Members")]
|
||||
[HttpPost("/api/members/logout")]
|
||||
public class LogoutMemberEndpoint : EndpointWithoutRequest
|
||||
{
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
// JWT Token 由客户端负责清除,服务端无需特殊处理
|
||||
// 这里可以添加黑名单Token逻辑(如果需要)
|
||||
await Send.NoContentAsync(ct);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using System.Security.Claims;
|
||||
using FastEndpoints;
|
||||
using Fengling.Backend.Web.Application.Queries.PointsTransactions;
|
||||
|
||||
namespace Fengling.Backend.Web.Endpoints.PointsTransactions;
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的积分流水记录
|
||||
/// </summary>
|
||||
[Tags("PointsTransactions")]
|
||||
[HttpGet("/api/points-transactions/my")]
|
||||
public class GetMyPointsTransactionsEndpoint(IMediator mediator)
|
||||
: Endpoint<GetMyPointsTransactionsRequest, ResponseData<List<PointsTransactionDto>>>
|
||||
{
|
||||
public override async Task HandleAsync(GetMyPointsTransactionsRequest req, CancellationToken ct)
|
||||
{
|
||||
// 从 JWT Claims 中提取 MemberId
|
||||
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
|
||||
{
|
||||
await Send.UnauthorizedAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
var query = new GetPointsTransactionsQuery(MemberId: memberGuid, Type: req.Type);
|
||||
var transactions = await mediator.Send(query, ct);
|
||||
await Send.OkAsync(transactions.AsResponseData(), ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record GetMyPointsTransactionsRequest(int? Type = null);
|
||||
@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// 获取订单详情端点(会员端)
|
||||
|
||||
@ -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<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
|
||||
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
|
||||
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>()
|
||||
?? new AppConfiguration
|
||||
{
|
||||
JwtIssuer = "FenglingBackend",
|
||||
JwtAudience = "FenglingBackend",
|
||||
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!"
|
||||
};
|
||||
|
||||
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
||||
{
|
||||
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<Fengling.Backend.Web.Services.IFileStorageService, Fengling.Backend.Web.Services.LocalFileStorageService>();
|
||||
|
||||
#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<ApplicationDbContext>();
|
||||
await dbContext.Database.MigrateAsync();
|
||||
|
||||
// 初始化默认管理员账号
|
||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
await mediator.Send(new Fengling.Backend.Web.Application.Commands.AdminAuth.InitializeDefaultAdminCommand());
|
||||
}
|
||||
|
||||
|
||||
@ -198,6 +225,15 @@ try
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseCors(x =>
|
||||
{
|
||||
x
|
||||
.SetIsOriginAllowed(_=>true)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyMethod()
|
||||
.Build();
|
||||
});
|
||||
//app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication(); // Authentication 必须在 Authorization 之前
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
namespace Fengling.Backend.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储服务接口
|
||||
/// </summary>
|
||||
public interface IFileStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传图片
|
||||
/// </summary>
|
||||
/// <param name="file">图片文件</param>
|
||||
/// <param name="folder">文件夹名称</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>图片URL</returns>
|
||||
Task<string> UploadImageAsync(IFormFile file, string folder, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
namespace Fengling.Backend.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 本地文件存储服务实现
|
||||
/// </summary>
|
||||
public class LocalFileStorageService(IWebHostEnvironment environment) : IFileStorageService
|
||||
{
|
||||
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
|
||||
private const long MaxFileSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
public async Task<string> UploadImageAsync(IFormFile file, string folder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 验证文件
|
||||
if (file == null || file.Length == 0)
|
||||
throw new KnownException("文件不能为空");
|
||||
|
||||
if (file.Length > MaxFileSize)
|
||||
throw new KnownException($"文件大小不能超过{MaxFileSize / 1024 / 1024}MB");
|
||||
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (!AllowedExtensions.Contains(extension))
|
||||
throw new KnownException($"不支持的文件格式,仅支持: {string.Join(", ", AllowedExtensions)}");
|
||||
|
||||
// 生成文件路径
|
||||
var yearMonth = DateTime.Now.ToString("yyyy-MM");
|
||||
var fileName = $"{Guid.NewGuid()}{extension}";
|
||||
var relativePath = Path.Combine("uploads", folder, yearMonth, fileName);
|
||||
var absolutePath = Path.Combine(environment.WebRootPath, relativePath);
|
||||
|
||||
// 确保目录存在
|
||||
var directory = Path.GetDirectoryName(absolutePath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
using (var stream = new FileStream(absolutePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream, cancellationToken);
|
||||
}
|
||||
|
||||
// 返回相对URL
|
||||
return $"/{relativePath.Replace("\\", "/")}";
|
||||
}
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
Backend/src/Fengling.Backend.Web/fengling.db
Normal file
BIN
Backend/src/Fengling.Backend.Web/fengling.db-shm
Normal file
BIN
Backend/src/Fengling.Backend.Web/fengling.db-wal
Normal file
2
Frontend/Fengling.Backend.Admin/.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:5511
|
||||
VITE_APP_TITLE=Fengling 管理后台
|
||||
2
Frontend/Fengling.Backend.Admin/.env.production
Normal file
@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=
|
||||
VITE_APP_TITLE=Fengling 管理后台
|
||||
24
Frontend/Fengling.Backend.Admin/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
Frontend/Fengling.Backend.Admin/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
Frontend/Fengling.Backend.Admin/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
21
Frontend/Fengling.Backend.Admin/components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/style.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"composables": "@/composables"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
13
Frontend/Fengling.Backend.Admin/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fengling 管理后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
36
Frontend/Fengling.Backend.Admin/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "fengling-backend-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.563.0",
|
||||
"pinia": "^3.0.4",
|
||||
"reka-ui": "^2.8.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "^2.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vue-tsc": "^3.1.5"
|
||||
}
|
||||
}
|
||||
1869
Frontend/Fengling.Backend.Admin/pnpm-lock.yaml
Normal file
1
Frontend/Fengling.Backend.Admin/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
8
Frontend/Fengling.Backend.Admin/src/App.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<Toaster position="top-right" :duration="3000" />
|
||||
</template>
|
||||
47
Frontend/Fengling.Backend.Admin/src/api/admin-auth.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import apiClient from './client'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 管理员登录请求
|
||||
*/
|
||||
export interface AdminLoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录响应
|
||||
*/
|
||||
export interface AdminLoginResponse {
|
||||
adminId: string
|
||||
username: string
|
||||
token: string
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员信息DTO
|
||||
*/
|
||||
export interface AdminDto {
|
||||
adminId: string
|
||||
username: string
|
||||
status: string
|
||||
lastLoginAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
export const adminLogin = async (data: AdminLoginRequest): Promise<AdminLoginResponse> => {
|
||||
const res = await apiClient.post<ResponseData<AdminLoginResponse>>('/api/admin/auth/login', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前管理员信息
|
||||
*/
|
||||
export const getCurrentAdmin = async (): Promise<AdminDto> => {
|
||||
const res = await apiClient.get<ResponseData<AdminDto>>('/api/admin/auth/me')
|
||||
return res.data.data
|
||||
}
|
||||
26
Frontend/Fengling.Backend.Admin/src/api/categories.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import apiClient from './client'
|
||||
import type { CategoryDto, CreateCategoryRequest, UpdateCategoryRequest } from '@/types/category'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function getCategories(): Promise<CategoryDto[]> {
|
||||
const res = await apiClient.get<ResponseData<CategoryDto[]>>('/api/admin/categories')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getCategoryById(id: string): Promise<CategoryDto> {
|
||||
const res = await apiClient.get<ResponseData<CategoryDto>>(`/api/admin/categories/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function createCategory(data: CreateCategoryRequest): Promise<string> {
|
||||
const res = await apiClient.post<ResponseData<string>>('/api/admin/categories', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function updateCategory(id: string, data: UpdateCategoryRequest): Promise<void> {
|
||||
await apiClient.put(`/api/admin/categories/${id}`, { ...data, categoryId: id })
|
||||
}
|
||||
|
||||
export async function deleteCategory(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/admin/categories/${id}`)
|
||||
}
|
||||
42
Frontend/Fengling.Backend.Admin/src/api/client.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import axios from 'axios'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data as ResponseData
|
||||
if (data.success === false) {
|
||||
toast.error(data.message || '操作失败')
|
||||
return Promise.reject(new Error(data.message || '操作失败'))
|
||||
}
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('admin_token')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(error)
|
||||
}
|
||||
const message = error.response?.data?.message || error.message || '网络请求失败'
|
||||
toast.error(message)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default apiClient
|
||||
34
Frontend/Fengling.Backend.Admin/src/api/gifts.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import apiClient from './client'
|
||||
import type { GiftDto, CreateGiftRequest, UpdateGiftRequest } from '@/types/gift'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function getGifts(): Promise<GiftDto[]> {
|
||||
const res = await apiClient.get<ResponseData<GiftDto[]>>('/api/admin/gifts')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getGiftById(id: string): Promise<GiftDto> {
|
||||
const res = await apiClient.get<ResponseData<GiftDto>>(`/api/admin/gifts/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function createGift(data: CreateGiftRequest): Promise<string> {
|
||||
const res = await apiClient.post<ResponseData<string>>('/api/admin/gifts', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function updateGift(id: string, data: UpdateGiftRequest): Promise<void> {
|
||||
await apiClient.put(`/api/admin/gifts/${id}`, { ...data, giftId: id })
|
||||
}
|
||||
|
||||
export async function putOnShelf(id: string): Promise<void> {
|
||||
await apiClient.post(`/api/admin/gifts/${id}/putonshelf`, {})
|
||||
}
|
||||
|
||||
export async function putOffShelf(id: string): Promise<void> {
|
||||
await apiClient.post(`/api/admin/gifts/${id}/putoffshelf`, {})
|
||||
}
|
||||
|
||||
export async function addGiftStock(id: string, quantity: number): Promise<void> {
|
||||
await apiClient.post(`/api/admin/gifts/${id}/addstock`, { giftId: id, quantity })
|
||||
}
|
||||
19
Frontend/Fengling.Backend.Admin/src/api/marketing-codes.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import apiClient from './client'
|
||||
import type { GenerateMarketingCodesRequest, GenerateMarketingCodesResponse, MarketingCodeDto, MarketingCodeBatchDto, GetMarketingCodesParams } from '@/types/marketing-code'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function generateMarketingCodes(data: GenerateMarketingCodesRequest): Promise<GenerateMarketingCodesResponse> {
|
||||
const res = await apiClient.post<ResponseData<GenerateMarketingCodesResponse>>('/api/admin/marketing-codes/generate', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getMarketingCodes(params: GetMarketingCodesParams): Promise<MarketingCodeDto[]> {
|
||||
const res = await apiClient.get<ResponseData<MarketingCodeDto[]>>('/api/admin/marketing-codes', { params })
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getMarketingCodeBatches(): Promise<MarketingCodeBatchDto[]> {
|
||||
const res = await apiClient.get<ResponseData<MarketingCodeBatchDto[]>>('/api/admin/marketing-codes/batches')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
8
Frontend/Fengling.Backend.Admin/src/api/members.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import apiClient from './client'
|
||||
import type { MemberDto } from '@/types/member'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function getMemberById(memberId: string): Promise<MemberDto> {
|
||||
const res = await apiClient.get<ResponseData<MemberDto>>(`/api/members/${memberId}`)
|
||||
return res.data.data
|
||||
}
|
||||
25
Frontend/Fengling.Backend.Admin/src/api/orders.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import apiClient from './client'
|
||||
import type { RedemptionOrderDto, DispatchOrderRequest, CancelOrderRequest } from '@/types/order'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function getOrders(): Promise<RedemptionOrderDto[]> {
|
||||
const res = await apiClient.get<ResponseData<RedemptionOrderDto[]>>('/api/admin/redemption-orders')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getOrderById(id: string): Promise<RedemptionOrderDto> {
|
||||
const res = await apiClient.get<ResponseData<RedemptionOrderDto>>(`/api/admin/redemption-orders/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function dispatchOrder(data: DispatchOrderRequest): Promise<void> {
|
||||
await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/dispatch`, data)
|
||||
}
|
||||
|
||||
export async function completeOrder(orderId: string): Promise<void> {
|
||||
await apiClient.post(`/api/admin/redemption-orders/${orderId}/complete`, { orderId })
|
||||
}
|
||||
|
||||
export async function cancelOrder(data: CancelOrderRequest): Promise<void> {
|
||||
await apiClient.post(`/api/admin/redemption-orders/${data.orderId}/cancel`, data)
|
||||
}
|
||||
41
Frontend/Fengling.Backend.Admin/src/api/points-rules.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import apiClient from './client'
|
||||
import type {
|
||||
CreatePointsRuleRequest,
|
||||
CreatePointsRuleResponse,
|
||||
PointsRuleDto,
|
||||
UpdatePointsRuleRequest
|
||||
} from '@/types/points-rule'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
// 创建积分规则
|
||||
export async function createPointsRule(data: CreatePointsRuleRequest): Promise<CreatePointsRuleResponse> {
|
||||
const res = await apiClient.post<ResponseData<CreatePointsRuleResponse>>('/api/admin/points-rules', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// 获取积分规则列表
|
||||
export async function getPointsRules(): Promise<PointsRuleDto[]> {
|
||||
const res = await apiClient.get<ResponseData<PointsRuleDto[]>>('/api/admin/points-rules')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// 根据ID获取积分规则
|
||||
export async function getPointsRuleById(id: string): Promise<PointsRuleDto> {
|
||||
const res = await apiClient.get<ResponseData<PointsRuleDto>>(`/api/admin/points-rules/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
// 更新积分规则
|
||||
export async function updatePointsRule(id: string, data: UpdatePointsRuleRequest): Promise<void> {
|
||||
await apiClient.put<ResponseData<object>>(`/api/admin/points-rules/${id}`, data)
|
||||
}
|
||||
|
||||
// 激活积分规则
|
||||
export async function activatePointsRule(id: string): Promise<void> {
|
||||
await apiClient.post<ResponseData<object>>(`/api/admin/points-rules/${id}/activate`)
|
||||
}
|
||||
|
||||
// 停用积分规则
|
||||
export async function deactivatePointsRule(id: string): Promise<void> {
|
||||
await apiClient.post<ResponseData<object>>(`/api/admin/points-rules/${id}/deactivate`)
|
||||
}
|
||||
26
Frontend/Fengling.Backend.Admin/src/api/products.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import apiClient from './client'
|
||||
import type { ProductDto, CreateProductRequest, UpdateProductRequest } from '@/types/product'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function getProducts(): Promise<ProductDto[]> {
|
||||
const res = await apiClient.get<ResponseData<ProductDto[]>>('/api/admin/products')
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function getProductById(id: string): Promise<ProductDto> {
|
||||
const res = await apiClient.get<ResponseData<ProductDto>>(`/api/admin/products/${id}`)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function createProduct(data: CreateProductRequest): Promise<string> {
|
||||
const res = await apiClient.post<ResponseData<string>>('/api/admin/products', data)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function updateProduct(id: string, data: UpdateProductRequest): Promise<void> {
|
||||
await apiClient.put(`/api/admin/products/${id}`, { ...data, productId: id })
|
||||
}
|
||||
|
||||
export async function deleteProduct(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/admin/products/${id}`)
|
||||
}
|
||||
17
Frontend/Fengling.Backend.Admin/src/api/upload.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import apiClient from './client'
|
||||
import type { ResponseData } from '@/types/api'
|
||||
|
||||
export async function uploadImage(file: File, folder?: string): Promise<string> {
|
||||
const formData = new FormData()
|
||||
formData.append('File', file)
|
||||
if (folder) {
|
||||
formData.append('Folder', folder)
|
||||
}
|
||||
|
||||
const res = await apiClient.post<ResponseData<string>>('/api/admin/upload/image', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
After Width: | Height: | Size: 923 KiB |
|
After Width: | Height: | Size: 846 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 884 KiB |
BIN
Frontend/Fengling.Backend.Admin/src/assets/design/login-page.png
Normal file
|
After Width: | Height: | Size: 777 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 790 KiB |
|
After Width: | Height: | Size: 743 KiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 238 KiB |
1
Frontend/Fengling.Backend.Admin/src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |