feat: 实现完整的前后端功能
- 后端新增管理员、商品、分类聚合模型 - 实现积分规则、礼品、订单、会员等完整功能 - 添加管理员认证和权限管理 - 完善数据库迁移和实体配置 - 前端管理后台实现登录、仪表盘、积分规则、礼品、订单、会员等页面 - 集成shadcn-vue UI组件库 - 添加前后端功能文档和截图
@ -2,7 +2,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Third-party package versions -->
|
<!-- Third-party package versions -->
|
||||||
<NetCorePalVersion>3.2.1</NetCorePalVersion>
|
<NetCorePalVersion>3.2.1</NetCorePalVersion>
|
||||||
@ -13,14 +12,12 @@
|
|||||||
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
|
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
|
||||||
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
|
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
|
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
|
||||||
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
|
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
|
||||||
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
|
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
|
||||||
|
|
||||||
<!-- Database providers - framework specific versions -->
|
<!-- Database providers - framework specific versions -->
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
|
||||||
@ -39,7 +36,6 @@
|
|||||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
|
||||||
|
|
||||||
<!-- CAP packages for .NET 9.0+ -->
|
<!-- CAP packages for .NET 9.0+ -->
|
||||||
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
|
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
|
||||||
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
|
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
|
||||||
@ -50,14 +46,12 @@
|
|||||||
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
|
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
|
||||||
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
|
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
|
||||||
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
|
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
|
||||||
|
|
||||||
<!-- FastEndpoints -->
|
<!-- FastEndpoints -->
|
||||||
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
|
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
|
||||||
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
|
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
|
||||||
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
|
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Other packages -->
|
<!-- Other packages -->
|
||||||
|
<PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||||
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
|
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.17" />
|
||||||
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
|
<PackageVersion Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
|
||||||
@ -70,7 +64,6 @@
|
|||||||
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
|
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
|
||||||
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
|
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
|
|
||||||
<!-- Aspire packages -->
|
<!-- Aspire packages -->
|
||||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
|
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
|
||||||
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
|
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
|
||||||
@ -98,7 +91,6 @@
|
|||||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
|
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
|
||||||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
|
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
|
||||||
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
|
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
|
||||||
|
|
||||||
<!-- NetCorePal packages -->
|
<!-- NetCorePal packages -->
|
||||||
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
|
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
|
||||||
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
|
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
|
||||||
@ -126,7 +118,6 @@
|
|||||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
|
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
|
||||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
|
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
|
||||||
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
|
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
|
||||||
|
|
||||||
<!-- Testing packages -->
|
<!-- Testing packages -->
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
|
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
|
||||||
@ -145,7 +136,6 @@
|
|||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||||
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
|
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
|
||||||
|
|
||||||
<!-- Code analysis -->
|
<!-- Code analysis -->
|
||||||
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
|
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
13
Backend/dotnet-tools.json
Normal file
@ -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>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" />
|
||||||
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
|
<PackageReference Include="NetCorePal.Extensions.CodeAnalysis" />
|
||||||
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
|
<PackageReference Include="NetCorePal.Extensions.Domain.Abstractions" />
|
||||||
<PackageReference Include="NetCorePal.Extensions.Primitives" />
|
<PackageReference Include="NetCorePal.Extensions.Primitives" />
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
|
using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.AdminAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.PointsRuleAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.PointsTransactionAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.CategoryAggregate;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.ProductAggregate;
|
||||||
|
|
||||||
namespace Fengling.Backend.Infrastructure;
|
namespace Fengling.Backend.Infrastructure;
|
||||||
|
|
||||||
@ -14,6 +17,9 @@ public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
|
|||||||
: AppDbContextBase(options, mediator)
|
: AppDbContextBase(options, mediator)
|
||||||
, ISqliteCapDataStorage
|
, ISqliteCapDataStorage
|
||||||
{
|
{
|
||||||
|
// 管理员聚合
|
||||||
|
public DbSet<Admin> Admins => Set<Admin>();
|
||||||
|
|
||||||
// 会员聚合
|
// 会员聚合
|
||||||
public DbSet<Member> Members => Set<Member>();
|
public DbSet<Member> Members => Set<Member>();
|
||||||
public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>();
|
public DbSet<PointsTransaction> PointsTransactions => Set<PointsTransaction>();
|
||||||
@ -30,6 +36,12 @@ public partial class ApplicationDbContext(DbContextOptions<ApplicationDbContext>
|
|||||||
// 兑换订单聚合
|
// 兑换订单聚合
|
||||||
public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>();
|
public DbSet<RedemptionOrder> RedemptionOrders => Set<RedemptionOrder>();
|
||||||
|
|
||||||
|
// 品类聚合
|
||||||
|
public DbSet<Category> Categories => Set<Category>();
|
||||||
|
|
||||||
|
// 产品聚合
|
||||||
|
public DbSet<Product> Products => Set<Product>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
if (modelBuilder is null)
|
if (modelBuilder is null)
|
||||||
|
|||||||
@ -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.MemberId).HasDatabaseName("IX_PointsTransactions_MemberId");
|
||||||
builder.HasIndex(x => x.RelatedId).IsUnique().HasDatabaseName("IX_PointsTransactions_RelatedId");
|
builder.HasIndex(x => x.RelatedId).HasDatabaseName("IX_PointsTransactions_RelatedId");
|
||||||
builder.HasIndex(x => x.Type).HasDatabaseName("IX_PointsTransactions_Type");
|
builder.HasIndex(x => x.Type).HasDatabaseName("IX_PointsTransactions_Type");
|
||||||
builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt");
|
builder.HasIndex(x => x.CreatedAt).HasDatabaseName("IX_PointsTransactions_CreatedAt");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
namespace Fengling.Backend.Infrastructure.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
[Migration("20260211044819_Init")]
|
[Migration("20260211061437_Init")]
|
||||||
partial class Init
|
partial class Init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -355,7 +355,6 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
||||||
|
|
||||||
b.HasIndex("RelatedId")
|
b.HasIndex("RelatedId")
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
||||||
|
|
||||||
b.HasIndex("Type")
|
b.HasIndex("Type")
|
||||||
@ -449,6 +448,112 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
b.ToTable("RedemptionOrders", (string)null);
|
b.ToTable("RedemptionOrders", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Instance")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLockTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("CAPLock", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.ToTable("CAPPublishedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(400)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.ToTable("CAPReceivedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||||
{
|
{
|
||||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||||
@ -11,6 +11,58 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPLock",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
Instance = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||||
|
LastLockTime = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPLock", x => x.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPPublishedMessage",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Retries = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
StatusName = table.Column<string>(type: "TEXT", maxLength: 40, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPPublishedMessage", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CAPReceivedMessage",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Version = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||||
|
Group = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Content = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Retries = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
Added = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
StatusName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CAPReceivedMessage", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Gifts",
|
name: "Gifts",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
@ -160,6 +212,26 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
table.PrimaryKey("PK_RedemptionOrders", x => x.Id);
|
table.PrimaryKey("PK_RedemptionOrders", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpiresAt_StatusName",
|
||||||
|
table: "CAPPublishedMessage",
|
||||||
|
columns: new[] { "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Version_ExpiresAt_StatusName",
|
||||||
|
table: "CAPPublishedMessage",
|
||||||
|
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExpiresAt_StatusName1",
|
||||||
|
table: "CAPReceivedMessage",
|
||||||
|
columns: new[] { "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Version_ExpiresAt_StatusName1",
|
||||||
|
table: "CAPReceivedMessage",
|
||||||
|
columns: new[] { "Version", "ExpiresAt", "StatusName" });
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Gifts_IsOnShelf",
|
name: "IX_Gifts_IsOnShelf",
|
||||||
table: "Gifts",
|
table: "Gifts",
|
||||||
@ -235,8 +307,7 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_PointsTransactions_RelatedId",
|
name: "IX_PointsTransactions_RelatedId",
|
||||||
table: "PointsTransactions",
|
table: "PointsTransactions",
|
||||||
column: "RelatedId",
|
column: "RelatedId");
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_PointsTransactions_Type",
|
name: "IX_PointsTransactions_Type",
|
||||||
@ -268,6 +339,15 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPLock");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPPublishedMessage");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CAPReceivedMessage");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Gifts");
|
name: "Gifts");
|
||||||
|
|
||||||
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
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.AdminAggregate.Admin", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("管理员ID");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<bool>("Deleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLoginAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("最后登录时间");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("密码哈希");
|
||||||
|
|
||||||
|
b.Property<int>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasComment("管理员状态(1=Active,2=Disabled)");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("用户名");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status")
|
||||||
|
.HasDatabaseName("IX_Admins_Status");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Admins_Username");
|
||||||
|
|
||||||
|
b.ToTable("Admins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.CategoryAggregate.Category", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("品类ID");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("品类编码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<bool>("Deleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("描述");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasComment("是否激活");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("品类名称");
|
||||||
|
|
||||||
|
b.Property<int>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasComment("排序");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Code")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Categories_Code");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive")
|
||||||
|
.HasDatabaseName("IX_Categories_IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder")
|
||||||
|
.HasDatabaseName("IX_Categories_SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("Categories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.GiftAggregate.Gift", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -352,7 +463,6 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
.HasDatabaseName("IX_PointsTransactions_MemberId");
|
||||||
|
|
||||||
b.HasIndex("RelatedId")
|
b.HasIndex("RelatedId")
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
.HasDatabaseName("IX_PointsTransactions_RelatedId");
|
||||||
|
|
||||||
b.HasIndex("Type")
|
b.HasIndex("Type")
|
||||||
@ -361,6 +471,64 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
b.ToTable("PointsTransactions", (string)null);
|
b.ToTable("PointsTransactions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.ProductAggregate.Product", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("产品ID");
|
||||||
|
|
||||||
|
b.Property<Guid>("CategoryId")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("品类ID");
|
||||||
|
|
||||||
|
b.Property<string>("CategoryName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("品类名称");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<bool>("Deleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("描述");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasComment("是否激活");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("产品名称");
|
||||||
|
|
||||||
|
b.Property<int>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId")
|
||||||
|
.HasDatabaseName("IX_Products_CategoryId");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive")
|
||||||
|
.HasDatabaseName("IX_Products_IsActive");
|
||||||
|
|
||||||
|
b.ToTable("Products", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.RedemptionOrderAggregate.RedemptionOrder", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -446,6 +614,112 @@ namespace Fengling.Backend.Infrastructure.Migrations
|
|||||||
b.ToTable("RedemptionOrders", (string)null);
|
b.ToTable("RedemptionOrders", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Instance")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastLockTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("CAPLock", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
|
||||||
|
|
||||||
|
b.ToTable("CAPPublishedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Added")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Group")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(400)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Retries")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StatusName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
|
||||||
|
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
|
||||||
|
|
||||||
|
b.ToTable("CAPReceivedMessage", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
modelBuilder.Entity("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.MarketingCode", b =>
|
||||||
{
|
{
|
||||||
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
b.OwnsOne("Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate.ProductInfo", "ProductInfo", b1 =>
|
||||||
|
|||||||
@ -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,
|
string? memberLevelCode,
|
||||||
DateTime startDate,
|
DateTime startDate,
|
||||||
DateTime? endDate,
|
DateTime? endDate,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default,
|
||||||
|
PointsRuleId? excludeRuleId = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,10 +58,17 @@ public class PointsRuleRepository(ApplicationDbContext context)
|
|||||||
string? memberLevelCode,
|
string? memberLevelCode,
|
||||||
DateTime startDate,
|
DateTime startDate,
|
||||||
DateTime? endDate,
|
DateTime? endDate,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default,
|
||||||
|
PointsRuleId? excludeRuleId = null)
|
||||||
{
|
{
|
||||||
var query = DbContext.PointsRules.AsQueryable();
|
var query = DbContext.PointsRules.AsQueryable();
|
||||||
|
|
||||||
|
// 排除特定规则
|
||||||
|
if (excludeRuleId != null)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Id != excludeRuleId);
|
||||||
|
}
|
||||||
|
|
||||||
// 检查维度是否完全一致
|
// 检查维度是否完全一致
|
||||||
if (productId.HasValue)
|
if (productId.HasValue)
|
||||||
query = query.Where(x => x.ProductId == productId);
|
query = query.Where(x => x.ProductId == productId);
|
||||||
@ -78,8 +86,9 @@ public class PointsRuleRepository(ApplicationDbContext context)
|
|||||||
query = query.Where(x => x.MemberLevelCode == null);
|
query = query.Where(x => x.MemberLevelCode == null);
|
||||||
|
|
||||||
// 检查时间重叠
|
// 检查时间重叠
|
||||||
|
var effectiveEndDate = endDate ?? DateTime.MaxValue;
|
||||||
query = query.Where(x =>
|
query = query.Where(x =>
|
||||||
x.StartDate <= (endDate ?? DateTime.MaxValue) &&
|
x.StartDate <= effectiveEndDate &&
|
||||||
(x.EndDate == null || x.EndDate >= startDate));
|
(x.EndDate == null || x.EndDate >= startDate));
|
||||||
|
|
||||||
return await query.AnyAsync(cancellationToken);
|
return await query.AnyAsync(cancellationToken);
|
||||||
|
|||||||
@ -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>
|
/// </summary>
|
||||||
public record CreateGiftCommand(
|
public record CreateGiftCommand(
|
||||||
string Name,
|
string Name,
|
||||||
int Type,
|
GiftType Type,
|
||||||
string Description,
|
string Description,
|
||||||
string ImageUrl,
|
string ImageUrl,
|
||||||
int RequiredPoints,
|
int RequiredPoints,
|
||||||
@ -20,7 +20,7 @@ public class CreateGiftCommandValidator : AbstractValidator<CreateGiftCommand>
|
|||||||
public CreateGiftCommandValidator()
|
public CreateGiftCommandValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
|
||||||
RuleFor(x => x.Type).IsInEnum();
|
RuleFor(x => x.Type).IsInEnum().WithMessage("礼品类型无效");
|
||||||
RuleFor(x => x.Description).NotEmpty().MaximumLength(500);
|
RuleFor(x => x.Description).NotEmpty().MaximumLength(500);
|
||||||
RuleFor(x => x.ImageUrl).NotEmpty().MaximumLength(500);
|
RuleFor(x => x.ImageUrl).NotEmpty().MaximumLength(500);
|
||||||
RuleFor(x => x.RequiredPoints).GreaterThan(0);
|
RuleFor(x => x.RequiredPoints).GreaterThan(0);
|
||||||
@ -33,11 +33,9 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository)
|
|||||||
{
|
{
|
||||||
public async Task<GiftId> Handle(CreateGiftCommand request, CancellationToken cancellationToken)
|
public async Task<GiftId> Handle(CreateGiftCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var giftType = (GiftType)request.Type;
|
|
||||||
|
|
||||||
var gift = new Gift(
|
var gift = new Gift(
|
||||||
request.Name,
|
request.Name,
|
||||||
giftType,
|
request.Type,
|
||||||
request.Description,
|
request.Description,
|
||||||
request.ImageUrl,
|
request.ImageUrl,
|
||||||
request.RequiredPoints,
|
request.RequiredPoints,
|
||||||
@ -54,7 +52,7 @@ public class CreateGiftCommandHandler(IGiftRepository giftRepository)
|
|||||||
/// 更新礼品命令
|
/// 更新礼品命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record UpdateGiftCommand(
|
public record UpdateGiftCommand(
|
||||||
Guid GiftId,
|
GiftId GiftId,
|
||||||
string? Name = null,
|
string? Name = null,
|
||||||
string? Description = null,
|
string? Description = null,
|
||||||
string? ImageUrl = null,
|
string? ImageUrl = null,
|
||||||
@ -78,8 +76,7 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository)
|
|||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(UpdateGiftCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(UpdateGiftCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var giftId = new GiftId(request.GiftId);
|
var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
|
||||||
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
|
|
||||||
|
|
||||||
if (gift == null)
|
if (gift == null)
|
||||||
throw new KnownException("礼品不存在");
|
throw new KnownException("礼品不存在");
|
||||||
@ -100,15 +97,14 @@ public class UpdateGiftCommandHandler(IGiftRepository giftRepository)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 上架礼品命令
|
/// 上架礼品命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record PutOnShelfCommand(Guid GiftId) : ICommand<ResponseData>;
|
public record PutOnShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
|
public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
|
||||||
: ICommandHandler<PutOnShelfCommand, ResponseData>
|
: ICommandHandler<PutOnShelfCommand, ResponseData>
|
||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(PutOnShelfCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(PutOnShelfCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var giftId = new GiftId(request.GiftId);
|
var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
|
||||||
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
|
|
||||||
|
|
||||||
if (gift == null)
|
if (gift == null)
|
||||||
throw new KnownException("礼品不存在");
|
throw new KnownException("礼品不存在");
|
||||||
@ -123,15 +119,14 @@ public class PutOnShelfCommandHandler(IGiftRepository giftRepository)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 下架礼品命令
|
/// 下架礼品命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record PutOffShelfCommand(Guid GiftId) : ICommand<ResponseData>;
|
public record PutOffShelfCommand(GiftId GiftId) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
|
public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
|
||||||
: ICommandHandler<PutOffShelfCommand, ResponseData>
|
: ICommandHandler<PutOffShelfCommand, ResponseData>
|
||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(PutOffShelfCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(PutOffShelfCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var giftId = new GiftId(request.GiftId);
|
var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
|
||||||
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
|
|
||||||
|
|
||||||
if (gift == null)
|
if (gift == null)
|
||||||
throw new KnownException("礼品不存在");
|
throw new KnownException("礼品不存在");
|
||||||
@ -146,7 +141,7 @@ public class PutOffShelfCommandHandler(IGiftRepository giftRepository)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 增加库存命令
|
/// 增加库存命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record AddGiftStockCommand(Guid GiftId, int Quantity) : ICommand<ResponseData>;
|
public record AddGiftStockCommand(GiftId GiftId, int Quantity) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand>
|
public class AddGiftStockCommandValidator : AbstractValidator<AddGiftStockCommand>
|
||||||
{
|
{
|
||||||
@ -162,8 +157,7 @@ public class AddGiftStockCommandHandler(IGiftRepository giftRepository)
|
|||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(AddGiftStockCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(AddGiftStockCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var giftId = new GiftId(request.GiftId);
|
var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
|
||||||
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
|
|
||||||
|
|
||||||
if (gift == null)
|
if (gift == null)
|
||||||
throw new KnownException("礼品不存在");
|
throw new KnownException("礼品不存在");
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||||
using Fengling.Backend.Infrastructure.Repositories;
|
using Fengling.Backend.Infrastructure.Repositories;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Fengling.Backend.Web.Utils;
|
||||||
|
|
||||||
namespace Fengling.Backend.Web.Application.Commands.Members;
|
namespace Fengling.Backend.Web.Application.Commands.Members;
|
||||||
|
|
||||||
@ -32,7 +37,8 @@ public class LoginMemberCommandValidator : AbstractValidator<LoginMemberCommand>
|
|||||||
/// 会员登录命令处理器
|
/// 会员登录命令处理器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LoginMemberCommandHandler(
|
public class LoginMemberCommandHandler(
|
||||||
IMemberRepository memberRepository)
|
IMemberRepository memberRepository,
|
||||||
|
IConfiguration configuration)
|
||||||
: ICommandHandler<LoginMemberCommand, LoginMemberResponse>
|
: ICommandHandler<LoginMemberCommand, LoginMemberResponse>
|
||||||
{
|
{
|
||||||
public async Task<LoginMemberResponse> Handle(LoginMemberCommand command, CancellationToken cancellationToken)
|
public async Task<LoginMemberResponse> Handle(LoginMemberCommand command, CancellationToken cancellationToken)
|
||||||
@ -50,8 +56,8 @@ public class LoginMemberCommandHandler(
|
|||||||
if (member.Status == MemberStatus.Disabled)
|
if (member.Status == MemberStatus.Disabled)
|
||||||
throw new KnownException("该账号已被禁用");
|
throw new KnownException("该账号已被禁用");
|
||||||
|
|
||||||
// 生成Token(这里简化处理)
|
// 生成JWT Token
|
||||||
var token = GenerateToken(member.Id);
|
var token = GenerateJwtToken(member.Id, member.Phone);
|
||||||
|
|
||||||
return new LoginMemberResponse(member.Id, token);
|
return new LoginMemberResponse(member.Id, token);
|
||||||
}
|
}
|
||||||
@ -61,9 +67,37 @@ public class LoginMemberCommandHandler(
|
|||||||
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
|
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GenerateToken(MemberId memberId)
|
private string GenerateJwtToken(MemberId memberId, string phone)
|
||||||
{
|
{
|
||||||
// TODO: 实际项目中应使用JWT
|
var appConfig = configuration.GetSection("AppConfiguration").Get<AppConfiguration>()
|
||||||
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"Member:{memberId}:{DateTime.UtcNow:O}"));
|
?? new AppConfiguration
|
||||||
|
{
|
||||||
|
JwtIssuer = "FenglingBackend",
|
||||||
|
JwtAudience = "FenglingBackend",
|
||||||
|
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!",
|
||||||
|
TokenExpiryInMinutes = 1440 // 24小时
|
||||||
|
};
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!"));
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, memberId.ToString()),
|
||||||
|
new Claim(ClaimTypes.Name, phone),
|
||||||
|
new Claim(ClaimTypes.Role, "Member"),
|
||||||
|
new Claim("member_id", memberId.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: appConfig.JwtIssuer,
|
||||||
|
audience: appConfig.JwtAudience,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddMinutes(appConfig.TokenExpiryInMinutes),
|
||||||
|
signingCredentials: creds);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.MemberLevelCode,
|
||||||
command.StartDate,
|
command.StartDate,
|
||||||
command.EndDate,
|
command.EndDate,
|
||||||
cancellationToken))
|
cancellationToken,
|
||||||
|
null)) // 新创建的规则,不需要排除任何规则
|
||||||
{
|
{
|
||||||
throw new KnownException("存在冲突的积分规则,同一维度和时间范围内不允许重复规则");
|
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>
|
/// </summary>
|
||||||
public record CreateRedemptionOrderCommand(
|
public record CreateRedemptionOrderCommand(
|
||||||
Guid MemberId,
|
MemberId MemberId,
|
||||||
Guid GiftId,
|
GiftId GiftId,
|
||||||
int Quantity,
|
int Quantity,
|
||||||
AddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
|
CreateRedemptionOrderAddressDto? ShippingAddress = null) : ICommand<RedemptionOrderId>;
|
||||||
|
|
||||||
public record AddressDto(
|
public record CreateRedemptionOrderAddressDto(
|
||||||
string ReceiverName,
|
string ReceiverName,
|
||||||
string Phone,
|
string Phone,
|
||||||
string Province,
|
string Province,
|
||||||
@ -30,8 +30,7 @@ public class CreateRedemptionOrderCommandHandler(
|
|||||||
public async Task<RedemptionOrderId> Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken)
|
public async Task<RedemptionOrderId> Handle(CreateRedemptionOrderCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. 获取礼品信息
|
// 1. 获取礼品信息
|
||||||
var giftId = new GiftId(request.GiftId);
|
var gift = await giftRepository.GetByIdAsync(request.GiftId, cancellationToken);
|
||||||
var gift = await giftRepository.GetByIdAsync(giftId, cancellationToken);
|
|
||||||
if (gift == null)
|
if (gift == null)
|
||||||
throw new KnownException("礼品不存在");
|
throw new KnownException("礼品不存在");
|
||||||
|
|
||||||
@ -46,15 +45,14 @@ public class CreateRedemptionOrderCommandHandler(
|
|||||||
if (gift.LimitPerMember.HasValue)
|
if (gift.LimitPerMember.HasValue)
|
||||||
{
|
{
|
||||||
var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync(
|
var redeemedCount = await redemptionOrderRepository.GetMemberRedemptionCountAsync(
|
||||||
request.MemberId, request.GiftId, cancellationToken);
|
request.MemberId.Id, request.GiftId.Id, cancellationToken);
|
||||||
|
|
||||||
if (redeemedCount + request.Quantity > gift.LimitPerMember.Value)
|
if (redeemedCount + request.Quantity > gift.LimitPerMember.Value)
|
||||||
throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个");
|
throw new KnownException($"超出限兑数量,每人限兑{gift.LimitPerMember.Value}个,已兑换{redeemedCount}个");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 获取会员信息并检查积分
|
// 4. 获取会员信息并检查积分
|
||||||
var memberId = new MemberId(request.MemberId);
|
var member = await memberRepository.GetAsync(request.MemberId, cancellationToken);
|
||||||
var member = await memberRepository.GetAsync(memberId, cancellationToken);
|
|
||||||
if (member == null)
|
if (member == null)
|
||||||
throw new KnownException("会员不存在");
|
throw new KnownException("会员不存在");
|
||||||
|
|
||||||
@ -89,8 +87,8 @@ public class CreateRedemptionOrderCommandHandler(
|
|||||||
// 8. 创建订单
|
// 8. 创建订单
|
||||||
var order = new RedemptionOrder(
|
var order = new RedemptionOrder(
|
||||||
orderNo,
|
orderNo,
|
||||||
request.MemberId,
|
request.MemberId.Id,
|
||||||
request.GiftId,
|
request.GiftId.Id,
|
||||||
gift.Name,
|
gift.Name,
|
||||||
(int)gift.Type,
|
(int)gift.Type,
|
||||||
request.Quantity,
|
request.Quantity,
|
||||||
@ -106,15 +104,14 @@ public class CreateRedemptionOrderCommandHandler(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标记订单为已发货命令
|
/// 标记订单为已发货命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record MarkOrderAsDispatchedCommand(Guid OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
|
public record MarkOrderAsDispatchedCommand(RedemptionOrderId OrderId, string? TrackingNo = null) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class MarkOrderAsDispatchedCommandHandler(
|
public class MarkOrderAsDispatchedCommandHandler(
|
||||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData>
|
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<MarkOrderAsDispatchedCommand, ResponseData>
|
||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(MarkOrderAsDispatchedCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orderId = new RedemptionOrderId(request.OrderId);
|
var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
|
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
throw new KnownException("订单不存在");
|
throw new KnownException("订单不存在");
|
||||||
@ -129,15 +126,14 @@ public class MarkOrderAsDispatchedCommandHandler(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 完成订单命令
|
/// 完成订单命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CompleteOrderCommand(Guid OrderId) : ICommand<ResponseData>;
|
public record CompleteOrderCommand(RedemptionOrderId OrderId) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class CompleteOrderCommandHandler(
|
public class CompleteOrderCommandHandler(
|
||||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData>
|
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CompleteOrderCommand, ResponseData>
|
||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orderId = new RedemptionOrderId(request.OrderId);
|
var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
|
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
throw new KnownException("订单不存在");
|
throw new KnownException("订单不存在");
|
||||||
@ -152,15 +148,14 @@ public class CompleteOrderCommandHandler(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 取消订单命令
|
/// 取消订单命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record CancelOrderCommand(Guid OrderId, string Reason) : ICommand<ResponseData>;
|
public record CancelOrderCommand(RedemptionOrderId OrderId, string Reason) : ICommand<ResponseData>;
|
||||||
|
|
||||||
public class CancelOrderCommandHandler(
|
public class CancelOrderCommandHandler(
|
||||||
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData>
|
IRedemptionOrderRepository redemptionOrderRepository) : ICommandHandler<CancelOrderCommand, ResponseData>
|
||||||
{
|
{
|
||||||
public async Task<ResponseData> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
|
public async Task<ResponseData> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orderId = new RedemptionOrderId(request.OrderId);
|
var order = await redemptionOrderRepository.GetByIdAsync(request.OrderId, cancellationToken);
|
||||||
var order = await redemptionOrderRepository.GetByIdAsync(orderId, cancellationToken);
|
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
throw new KnownException("订单不存在");
|
throw new KnownException("订单不存在");
|
||||||
|
|||||||
@ -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;
|
using Fengling.Backend.Infrastructure;
|
||||||
|
|
||||||
namespace Fengling.Backend.Web.Application.Queries.Gifts;
|
namespace Fengling.Backend.Web.Application.Queries.Gifts;
|
||||||
@ -66,14 +67,14 @@ public class GetGiftsQueryHandler(ApplicationDbContext dbContext) : IQueryHandle
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 礼品详情查询
|
/// 礼品详情查询
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record GetGiftByIdQuery(Guid GiftId) : IQuery<GiftDto?>;
|
public record GetGiftByIdQuery(GiftId GiftId) : IQuery<GiftDto?>;
|
||||||
|
|
||||||
public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?>
|
public class GetGiftByIdQueryHandler(ApplicationDbContext dbContext) : IQueryHandler<GetGiftByIdQuery, GiftDto?>
|
||||||
{
|
{
|
||||||
public async Task<GiftDto?> Handle(GetGiftByIdQuery request, CancellationToken cancellationToken)
|
public async Task<GiftDto?> Handle(GetGiftByIdQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var gift = await dbContext.Gifts
|
var gift = await dbContext.Gifts
|
||||||
.Where(x => x.Id.Id == request.GiftId)
|
.Where(x => x.Id == request.GiftId)
|
||||||
.Select(x => new GiftDto
|
.Select(x => new GiftDto
|
||||||
{
|
{
|
||||||
Id = x.Id.Id,
|
Id = x.Id.Id,
|
||||||
|
|||||||
@ -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 GiftType { get; init; }
|
||||||
public int Quantity { get; init; }
|
public int Quantity { get; init; }
|
||||||
public int ConsumedPoints { get; init; }
|
public int ConsumedPoints { get; init; }
|
||||||
public AddressDto? ShippingAddress { get; init; }
|
public RedemptionOrderAddressDto? ShippingAddress { get; init; }
|
||||||
public string? TrackingNo { get; init; }
|
public string? TrackingNo { get; init; }
|
||||||
public int Status { get; init; }
|
public int Status { get; init; }
|
||||||
public string? CancelReason { get; init; }
|
public string? CancelReason { get; init; }
|
||||||
@ -25,7 +25,7 @@ public record RedemptionOrderDto
|
|||||||
public DateTime UpdatedAt { get; init; }
|
public DateTime UpdatedAt { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AddressDto
|
public record RedemptionOrderAddressDto
|
||||||
{
|
{
|
||||||
public string ReceiverName { get; init; } = string.Empty;
|
public string ReceiverName { get; init; } = string.Empty;
|
||||||
public string Phone { get; init; } = string.Empty;
|
public string Phone { get; init; } = string.Empty;
|
||||||
@ -64,7 +64,7 @@ public class GetRedemptionOrdersQueryHandler(ApplicationDbContext dbContext)
|
|||||||
GiftType = x.GiftType,
|
GiftType = x.GiftType,
|
||||||
Quantity = x.Quantity,
|
Quantity = x.Quantity,
|
||||||
ConsumedPoints = x.ConsumedPoints,
|
ConsumedPoints = x.ConsumedPoints,
|
||||||
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto
|
ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto
|
||||||
{
|
{
|
||||||
ReceiverName = x.ShippingAddress.ReceiverName,
|
ReceiverName = x.ShippingAddress.ReceiverName,
|
||||||
Phone = x.ShippingAddress.Phone,
|
Phone = x.ShippingAddress.Phone,
|
||||||
@ -107,7 +107,7 @@ public class GetRedemptionOrderByIdQueryHandler(ApplicationDbContext dbContext)
|
|||||||
GiftType = x.GiftType,
|
GiftType = x.GiftType,
|
||||||
Quantity = x.Quantity,
|
Quantity = x.Quantity,
|
||||||
ConsumedPoints = x.ConsumedPoints,
|
ConsumedPoints = x.ConsumedPoints,
|
||||||
ShippingAddress = x.ShippingAddress == null ? null : new AddressDto
|
ShippingAddress = x.ShippingAddress == null ? null : new RedemptionOrderAddressDto
|
||||||
{
|
{
|
||||||
ReceiverName = x.ShippingAddress.ReceiverName,
|
ReceiverName = x.ShippingAddress.ReceiverName,
|
||||||
Phone = x.ShippingAddress.Phone,
|
Phone = x.ShippingAddress.Phone,
|
||||||
|
|||||||
@ -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 FastEndpoints;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.GiftAggregate;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||||
using Fengling.Backend.Web.Application.Commands.RedemptionOrders;
|
using Fengling.Backend.Web.Application.Commands.RedemptionOrders;
|
||||||
using Fengling.Backend.Web.Application.Queries.Gifts;
|
using Fengling.Backend.Web.Application.Queries.Gifts;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Fengling.Backend.Web.Endpoints.Gifts;
|
namespace Fengling.Backend.Web.Endpoints.Gifts;
|
||||||
|
|
||||||
@ -37,17 +40,71 @@ public class GetGiftDetailEndpoint(IMediator mediator)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兑换礼品请求
|
||||||
|
/// </summary>
|
||||||
|
public record RedeemGiftRequest(
|
||||||
|
string GiftId,
|
||||||
|
int Quantity,
|
||||||
|
RedeemGiftAddressDto? ShippingAddress = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收货地址
|
||||||
|
/// </summary>
|
||||||
|
public record RedeemGiftAddressDto(
|
||||||
|
string ReceiverName,
|
||||||
|
string ReceiverPhone,
|
||||||
|
string Province,
|
||||||
|
string City,
|
||||||
|
string District,
|
||||||
|
string DetailAddress);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 兑换礼品端点(会员端)
|
/// 兑换礼品端点(会员端)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Tags("Gifts")]
|
[Tags("Gifts")]
|
||||||
[HttpPost("/api/gifts/redeem")]
|
[HttpPost("/api/gifts/redeem")]
|
||||||
public class RedeemGiftEndpoint(IMediator mediator)
|
public class RedeemGiftEndpoint(IMediator mediator)
|
||||||
: Endpoint<CreateRedemptionOrderCommand, ResponseData<Guid>>
|
: Endpoint<RedeemGiftRequest, ResponseData<string>>
|
||||||
{
|
{
|
||||||
public override async Task HandleAsync(CreateRedemptionOrderCommand req, CancellationToken ct)
|
public override async Task HandleAsync(RedeemGiftRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var orderId = await mediator.Send(req, ct);
|
// 从 JWT Claims 中提取 MemberId
|
||||||
await Send.OkAsync(orderId.Id.AsResponseData(), ct);
|
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
|
||||||
|
{
|
||||||
|
await Send.UnauthorizedAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Guid.TryParse(req.GiftId, out var giftGuid))
|
||||||
|
{
|
||||||
|
throw new KnownException("礼品ID格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberId = new MemberId(memberGuid);
|
||||||
|
var giftId = new GiftId(giftGuid);
|
||||||
|
|
||||||
|
CreateRedemptionOrderAddressDto? shippingAddress = null;
|
||||||
|
if (req.ShippingAddress != null)
|
||||||
|
{
|
||||||
|
shippingAddress = new CreateRedemptionOrderAddressDto(
|
||||||
|
req.ShippingAddress.ReceiverName,
|
||||||
|
req.ShippingAddress.ReceiverPhone,
|
||||||
|
req.ShippingAddress.Province,
|
||||||
|
req.ShippingAddress.City,
|
||||||
|
req.ShippingAddress.District,
|
||||||
|
req.ShippingAddress.DetailAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = new CreateRedemptionOrderCommand(
|
||||||
|
memberId,
|
||||||
|
giftId,
|
||||||
|
req.Quantity,
|
||||||
|
shippingAddress);
|
||||||
|
|
||||||
|
var orderId = await mediator.Send(command, ct);
|
||||||
|
await Send.OkAsync(orderId.Id.ToString().AsResponseData(), ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||||
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
|
using Fengling.Backend.Domain.AggregatesModel.MarketingCodeAggregate;
|
||||||
@ -8,7 +9,7 @@ namespace Fengling.Backend.Web.Endpoints.MarketingCodes;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫码请求
|
/// 扫码请求
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record UseMarketingCodeRequest(string Code, MemberId MemberId);
|
public record UseMarketingCodeRequest(string Code);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫码响应
|
/// 扫码响应
|
||||||
@ -24,13 +25,22 @@ public record UseMarketingCodeEndpointResponse(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Tags("MarketingCodes")]
|
[Tags("MarketingCodes")]
|
||||||
[HttpPost("/api/marketing-codes/scan")]
|
[HttpPost("/api/marketing-codes/scan")]
|
||||||
[AllowAnonymous]
|
|
||||||
public class UseMarketingCodeEndpoint(IMediator mediator)
|
public class UseMarketingCodeEndpoint(IMediator mediator)
|
||||||
: Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>>
|
: Endpoint<UseMarketingCodeRequest, ResponseData<UseMarketingCodeEndpointResponse>>
|
||||||
{
|
{
|
||||||
public override async Task HandleAsync(UseMarketingCodeRequest req, CancellationToken ct)
|
public override async Task HandleAsync(UseMarketingCodeRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var command = new UseMarketingCodeCommand(req.Code, req.MemberId);
|
// 从 JWT Claims 中提取 MemberId
|
||||||
|
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
|
||||||
|
{
|
||||||
|
await Send.UnauthorizedAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberId = new MemberId(memberGuid);
|
||||||
|
var command = new UseMarketingCodeCommand(req.Code, memberId);
|
||||||
var result = await mediator.Send(command, ct);
|
var result = await mediator.Send(command, ct);
|
||||||
|
|
||||||
var response = new UseMarketingCodeEndpointResponse(
|
var response = new UseMarketingCodeEndpointResponse(
|
||||||
|
|||||||
@ -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 FastEndpoints;
|
||||||
|
using Fengling.Backend.Domain.AggregatesModel.MemberAggregate;
|
||||||
using Fengling.Backend.Web.Application.Queries.RedemptionOrders;
|
using Fengling.Backend.Web.Application.Queries.RedemptionOrders;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Fengling.Backend.Web.Endpoints.RedemptionOrders;
|
namespace Fengling.Backend.Web.Endpoints.RedemptionOrders;
|
||||||
|
|
||||||
@ -13,15 +15,23 @@ public class GetMyRedemptionOrdersEndpoint(IMediator mediator)
|
|||||||
{
|
{
|
||||||
public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct)
|
public override async Task HandleAsync(GetMyRedemptionOrdersRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// TODO: 从JWT Token中获取当前登录会员ID
|
// 从 JWT Claims 中提取 MemberId
|
||||||
// 暂时使用请求中的MemberId
|
var memberIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
var query = new GetRedemptionOrdersQuery(MemberId: req.MemberId, Status: req.Status);
|
|
||||||
|
if (string.IsNullOrEmpty(memberIdClaim) || !Guid.TryParse(memberIdClaim, out var memberGuid))
|
||||||
|
{
|
||||||
|
await Send.UnauthorizedAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberId = memberGuid;
|
||||||
|
var query = new GetRedemptionOrdersQuery(MemberId: memberId, Status: req.Status);
|
||||||
var orders = await mediator.Send(query, ct);
|
var orders = await mediator.Send(query, ct);
|
||||||
await Send.OkAsync(orders.AsResponseData(), ct);
|
await Send.OkAsync(orders.AsResponseData(), ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record GetMyRedemptionOrdersRequest(Guid MemberId, int? Status = null);
|
public record GetMyRedemptionOrdersRequest(int? Status = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取订单详情端点(会员端)
|
/// 获取订单详情端点(会员端)
|
||||||
|
|||||||
@ -19,10 +19,13 @@ using Newtonsoft.Json;
|
|||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using Refit;
|
using Refit;
|
||||||
using NetCorePal.Extensions.CodeAnalysis;
|
using NetCorePal.Extensions.CodeAnalysis;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.Enrich.WithClientIp()
|
.Enrich.WithClientIp()
|
||||||
.WriteTo.Console(new JsonFormatter())
|
.WriteTo.Console()
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -59,17 +62,34 @@ try
|
|||||||
|
|
||||||
// 配置JWT认证
|
// 配置JWT认证
|
||||||
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
|
builder.Services.Configure<AppConfiguration>(builder.Configuration.GetSection("AppConfiguration"));
|
||||||
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>() ?? new AppConfiguration { JwtIssuer = "netcorepal", JwtAudience = "netcorepal" };
|
var appConfig = builder.Configuration.GetSection("AppConfiguration").Get<AppConfiguration>()
|
||||||
|
?? new AppConfiguration
|
||||||
|
{
|
||||||
|
JwtIssuer = "FenglingBackend",
|
||||||
|
JwtAudience = "FenglingBackend",
|
||||||
|
Secret = "YourVerySecretKeyForJwtTokenGeneration12345!"
|
||||||
|
};
|
||||||
|
|
||||||
builder.Services.AddAuthentication().AddJwtBearer(options =>
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
{
|
appConfig.Secret.Length >= 32 ? appConfig.Secret : "YourVerySecretKeyForJwtTokenGeneration12345!"));
|
||||||
options.RequireHttpsMetadata = false;
|
|
||||||
options.TokenValidationParameters.ValidAudience = appConfig.JwtAudience;
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
options.TokenValidationParameters.ValidateAudience = true;
|
.AddJwtBearer(options =>
|
||||||
options.TokenValidationParameters.ValidIssuer = appConfig.JwtIssuer;
|
{
|
||||||
options.TokenValidationParameters.ValidateIssuer = true;
|
options.RequireHttpsMetadata = false;
|
||||||
});
|
options.SaveToken = true;
|
||||||
builder.Services.AddNetCorePalJwt().AddRedisStore();
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = appConfig.JwtIssuer,
|
||||||
|
ValidAudience = appConfig.JwtAudience,
|
||||||
|
IssuerSigningKey = key,
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -143,6 +163,9 @@ try
|
|||||||
.AddKnownExceptionValidationBehavior()
|
.AddKnownExceptionValidationBehavior()
|
||||||
.AddUnitOfWorkBehaviors());
|
.AddUnitOfWorkBehaviors());
|
||||||
|
|
||||||
|
// 文件存储服务
|
||||||
|
builder.Services.AddSingleton<Fengling.Backend.Web.Services.IFileStorageService, Fengling.Backend.Web.Services.LocalFileStorageService>();
|
||||||
|
|
||||||
#region 多环境支持与服务注册发现
|
#region 多环境支持与服务注册发现
|
||||||
|
|
||||||
builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template")
|
builder.Services.AddMultiEnv(envOption => envOption.ServiceName = "Abc.Template")
|
||||||
@ -180,12 +203,16 @@ try
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// 在非生产环境中执行数据库迁移(包括开发、测试、Staging等环境)
|
// 在非生产环境中执行数据库迁移(包括开发、测试、Staging等环境)
|
||||||
if (!app.Environment.IsProduction())
|
if (!app.Environment.IsProduction())
|
||||||
{
|
{
|
||||||
using var scope = app.Services.CreateScope();
|
using var scope = app.Services.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
await dbContext.Database.MigrateAsync();
|
await dbContext.Database.MigrateAsync();
|
||||||
|
|
||||||
|
// 初始化默认管理员账号
|
||||||
|
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
await mediator.Send(new Fengling.Backend.Web.Application.Commands.AdminAuth.InitializeDefaultAdminCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -198,6 +225,15 @@ try
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
app.UseCors(x =>
|
||||||
|
{
|
||||||
|
x
|
||||||
|
.SetIsOriginAllowed(_=>true)
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.Build();
|
||||||
|
});
|
||||||
//app.UseHttpsRedirection();
|
//app.UseHttpsRedirection();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseAuthentication(); // Authentication 必须在 Authorization 之前
|
app.UseAuthentication(); // Authentication 必须在 Authorization 之前
|
||||||
|
|||||||
@ -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": {
|
"ConnectionStrings": {
|
||||||
"SQLite": "Data Source=fengling.db",
|
"SQLite": "Data Source=fengling.db",
|
||||||
"Redis": "81.68.223.70:6379"
|
"Redis": "81.68.223.70:16379,password=sl52788542"
|
||||||
},
|
},
|
||||||
"Services": {
|
"Services": {
|
||||||
"user": {
|
"user": {
|
||||||
|
|||||||
@ -8,10 +8,10 @@
|
|||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"SQLite": "Data Source=fengling.db",
|
"SQLite": "Data Source=fengling.db",
|
||||||
"Redis": "81.68.223.70:6379"
|
"Redis": "81.68.223.70:16379,password=sl52788542"
|
||||||
},
|
},
|
||||||
"RedisStreams": {
|
"RedisStreams": {
|
||||||
"ConnectionString": "81.68.223.70:6379"
|
"ConnectionString": "81.68.223.70:16379,password=sl52788542"
|
||||||
},
|
},
|
||||||
"Services": {
|
"Services": {
|
||||||
"user": {
|
"user": {
|
||||||
@ -24,5 +24,11 @@
|
|||||||
"https://user-v2:8443"
|
"https://user-v2:8443"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"AppConfiguration": {
|
||||||
|
"Secret": "YourVerySecretKeyForJwtTokenGeneration12345!",
|
||||||
|
"TokenExpiryInMinutes": 1440,
|
||||||
|
"JwtIssuer": "FenglingBackend",
|
||||||
|
"JwtAudience": "FenglingBackend"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 |