添加项目文件。
This commit is contained in:
41
TechHelper.Server/Context/ApplicationContext.cs
Normal file
41
TechHelper.Server/Context/ApplicationContext.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using TechHelper.Context.Configuration;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Entities.Contracts;
|
||||
|
||||
namespace TechHelper.Context
|
||||
{
|
||||
public class ApplicationContext : IdentityDbContext<User, IdentityRole<Guid>, Guid>
|
||||
{
|
||||
public ApplicationContext(DbContextOptions options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<AssignmentClass> AssignmentClasses { get; set; }
|
||||
public DbSet<Assignment> Assignments { get; set; }
|
||||
public DbSet<AssignmentQuestion> AssignmentGroups { get; set; }
|
||||
public DbSet<AssignmentQuestion> AssignmentQuestions { get; set; }
|
||||
public DbSet<Class> Classes { get; set; }
|
||||
public DbSet<ClassTeacher> ClassStudents { get; set; }
|
||||
public DbSet<ClassTeacher> ClassTeachers { get; set; }
|
||||
public DbSet<Question> Questions { get; set; }
|
||||
public DbSet<Submission> Submissions { get; set; }
|
||||
public DbSet<SubmissionDetail> SubmissionDetails { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.ApplyConfiguration(new RoleConfiguration());
|
||||
builder.ApplyConfiguration(new AssignmentConfiguration());
|
||||
builder.ApplyConfiguration(new AssignmentClassConfiguration());
|
||||
builder.ApplyConfiguration(new AssignmentGroupConfiguration());
|
||||
builder.ApplyConfiguration(new AssignmentQuestionConfiguration());
|
||||
builder.ApplyConfiguration(new ClassConfiguration());
|
||||
builder.ApplyConfiguration(new ClassStudentConfiguration());
|
||||
builder.ApplyConfiguration(new ClassTeacherConfiguration());
|
||||
builder.ApplyConfiguration(new QuestionConfiguration());
|
||||
builder.ApplyConfiguration(new SubmissionConfiguration());
|
||||
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
24
TechHelper.Server/Context/AutoMapperProFile.cs
Normal file
24
TechHelper.Server/Context/AutoMapperProFile.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using Entities.Contracts;
|
||||
using Entities.DTO;
|
||||
|
||||
namespace TechHelper.Context
|
||||
{
|
||||
public class AutoMapperProFile : Profile
|
||||
{
|
||||
public AutoMapperProFile()
|
||||
{
|
||||
CreateMap<UserForRegistrationDto, User>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // ID由IdentityUser生成
|
||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name)) // 或者 MapFrom(src => src.Name)
|
||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
|
||||
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.HomeAddress)) // 映射到 IdentityUser 的 Address 属性
|
||||
.ForMember(dest => dest.PasswordHash, opt => opt.Ignore()) // 密码哈希由 UserManager 处理
|
||||
.ForMember(dest => dest.EmailConfirmed, opt => opt.Ignore()); // 邮箱确认状态由服务层处理
|
||||
|
||||
CreateMap<ClassDto, Class>()
|
||||
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class)).ReverseMap();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class AssignmentAttachmentConfiguration : IEntityTypeConfiguration<AssignmentAttachment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentAttachment> builder)
|
||||
{
|
||||
builder.ToTable("assignment_attachments");
|
||||
|
||||
builder.HasKey(aa => aa.Id);
|
||||
|
||||
builder.Property(aa => aa.Id)
|
||||
.HasColumnName("id");
|
||||
|
||||
builder.Property(aa => aa.AssignmentId)
|
||||
.HasColumnName("assignment_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(aa => aa.FilePath)
|
||||
.HasColumnName("file_path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255);
|
||||
|
||||
builder.Property(aa => aa.FileName)
|
||||
.HasColumnName("file_name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255);
|
||||
|
||||
builder.Property(aa => aa.UploadedAt)
|
||||
.HasColumnName("uploaded_at")
|
||||
.ValueGeneratedOnAdd(); // Set value on creation
|
||||
|
||||
builder.Property(aa => aa.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
// Configure the relationship explicitly
|
||||
builder.HasOne(aa => aa.Assignment) // An AssignmentAttachment has one Assignment
|
||||
.WithMany(a => a.AssignmentAttachments) // An Assignment has many AssignmentAttachments (assuming 'Attachments' collection in Assignment)
|
||||
.HasForeignKey(aa => aa.AssignmentId) // The foreign key is AssignmentAttachment.AssignmentId
|
||||
.IsRequired() // It's a required relationship based on your [Required] attribute
|
||||
.OnDelete(DeleteBehavior.Cascade); // If an Assignment is deleted, its attachments should also be deleted
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class AssignmentClassConfiguration : IEntityTypeConfiguration<AssignmentClass>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentClass> builder)
|
||||
{
|
||||
// 设置表名为 "assignment_class"
|
||||
builder.ToTable("assignment_class");
|
||||
|
||||
// 设置复合主键
|
||||
builder.HasKey(ac => new { ac.AssignmentId, ac.ClassId });
|
||||
|
||||
// 配置 AssignmentId 列名
|
||||
builder.Property(ac => ac.AssignmentId)
|
||||
.HasColumnName("assignment_id");
|
||||
|
||||
// 配置 ClassId 列名
|
||||
builder.Property(ac => ac.ClassId)
|
||||
.HasColumnName("class_id");
|
||||
|
||||
// 配置 AssignedAt 列名
|
||||
builder.Property(ac => ac.AssignedAt)
|
||||
.HasColumnName("assigned_at")
|
||||
.IsRequired(); // 通常时间字段不能为空
|
||||
|
||||
// 配置 IsDeleted 列名
|
||||
builder.Property(ac => ac.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 可以设置默认值,表示默认未删除
|
||||
|
||||
// 配置到 Assignment 的关系 (多对一)
|
||||
// 假设 Assignment 类中有一个名为 AssignmentClasses 的集合属性
|
||||
builder.HasOne(ac => ac.Assignment) // AssignmentClass 有一个 Assignment
|
||||
.WithMany(a => a.AssignmentClasses) // Assignment 有多个 AssignmentClass 记录
|
||||
.HasForeignKey(ac => ac.AssignmentId) // 通过 AssignmentId 建立外键
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Assignment 被删除时,相关的 AssignmentClass 记录也级联删除
|
||||
|
||||
// 配置到 Class 的关系 (多对一)
|
||||
// 假设 Class 类中有一个名为 AssignmentClasses 的集合属性
|
||||
builder.HasOne(ac => ac.Class) // AssignmentClass 有一个 Class
|
||||
.WithMany(c => c.AssignmentClasses) // Class 有多个 AssignmentClass 记录
|
||||
.HasForeignKey(ac => ac.ClassId) // 通过 ClassId 建立外键
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Class 被删除时,相关的 AssignmentClass 记录也级联删除
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class AssignmentConfiguration : IEntityTypeConfiguration<Assignment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Assignment> builder)
|
||||
{
|
||||
builder.ToTable("assignments");
|
||||
|
||||
builder.HasKey(a => a.Id);
|
||||
|
||||
builder.Property(a => a.Id)
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
builder.Property(a => a.Title)
|
||||
.IsRequired()
|
||||
.HasColumnName("title")
|
||||
.HasMaxLength(255);
|
||||
|
||||
builder.Property(a => a.Description)
|
||||
.HasColumnName("description");
|
||||
|
||||
builder.Property(a => a.DueDate)
|
||||
.IsRequired()
|
||||
.HasColumnName("due_date");
|
||||
|
||||
builder.Property(a => a.TotalPoints)
|
||||
.HasColumnName("total_points");
|
||||
|
||||
builder.Property(a => a.CreatedBy)
|
||||
.HasColumnName("created_by");
|
||||
|
||||
builder.Property(a => a.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.Property(a => a.UpdatedAt)
|
||||
.IsRequired()
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
builder.Property(a => a.IsDeleted)
|
||||
.HasColumnName("deleted");
|
||||
|
||||
// 配置导航属性和关系
|
||||
|
||||
// 关系: Assignment (多) 到 User (一) - CreatedBy 外键
|
||||
// 假设 User 实体有 Id 作为主键
|
||||
// .WithMany() 表示 User 有多个 Assignment,但这里不指定 User 上的导航属性名称
|
||||
// 如果 User 有一个名为 AssignmentsCreated 的导航属性,应写为 .WithMany(u => u.AssignmentsCreated)
|
||||
builder.HasOne(a => a.Creator)
|
||||
.WithMany() // User 实体没有指向 Assignment 的导航属性 (或我们不知道)
|
||||
.HasForeignKey(a => a.CreatedBy)
|
||||
.IsRequired(); // CreatedBy 是必填的,对应 [Required] 在外键属性上
|
||||
|
||||
// 关系: Assignment (一) 到 AssignmentClass (多)
|
||||
// 假设 AssignmentClass 实体包含一个名为 AssignmentId 的外键属性
|
||||
builder.HasMany(a => a.AssignmentClasses)
|
||||
.WithOne(ac => ac.Assignment) // AssignmentClass 没有指向 Assignment 的导航属性 (或我们不知道)
|
||||
.HasForeignKey("AssignmentId") // 指定外键名称为 AssignmentId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 如果 Assignment 被删除,关联的 AssignmentClass 也会被删除
|
||||
|
||||
// 关系: Assignment (一) 到 AssignmentAttachment (多)
|
||||
// 假设 AssignmentAttachment 实体包含一个名为 AssignmentId 的外键属性
|
||||
builder.HasMany(a => a.AssignmentAttachments)
|
||||
.WithOne(aa => aa.Assignment)
|
||||
.HasForeignKey("AssignmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 关系: Assignment (一) 到 Submission (多)
|
||||
// 假设 Submission 实体包含一个名为 AssignmentId 的外键属性
|
||||
builder.HasMany(a => a.Submissions)
|
||||
.WithOne(s => s.Assignment)
|
||||
.HasForeignKey("AssignmentId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class AssignmentGroupConfiguration : IEntityTypeConfiguration<AssignmentGroup>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentGroup> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
// 将此实体映射到数据库中名为 "assignment_detail" 的表。
|
||||
builder.ToTable("assignment_group");
|
||||
|
||||
// 2. 配置主键
|
||||
// Id 属性作为主键。
|
||||
builder.HasKey(ag => ag.Id);
|
||||
|
||||
// 3. 配置列名、必需性、长度和默认值
|
||||
|
||||
// 配置 Id 属性对应的数据库列名为 "id"。
|
||||
builder.Property(ag => ag.Id)
|
||||
.HasColumnName("id");
|
||||
// EF Core 默认 Guid 类型主键由应用程序生成,因此无需 ValueGeneratedOnAdd()。
|
||||
|
||||
// 配置 AssignmentId 属性对应的数据库列名为 "assignment",并设置为必需字段。
|
||||
builder.Property(ag => ag.AssignmentId)
|
||||
.HasColumnName("assignment")
|
||||
.IsRequired();
|
||||
|
||||
// 配置 Title 属性对应的数据库列名为 "title",设置为必需字段,并设置最大长度。
|
||||
builder.Property(ag => ag.Title)
|
||||
.HasColumnName("title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
||||
|
||||
// 配置 Descript 属性对应的数据库列名为 "descript",并设置最大长度。
|
||||
builder.Property(ag => ag.Descript)
|
||||
.HasColumnName("descript")
|
||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
||||
|
||||
// 配置 TotalPoints 属性对应的数据库列名为 "total_points"。
|
||||
// TotalPoints 是 decimal? 类型,默认就是可选的,无需 IsRequired(false)。
|
||||
builder.Property(ag => ag.TotalPoints)
|
||||
.HasColumnName("total_points");
|
||||
|
||||
// 配置 Number 属性对应的数据库列名为 "number"。
|
||||
builder.Property(ag => ag.Number)
|
||||
.HasColumnName("number")
|
||||
.IsRequired(); // byte 默认非空,显式 IsRequired 增加可读性。
|
||||
|
||||
// 配置 ParentGroup 属性对应的数据库列名为 "sub_group"。
|
||||
// ParentGroup 是 Guid? 类型,默认就是可选的,无需 IsRequired(false)。
|
||||
builder.Property(ag => ag.ParentGroup)
|
||||
.HasColumnName("sub_group");
|
||||
|
||||
// 配置 IsDeleted 属性对应的数据库列名为 "deleted",并设置默认值为 false。
|
||||
builder.Property(ag => ag.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 适用于软删除策略
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// 配置 AssignmentGroup 到 Assignment 的多对一关系。
|
||||
// 一个 AssignmentGroup 记录属于一个 Assignment。
|
||||
builder.HasOne(ag => ag.Assignment) // 当前 AssignmentGroup 有一个 Assignment
|
||||
.WithMany(a => a.AssignmentGroups) // 该 Assignment 可以有多个 AssignmentGroup 记录
|
||||
.HasForeignKey(ag => ag.AssignmentId) // 通过 AssignmentId 建立外键
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当关联的 Assignment 被删除时,其所有相关的 AssignmentGroup 记录也级联删除。
|
||||
|
||||
// 配置 AssignmentGroup 到 AssignmentGroup 的自引用关系(父子关系)。
|
||||
// 一个 AssignmentGroup 可以有一个父 AssignmentGroup (SubAssignmentGroup)。
|
||||
// 假设父 AssignmentGroup 实体中有一个名为 ChildAssignmentGroups 的集合属性来表示它所包含的所有子组。
|
||||
builder.HasOne(ag => ag.ParentAssignmentGroup) // 当前 AssignmentGroup 有一个父 AssignmentGroup
|
||||
.WithMany(parentAg => parentAg.ChildAssignmentGroups) // 该父 AssignmentGroup 可以有多个子 AssignmentGroup
|
||||
.HasForeignKey(ag => ag.ParentGroup) // 通过 SubGroup 建立外键
|
||||
.IsRequired(false) // SubGroup 是可空的 (Guid?),所以这个关系是可选的。
|
||||
.OnDelete(DeleteBehavior.SetNull); // 当父 AssignmentGroup 被删除时,其子 AssignmentGroup 的 SubGroup 外键将被设置为 NULL。
|
||||
// 如果你希望父组被删除时子组不能脱离父组(即不允许父组被删除),
|
||||
// 可以使用 DeleteBehavior.Restrict 或 DeleteBehavior.NoAction。
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class AssignmentQuestionConfiguration : IEntityTypeConfiguration<AssignmentQuestion>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentQuestion> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
builder.ToTable("assignment_questions");
|
||||
|
||||
// 2. 设置主键
|
||||
builder.HasKey(aq => aq.Id);
|
||||
|
||||
// 3. 配置列名、必需性及其他属性
|
||||
|
||||
// 配置 Id 列
|
||||
builder.Property(aq => aq.Id)
|
||||
.HasColumnName("id");
|
||||
|
||||
// 配置 QuestionId 列 (已修正拼写)
|
||||
builder.Property(aq => aq.QuestionId)
|
||||
.HasColumnName("question_id")
|
||||
.IsRequired();
|
||||
|
||||
// 配置 QuestionNumber 列
|
||||
builder.Property(aq => aq.QuestionNumber)
|
||||
.HasColumnName("question_number")
|
||||
.IsRequired(); // uint 类型默认非空
|
||||
|
||||
// 配置 CreatedAt 列
|
||||
builder.Property(aq => aq.CreatedAt)
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired(); // 通常创建时间字段是非空的
|
||||
|
||||
// 配置 AssignmentGroupId 列
|
||||
// 该列在数据库中名为 "detail_id"
|
||||
builder.Property(aq => aq.AssignmentGroupId)
|
||||
.HasColumnName("detail_id")
|
||||
.IsRequired();
|
||||
|
||||
// 配置 IsDeleted 列
|
||||
builder.Property(aq => aq.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 适用于软删除策略
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 AssignmentQuestion 到 Question 的关系 (多对一)
|
||||
// 一个 AssignmentQuestion 属于一个 Question。
|
||||
//
|
||||
// 假设 `Question` 实体中有一个名为 `AssignmentQuestions` 的 `ICollection<AssignmentQuestion>` 集合属性。
|
||||
builder.HasOne(aq => aq.Question) // 当前 AssignmentQuestion 有一个 Question
|
||||
.WithMany(q => q.AssignmentQuestions) // 那个 Question 可以有多个 AssignmentQuestion
|
||||
.HasForeignKey(aq => aq.QuestionId) // 外键是 AssignmentQuestion.QuestionId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Question 被删除时,相关的 AssignmentQuestion 也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 AssignmentQuestion 到 AssignmentGroup 的关系 (多对一)
|
||||
// 一个 AssignmentQuestion 属于一个 AssignmentGroup。
|
||||
//
|
||||
// 你的 `AssignmentQuestion` 类现在有了 `public AssignmentGroup AssignmentGroup { get; set; }`
|
||||
// 这是一个非常好的改进,它与 `AssignmentGroupId` 外键完美匹配。
|
||||
// 假设 `AssignmentGroup` 实体中有一个名为 `AssignmentQuestions` 的 `ICollection<AssignmentQuestion>` 集合属性。
|
||||
builder.HasOne(aq => aq.AssignmentGroup) // 当前 AssignmentQuestion 有一个 AssignmentGroup
|
||||
.WithMany(ag => ag.AssignmentQuestions) // 那个 AssignmentGroup 可以有多个 AssignmentQuestion
|
||||
.HasForeignKey(aq => aq.AssignmentGroupId) // 外键是 AssignmentQuestion.AssignmentGroupId (列名 detail_id)
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 AssignmentGroup 被删除时,相关的 AssignmentQuestion 也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 AssignmentQuestion 到 SubmissionDetail 的关系 (一对多)
|
||||
// 一个 AssignmentQuestion 可以有多个 SubmissionDetail。
|
||||
//
|
||||
// 这个关系通常从 "多" 的一方(`SubmissionDetail` 实体)来配置外键。
|
||||
// 假设 `SubmissionDetail` 实体有一个 `AssignmentQuestionId` 外键和 `AssignmentQuestion` 导航属性。
|
||||
builder.HasMany(aq => aq.SubmissionDetails) // 当前 AssignmentQuestion 有多个 SubmissionDetail
|
||||
.WithOne(sd => sd.AssignmentQuestion); // 每一个 SubmissionDetail 都有一个 AssignmentQuestion
|
||||
// .HasForeignKey(sd => sd.AssignmentQuestionId); // 外键的配置应在 `SubmissionDetailConfiguration` 中进行
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class ClassConfiguration : IEntityTypeConfiguration<Class>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Class> builder)
|
||||
{
|
||||
// 1. 表名映射
|
||||
builder.ToTable("classes");
|
||||
|
||||
// 2. 主键
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Id)
|
||||
.HasColumnName("id");
|
||||
|
||||
builder.Property(c => c.Grade)
|
||||
.HasColumnName("grade");
|
||||
|
||||
// 3. Name 属性
|
||||
builder.Property(c => c.Number)
|
||||
.HasColumnName("class");
|
||||
|
||||
// 4. Description 属性
|
||||
builder.Property(c => c.Description)
|
||||
.HasColumnName("description")
|
||||
.IsRequired(false); // 明确其为可选,尽管 string 类型默认是可空的。
|
||||
|
||||
// 5. TeacherId (外键)
|
||||
builder.Property(c => c.HeadTeacherId)
|
||||
.HasColumnName("head_teacher_id")
|
||||
.IsRequired(false);
|
||||
|
||||
// 6. CreatedAt 属性
|
||||
builder.Property(c => c.CreatedAt)
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd();
|
||||
// DateTime 是非可空类型,因此按类型是必需的。
|
||||
// 如果希望数据库在创建时设置默认值,可以考虑 .HasDefaultValueSql("GETUTCDATE()") (SQL Server) 或类似方法。
|
||||
|
||||
// 7. UpdatedAt 属性
|
||||
builder.Property(c => c.UpdatedAt)
|
||||
.HasColumnName("updated_at")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate(); // DateTime 是非可空类型。
|
||||
// 如果需要在修改时自动更新,可以考虑配置(例如通过拦截器或触发器)。
|
||||
|
||||
// 8. IsDeleted 属性 (用于软删除)
|
||||
builder.Property(c => c.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false); // 通常默认为未删除。
|
||||
|
||||
// --- 导航属性配置 ---
|
||||
|
||||
// 9. 与 User (Teacher) 的关系
|
||||
builder.Property(c => c.HeadTeacherId).HasColumnName("head_teacher_id");
|
||||
builder.HasOne(c => c.HeadTeacher) // Class 实体中的导航属性
|
||||
.WithMany() // User 实体中可以不定义反向导航到其作为班主任的班级集合
|
||||
// 如果 User 实体中有 public ICollection<Class> HeadManagedClasses { get; set; }
|
||||
// 则这里应为 .WithMany(u => u.HeadManagedClasses)
|
||||
.HasForeignKey(c => c.HeadTeacherId) // Class 实体中的外键
|
||||
.IsRequired() // 班主任是必需的
|
||||
.OnDelete(DeleteBehavior.Restrict); // 删除用户时,如果其是班主任,则限制删除
|
||||
|
||||
// --- 授课老师关系 (多对多) ---
|
||||
// 这个关系主要通过 ClassTeacherConfiguration 来定义其两端。
|
||||
// Class 到 ClassTeacher 是一对多。
|
||||
builder.HasMany(c => c.ClassTeachers) // Class 实体中的集合导航属性
|
||||
.WithOne(ct => ct.Class) // ClassTeacher 实体中指向 Class 的导航属性
|
||||
.HasForeignKey(ct => ct.ClassId) // ClassTeacher 实体中的外键
|
||||
.OnDelete(DeleteBehavior.Cascade); // 如果班级被删除,相关的教师关联也应删除
|
||||
|
||||
// 10. 与 ClassStudent 的关系 (多对多中间表或一对多)
|
||||
// 一对多:一个 Class 有多个 ClassStudent。一个 ClassStudent 属于一个 Class。
|
||||
// 假设 ClassStudent 有 'ClassId' 外键和 'Class' 导航属性。
|
||||
builder.HasMany(c => c.ClassStudents) // Class 中的集合导航属性
|
||||
.WithOne(cs => cs.Class) // 假设 ClassStudent 中有 'public Class Class { get; set; }'
|
||||
.HasForeignKey(cs => cs.ClassId) // 假设 ClassStudent 中有 'public Guid ClassId { get; set; }'
|
||||
.OnDelete(DeleteBehavior.Cascade); // 常见:如果一个班级被删除,其学生注册记录也应被删除。
|
||||
|
||||
// 11. 与 AssignmentClass 的关系 (多对多中间表或一对多)
|
||||
// 一对多:一个 Class 有多个 AssignmentClass。一个 AssignmentClass 属于一个 Class。
|
||||
// 假设 AssignmentClass 有 'ClassId' 外键和 'Class' 导航属性。
|
||||
builder.HasMany(c => c.AssignmentClasses) // Class 中的集合导航属性
|
||||
.WithOne(ac => ac.Class) // 假设 AssignmentClass 中有 'public Class Class { get; set; }'
|
||||
.HasForeignKey(ac => ac.ClassId) // 假设 AssignmentClass 中有 'public Guid ClassId { get; set; }'
|
||||
.OnDelete(DeleteBehavior.Cascade); // 常见:如果一个班级被删除,其作业关联也应被删除。
|
||||
|
||||
// --- 可选:索引 ---
|
||||
// builder.HasIndex(c => c.TeacherId).HasDatabaseName("IX_classes_teacher_id");
|
||||
// builder.HasIndex(c => c.Name).IsUnique(false).HasDatabaseName("IX_classes_name"); // 如果经常按名称搜索
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class ClassStudentConfiguration : IEntityTypeConfiguration<ClassStudent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ClassStudent> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
// 将此实体映射到数据库中名为 "class_student" 的表。
|
||||
builder.ToTable("class_student");
|
||||
|
||||
// 2. 设置复合主键
|
||||
// ClassId 和 StudentId 的组合作为主键,确保一个班级中一个学生只有一条入学记录。
|
||||
builder.HasKey(cs => new { cs.ClassId, cs.StudentId });
|
||||
|
||||
// 3. 配置列名和属性特性
|
||||
|
||||
// 配置 ClassId 属性对应的数据库列名为 "class_id"。
|
||||
builder.Property(cs => cs.ClassId)
|
||||
.HasColumnName("class_id");
|
||||
|
||||
// 配置 StudentId 属性对应的数据库列名为 "student_id"。
|
||||
builder.Property(cs => cs.StudentId)
|
||||
.HasColumnName("student_id");
|
||||
|
||||
// 配置 EnrollmentDate 属性对应的数据库列名为 "enrollment_date",并设置为必需字段。
|
||||
builder.Property(cs => cs.EnrollmentDate)
|
||||
.HasColumnName("enrollment_date")
|
||||
.IsRequired();
|
||||
|
||||
// 配置 IsDeleted 属性对应的数据库列名为 "deleted",并设置默认值为 false。
|
||||
builder.Property(cs => cs.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 常用作软删除标记
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 ClassStudent 到 Class 的关系 (多对一)
|
||||
// 一个 ClassStudent 联结记录属于一个 Class。
|
||||
//
|
||||
// 假设 `Class` 实体中有一个名为 `ClassStudents` 的 `ICollection<ClassStudent>` 集合属性。
|
||||
builder.HasOne(cs => cs.Class) // 当前 ClassStudent 链接到一个 Class
|
||||
.WithMany(c => c.ClassStudents) // 那个 Class 可以有多个 ClassStudent 记录
|
||||
.HasForeignKey(cs => cs.ClassId) // 外键是 ClassStudent.ClassId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Class 被删除时,相关的 ClassStudent 记录也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 ClassStudent 到 User (Student) 的关系 (多对一)
|
||||
// 一个 ClassStudent 联结记录属于一个 User (作为 Student)。
|
||||
//
|
||||
// 假设 `User` 实体中有一个名为 `EnrolledClassesLink` 的 `ICollection<ClassStudent>` 集合属性,
|
||||
// 用于表示该用户所注册的班级联结记录 (与 `ClassTeacherConfiguration` 中的模式类似)。
|
||||
builder.HasOne(cs => cs.Student) // 当前 ClassStudent 链接到一个 User (学生)
|
||||
.WithMany(u => u.EnrolledClassesLink) // 那个 User (学生) 可以有多个 ClassStudent 记录
|
||||
.HasForeignKey(cs => cs.StudentId) // 外键是 ClassStudent.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 当 User (学生) 被删除时,如果还有相关的 ClassStudent 记录,则会阻止删除。
|
||||
// 这是更安全的做法,以避免意外数据丢失。如果你希望学生被删除时,其所有注册关系也一并删除,可改为 DeleteBehavior.Cascade。
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class ClassTeacherConfiguration : IEntityTypeConfiguration<ClassTeacher>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ClassTeacher> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
// 将此实体映射到数据库中名为 "class_teachers" 的表。
|
||||
builder.ToTable("class_teachers");
|
||||
|
||||
// 2. 设置复合主键
|
||||
// ClassId 和 TeacherId 的组合作为主键,确保一个班级中一个老师只有一条任教记录。
|
||||
// 这要求 ClassTeacher 类中的 ClassId 和 TeacherId 属性上都添加了 [Key] 特性。
|
||||
builder.HasKey(ct => new { ct.ClassId, ct.TeacherId });
|
||||
|
||||
// 3. 配置列名和属性特性
|
||||
|
||||
// 配置 ClassId 属性对应的数据库列名为 "class_id"。
|
||||
builder.Property(ct => ct.ClassId)
|
||||
.HasColumnName("class_id");
|
||||
|
||||
// 配置 TeacherId 属性对应的数据库列名为 "teacher_id"。
|
||||
builder.Property(ct => ct.TeacherId)
|
||||
.HasColumnName("teacher_id");
|
||||
|
||||
// 配置 SubjectTaught 属性对应的数据库列名为 "subject_taught"。
|
||||
// 假设 SubjectTaught 可以为空,所以没有 .IsRequired()
|
||||
builder.Property(ct => ct.SubjectTaught)
|
||||
.HasColumnName("subject_taught");
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 ClassTeacher 到 Class 的关系 (多对一)
|
||||
// 一个 ClassTeacher 联结记录属于一个 Class。
|
||||
//
|
||||
// 假设 `Class` 实体中有一个名为 `ClassTeachers` 的 `ICollection<ClassTeacher>` 集合属性。
|
||||
builder.HasOne(ct => ct.Class) // 当前 ClassTeacher 链接到一个 Class
|
||||
.WithMany(c => c.ClassTeachers) // 那个 Class 可以有多个 ClassTeacher 记录
|
||||
.HasForeignKey(ct => ct.ClassId) // 外键是 ClassTeacher.ClassId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Class 被删除时,相关的 ClassTeacher 记录也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 ClassTeacher 到 User (Teacher) 的关系 (多对一)
|
||||
// 一个 ClassTeacher 联结记录属于一个 User (作为 Teacher)。
|
||||
//
|
||||
// 假设 `User` 实体中有一个名为 `TaughtClassesLink` 的 `ICollection<ClassTeacher>` 集合属性,
|
||||
// 用于表示该用户所教授的班级联结记录。
|
||||
builder.HasOne(ct => ct.Teacher) // 当前 ClassTeacher 链接到一个 User (老师)
|
||||
.WithMany(u => u.TaughtClassesLink) // 那个 User (老师) 可以有多个 ClassTeacher 记录 (为所教授的班级)
|
||||
.HasForeignKey(ct => ct.TeacherId) // 外键是 ClassTeacher.TeacherId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 当 User (老师) 被删除时,如果还有相关的 ClassTeacher 记录,则会阻止删除。
|
||||
// 这通常是防止数据丢失的更安全选择。如果你希望老师被删除时,其所有任教关系也一并删除,可改为 DeleteBehavior.Cascade。
|
||||
}
|
||||
}
|
||||
|
||||
}
|
102
TechHelper.Server/Context/Configuration/QuestionConfiguration.cs
Normal file
102
TechHelper.Server/Context/Configuration/QuestionConfiguration.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class QuestionConfiguration : IEntityTypeConfiguration<Question>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Question> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
builder.ToTable("questions");
|
||||
|
||||
// 2. 设置主键
|
||||
builder.HasKey(q => q.Id);
|
||||
|
||||
// 3. 配置列名、必需性、长度及其他属性
|
||||
|
||||
// Id
|
||||
builder.Property(q => q.Id)
|
||||
.HasColumnName("id");
|
||||
// 对于 Guid 类型的主键,EF Core 默认由应用程序生成值,无需 ValueGeneratedOnAdd()
|
||||
|
||||
// QuestionText
|
||||
builder.Property(q => q.QuestionText)
|
||||
.HasColumnName("question_text")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
||||
|
||||
// QuestionType (枚举作为字符串存储)
|
||||
builder.Property(q => q.QuestionType)
|
||||
.HasColumnName("question_type")
|
||||
.IsRequired()
|
||||
.HasConversion<string>() // <-- 重要:将枚举存储为字符串
|
||||
.HasMaxLength(20); // <-- 应用最大长度
|
||||
|
||||
// CorrectAnswer
|
||||
builder.Property(q => q.CorrectAnswer)
|
||||
.HasColumnName("correct_answer")
|
||||
.HasMaxLength(65535); // 对应 MaxLength(65535)
|
||||
|
||||
// DifficultyLevel (枚举作为字符串存储)
|
||||
builder.Property(q => q.DifficultyLevel)
|
||||
.HasColumnName("difficulty_level")
|
||||
.HasConversion<string>() // <-- 重要:将枚举存储为字符串
|
||||
.HasMaxLength(10); // <-- 应用最大长度
|
||||
// DifficultyLevel 属性没有 [Required],所以默认是可选的
|
||||
|
||||
// SubjectArea
|
||||
builder.Property(q => q.SubjectArea)
|
||||
.HasColumnName("subject_area")
|
||||
.HasConversion<string>() // <--- 重要:将枚举存储为字符串
|
||||
.HasMaxLength(100); // <--- 应用最大长度 (从原 StringLength 继承)
|
||||
|
||||
// CreatedBy
|
||||
builder.Property(q => q.CreatedBy)
|
||||
.HasColumnName("created_by")
|
||||
.IsRequired();
|
||||
|
||||
// CreatedAt (自动在添加时设置)
|
||||
builder.Property(q => q.CreatedAt)
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired() // 通常创建时间是必需的
|
||||
.ValueGeneratedOnAdd(); // EF Core 在实体添加时自动设置值
|
||||
|
||||
// UpdatedAt (自动在添加或更新时设置,并作为并发令牌)
|
||||
builder.Property(q => q.UpdatedAt)
|
||||
.HasColumnName("updated_at")
|
||||
.IsRequired() // 通常更新时间是必需的
|
||||
.ValueGeneratedOnAddOrUpdate() // EF Core 在实体添加或更新时自动设置值
|
||||
.IsConcurrencyToken(); // 用作乐观并发令牌,防止数据冲突
|
||||
|
||||
// IsDeleted (软删除标记,默认 false)
|
||||
builder.Property(q => q.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 Question 到 User (Creator) 的关系 (多对一)
|
||||
// 一个 Question 由一个 User (Creator) 创建。
|
||||
//
|
||||
// 假设 `User` 实体中有一个名为 `CreatedQuestions` 的 `ICollection<Question>` 集合属性。
|
||||
builder.HasOne(q => q.Creator) // 当前 Question 有一个 Creator
|
||||
.WithMany(u => u.CreatedQuestions) // 那个 Creator 可以创建多个 Question
|
||||
.HasForeignKey(q => q.CreatedBy) // 外键是 Question.CreatedBy
|
||||
.OnDelete(DeleteBehavior.Restrict); // 当 User (Creator) 被删除时,如果还有他/她创建的 Question,则会阻止删除。
|
||||
// 由于 CreatedBy 是 [Required],Prevent/Restrict 是一个安全的选择。
|
||||
|
||||
// ---
|
||||
// 配置 Question 到 AssignmentQuestion 的关系 (一对多)
|
||||
// 一个 Question 可以被多个 AssignmentQuestion 引用。
|
||||
//
|
||||
// 这个关系的外键配置通常在 "多" 的一方(`AssignmentQuestion` 实体)进行。
|
||||
// 假设 `AssignmentQuestion` 实体有一个 `QuestionId` 外键和 `Question` 导航属性。
|
||||
builder.HasMany(q => q.AssignmentQuestions) // 当前 Question 有多个 AssignmentQuestion
|
||||
.WithOne(aq => aq.Question); // 每一个 AssignmentQuestion 都有一个 Question
|
||||
// .HasForeignKey(aq => aq.QuestionId); // 外键的配置应在 `AssignmentQuestionConfiguration` 中进行
|
||||
}
|
||||
}
|
||||
}
|
25
TechHelper.Server/Context/Configuration/RoleConfiguration.cs
Normal file
25
TechHelper.Server/Context/Configuration/RoleConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<IdentityRole<Guid>>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<IdentityRole<Guid>> builder)
|
||||
{
|
||||
builder.HasData(
|
||||
Enum.GetValues(typeof(UserRoles))
|
||||
.Cast<UserRoles>()
|
||||
.Select(roleEnum => new IdentityRole<Guid>
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = roleEnum.ToString(),
|
||||
NormalizedName = roleEnum.ToString().ToUpper()
|
||||
})
|
||||
.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class SubmissionConfiguration : IEntityTypeConfiguration<Submission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Submission> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
builder.ToTable("submissions");
|
||||
|
||||
// 2. 设置主键
|
||||
builder.HasKey(s => s.Id);
|
||||
|
||||
// 3. 配置列名、必需性、精度及其他属性
|
||||
|
||||
// Id
|
||||
builder.Property(s => s.Id)
|
||||
.HasColumnName("id");
|
||||
|
||||
// AssignmentId
|
||||
builder.Property(s => s.AssignmentId)
|
||||
.HasColumnName("assignment_id")
|
||||
.IsRequired();
|
||||
|
||||
// StudentId
|
||||
builder.Property(s => s.StudentId)
|
||||
.HasColumnName("student_id")
|
||||
.IsRequired();
|
||||
|
||||
// AttemptNumber
|
||||
// 注意:如果 AttemptNumber 应该是一个递增的数字,Guid 可能不是最合适的类型。
|
||||
// 但根据你的定义,这里按 Guid 类型配置。
|
||||
builder.Property(s => s.AttemptNumber)
|
||||
.HasColumnName("attempt_number")
|
||||
.IsRequired();
|
||||
|
||||
// SubmissionTime
|
||||
builder.Property(s => s.SubmissionTime)
|
||||
.HasColumnName("submission_time"); // 没有 [Required] 属性,所以可以是可空的
|
||||
|
||||
// OverallGrade
|
||||
builder.Property(s => s.OverallGrade)
|
||||
.HasColumnName("overall_grade")
|
||||
.HasPrecision(5, 2); // 应用精度设置
|
||||
|
||||
// OverallFeedback
|
||||
builder.Property(s => s.OverallFeedback)
|
||||
.HasColumnName("overall_feedback");
|
||||
|
||||
// GradedBy (现为 Guid? 类型)
|
||||
builder.Property(s => s.GradedBy)
|
||||
.HasColumnName("graded_by"); // 作为可空外键,不需要 IsRequired()
|
||||
|
||||
// GradedAt
|
||||
builder.Property(s => s.GradedAt)
|
||||
.HasColumnName("graded_at");
|
||||
|
||||
// IsDeleted
|
||||
builder.Property(s => s.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
// Status (枚举作为字符串存储)
|
||||
builder.Property(s => s.Status)
|
||||
.HasColumnName("status")
|
||||
.IsRequired()
|
||||
.HasConversion<string>() // <--- 重要:将枚举存储为字符串
|
||||
.HasMaxLength(15); // <--- 应用最大长度
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 Submission 到 Assignment 的关系 (多对一)
|
||||
// 一个 Submission 属于一个 Assignment。
|
||||
builder.HasOne(s => s.Assignment) // 当前 Submission 有一个 Assignment
|
||||
.WithMany(a => a.Submissions) // 那个 Assignment 可以有多个 Submission
|
||||
.HasForeignKey(s => s.AssignmentId) // 外键是 Submission.AssignmentId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Assignment 被删除时,相关的 Submission 也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 Submission 到 User (Student) 的关系 (多对一)
|
||||
// 一个 Submission 由一个 User (Student) 提交。
|
||||
builder.HasOne(s => s.Student) // 当前 Submission 有一个 Student (User)
|
||||
.WithMany(u => u.SubmissionsAsStudent) // 那个 User (Student) 可以有多个 Submission
|
||||
.HasForeignKey(s => s.StudentId) // 外键是 Submission.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 当 User (Student) 被删除时,如果还有其提交的 Submission,则会阻止删除。
|
||||
|
||||
// ---
|
||||
// 配置 Submission 到 User (Grader) 的关系 (多对一)
|
||||
// 一个 Submission 可以由一个 User (Grader) 批改 (可选)。
|
||||
builder.HasOne(s => s.Grader) // 当前 Submission 有一个 Grader (User),可以是空的
|
||||
.WithMany(u => u.GradedSubmissions) // 那个 User (Grader) 可以批改多个 Submission
|
||||
.HasForeignKey(s => s.GradedBy) // 外键是 Submission.GradedBy
|
||||
.OnDelete(DeleteBehavior.SetNull); // 当 User (Grader) 被删除时,如果 GradedBy 是可空的,则将其设置为 NULL。
|
||||
// 如果 GradedBy 是不可空的,需要改为 Restrict 或 Cascade。
|
||||
|
||||
// ---
|
||||
// 配置 Submission 到 SubmissionDetail 的关系 (一对多)
|
||||
// 一个 Submission 可以有多个 SubmissionDetail。
|
||||
// 这个关系的外键配置通常在 "多" 的一方 (`SubmissionDetail` 实体) 进行。
|
||||
builder.HasMany(s => s.SubmissionDetails) // 当前 Submission 有多个 SubmissionDetail
|
||||
.WithOne(sd => sd.Submission); // 每一个 SubmissionDetail 都有一个 Submission
|
||||
// .HasForeignKey(sd => sd.SubmissionId); // 外键的配置应在 `SubmissionDetailConfiguration` 中进行
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class SubmissionDetailConfiguration : IEntityTypeConfiguration<SubmissionDetail>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SubmissionDetail> builder)
|
||||
{
|
||||
// 1. 设置表名
|
||||
// 将此实体映射到数据库中名为 "submission_details" 的表。
|
||||
builder.ToTable("submission_details");
|
||||
|
||||
// 2. 设置主键
|
||||
// 将 Id 属性设置为主键。
|
||||
builder.HasKey(sd => sd.Id);
|
||||
|
||||
// 3. 配置列名、必需性、精度及其他属性
|
||||
|
||||
// Id 属性对应的数据库列名为 "id"。
|
||||
builder.Property(sd => sd.Id)
|
||||
.HasColumnName("id");
|
||||
|
||||
// SubmissionId 属性对应的数据库列名为 "submission_id",并设置为必需字段。
|
||||
builder.Property(sd => sd.SubmissionId)
|
||||
.HasColumnName("submission_id")
|
||||
.IsRequired();
|
||||
|
||||
// StudentId 属性对应的数据库列名为 "student_id",并设置为必需字段。
|
||||
// 此外键指向 User 实体,代表提交该详情的学生。
|
||||
builder.Property(sd => sd.StudentId)
|
||||
.HasColumnName("student_id")
|
||||
.IsRequired();
|
||||
|
||||
// AssignmentQuestionId 属性对应的数据库列名为 "assignment_question_id",并设置为必需字段。
|
||||
builder.Property(sd => sd.AssignmentQuestionId)
|
||||
.HasColumnName("assignment_question_id")
|
||||
.IsRequired();
|
||||
|
||||
// StudentAnswer 属性对应的数据库列名为 "student_answer"。
|
||||
builder.Property(sd => sd.StudentAnswer)
|
||||
.HasColumnName("student_answer"); // string 默认可空
|
||||
|
||||
// IsCorrect 属性对应的数据库列名为 "is_correct"。
|
||||
builder.Property(sd => sd.IsCorrect)
|
||||
.HasColumnName("is_correct"); // bool? 默认可空
|
||||
|
||||
// PointsAwarded 属性对应的数据库列名为 "points_awarded",并设置精度。
|
||||
builder.Property(sd => sd.PointsAwarded)
|
||||
.HasColumnName("points_awarded")
|
||||
.HasPrecision(5, 2); // 应用 [Precision(5, 2)] 设置
|
||||
|
||||
// TeacherFeedback 属性对应的数据库列名为 "teacher_feedback"。
|
||||
builder.Property(sd => sd.TeacherFeedback)
|
||||
.HasColumnName("teacher_feedback"); // string 默认可空
|
||||
|
||||
// CreatedAt 属性对应的数据库列名为 "created_at",设置为必需字段,并在添加时自动生成值。
|
||||
builder.Property(sd => sd.CreatedAt)
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd(); // 在实体首次保存时自动设置值
|
||||
|
||||
// UpdatedAt 属性对应的数据库列名为 "updated_at",设置为必需字段,并在添加或更新时自动生成值,同时作为并发令牌。
|
||||
builder.Property(sd => sd.UpdatedAt)
|
||||
.HasColumnName("updated_at")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAddOrUpdate() // 在实体添加或更新时自动设置值
|
||||
.IsConcurrencyToken(); // 用作乐观并发控制,防止同时修改同一记录
|
||||
|
||||
// IsDeleted 属性对应的数据库列名为 "deleted",并设置默认值为 false。
|
||||
builder.Property(sd => sd.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 常用作软删除标记
|
||||
|
||||
// 4. 配置导航属性和外键关系
|
||||
|
||||
// ---
|
||||
// 配置 SubmissionDetail 到 Submission 的关系 (多对一)
|
||||
// 一个 SubmissionDetail 记录属于一个 Submission。
|
||||
builder.HasOne(sd => sd.Submission) // 当前 SubmissionDetail 有一个 Submission
|
||||
.WithMany(s => s.SubmissionDetails) // 那个 Submission 可以有多个 SubmissionDetail 记录
|
||||
.HasForeignKey(sd => sd.SubmissionId) // 外键是 SubmissionDetail.SubmissionId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 Submission 被删除时,相关的 SubmissionDetail 记录也级联删除。
|
||||
|
||||
// ---
|
||||
// 配置 SubmissionDetail 到 User (作为 Student) 的关系 (多对一)
|
||||
// 一个 SubmissionDetail 记录与一个 User (提交该详情的学生) 相关联。
|
||||
// 假设 `User` 实体中有一个名为 `SubmissionDetailsAsStudent` 的 `ICollection<SubmissionDetail>` 集合属性。
|
||||
builder.HasOne(sd => sd.User) // 当前 SubmissionDetail 有一个 User (作为学生)
|
||||
.WithMany(u => u.SubmissionDetails)
|
||||
.HasForeignKey(sd => sd.StudentId) // 外键是 SubmissionDetail.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 当 User (学生) 被删除时,如果他/她还有提交详情,则会阻止删除。
|
||||
// 这是一个更安全的选择,以防止意外数据丢失。
|
||||
|
||||
// ---
|
||||
// 配置 SubmissionDetail 到 AssignmentQuestion 的关系 (多对一)
|
||||
// 一个 SubmissionDetail 记录对应一个 AssignmentQuestion。
|
||||
builder.HasOne(sd => sd.AssignmentQuestion) // 当前 SubmissionDetail 有一个 AssignmentQuestion
|
||||
.WithMany(aq => aq.SubmissionDetails) // 那个 AssignmentQuestion 可以有多个 SubmissionDetail 记录
|
||||
.HasForeignKey(sd => sd.AssignmentQuestionId) // 外键是 SubmissionDetail.AssignmentQuestionId
|
||||
.OnDelete(DeleteBehavior.Cascade); // 当 AssignmentQuestion 被删除时,相关的 SubmissionDetail 记录也级联删除。
|
||||
}
|
||||
}
|
||||
}
|
76
TechHelper.Server/Context/Configuration/UserConfiguration.cs
Normal file
76
TechHelper.Server/Context/Configuration/UserConfiguration.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace TechHelper.Context.Configuration
|
||||
{
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
// 映射到表名:如果 User 类上没有 [Table("users")],默认是 "AspNetUsers"。
|
||||
// 显式指定可以确保你的数据库表名和你期望的一致。
|
||||
builder.ToTable("AspNetUsers");
|
||||
|
||||
// IdentityUser 的 Id 属性和其他标准属性(如 UserName, Email 等)
|
||||
// 大多由 IdentityDbContext 自动处理,通常不需要在这里显式配置主键或默认列。
|
||||
|
||||
// 配置自定义属性
|
||||
builder.Property(u => u.RefreshToken)
|
||||
.HasColumnName("refresh_token");
|
||||
|
||||
builder.Property(u => u.RefreshTokenExpiryTime)
|
||||
.HasColumnName("refresh_token_expiry_time");
|
||||
|
||||
builder.Property(u => u.IsDeleted)
|
||||
.HasColumnName("deleted")
|
||||
.HasDefaultValue(false); // 软删除标记,默认 false
|
||||
|
||||
// 配置导航属性 (User 作为关系的“一”或“主”方)
|
||||
|
||||
// User 作为老师,与 ClassTeacher 的关系 (一对多)
|
||||
builder.HasMany(u => u.TaughtClassesLink) // 一个 User (老师) 可以教授多个班级
|
||||
.WithOne(ct => ct.Teacher) // 一个 ClassTeacher 记录对应一个 Teacher
|
||||
.HasForeignKey(ct => ct.TeacherId) // 外键在 ClassTeacher.TeacherId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果老师有任教记录,则不允许删除
|
||||
|
||||
// User 作为学生,与 ClassStudent 的关系 (一对多)
|
||||
builder.HasMany(u => u.EnrolledClassesLink) // 一个 User (学生) 可以注册多个班级
|
||||
.WithOne(cs => cs.Student) // 一个 ClassStudent 记录对应一个 Student
|
||||
.HasForeignKey(cs => cs.StudentId) // 外键在 ClassStudent.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果学生有注册记录,则不允许删除
|
||||
|
||||
// User 作为创建者,与 Question 的关系 (一对多)
|
||||
builder.HasMany(u => u.CreatedQuestions) // 一个 User 可以创建多个题目
|
||||
.WithOne(q => q.Creator) // 一个 Question 对应一个 Creator
|
||||
.HasForeignKey(q => q.CreatedBy) // 外键在 Question.CreatedBy
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果创建者有题目,则不允许删除
|
||||
|
||||
// User 作为创建者,与 Assignment 的关系 (一对多)
|
||||
builder.HasMany(u => u.CreatedAssignments) // 一个 User 可以创建多个作业
|
||||
.WithOne(a => a.Creator) // 一个 Assignment 对应一个 Creator
|
||||
.HasForeignKey(a => a.CreatedBy) // 外键在 Assignment.CreatedBy
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果创建者有作业,则不允许删除
|
||||
|
||||
// User 作为学生,与 SubmissionDetail 的关系 (一对多)
|
||||
// 尽管 SubmissionDetail 也可以通过 Submission 间接关联到 User,
|
||||
// 但这里提供了直接访问的导航属性,以方便直接查询学生的所有作答详情。
|
||||
builder.HasMany(u => u.SubmissionDetails) // 一个 User (学生) 可以有多个提交详情记录
|
||||
.WithOne(sd => sd.User) // 一个 SubmissionDetail 对应一个 User (学生)
|
||||
.HasForeignKey(sd => sd.StudentId) // 外键在 SubmissionDetail.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果学生有提交详情,则不允许删除
|
||||
|
||||
// User 作为学生,与 Submission 的关系 (一对多)
|
||||
builder.HasMany(u => u.SubmissionsAsStudent) // 一个 User (学生) 可以有多个提交记录
|
||||
.WithOne(s => s.Student) // 一个 Submission 对应一个 Student
|
||||
.HasForeignKey(s => s.StudentId) // 外键在 Submission.StudentId
|
||||
.OnDelete(DeleteBehavior.Restrict); // 限制删除:如果学生有提交记录,则不允许删除
|
||||
|
||||
// User 作为批改者,与 Submission 的关系 (一对多)
|
||||
builder.HasMany(u => u.GradedSubmissions) // 一个 User (批改者) 可以批改多个提交
|
||||
.WithOne(s => s.Grader) // 一个 Submission 对应一个 Grader
|
||||
.HasForeignKey(s => s.GradedBy) // 外键在 Submission.GradedBy
|
||||
.OnDelete(DeleteBehavior.SetNull); // 因为 GradedBy 是可空的,所以批改者删除时,设为 NULL
|
||||
}
|
||||
}
|
||||
}
|
287
TechHelper.Server/Controllers/AccountController.cs
Normal file
287
TechHelper.Server/Controllers/AccountController.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using TechHelper.Context;
|
||||
using TechHelper.Services;
|
||||
using Entities.DTO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TechHelper.Features;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Entities.Contracts;
|
||||
|
||||
namespace TechHelper.Controllers
|
||||
{
|
||||
[Route("api/account")]
|
||||
[ApiController]
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IUserRegistrationService _userRegistrationService;
|
||||
private IAuthenticationService _authenticationService;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public AccountController(UserManager<User> userManager,
|
||||
IUserRegistrationService userRegistrationService,
|
||||
IEmailSender emailSender,
|
||||
IAuthenticationService authenticationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
this._userRegistrationService = userRegistrationService;
|
||||
_emailSender = emailSender;
|
||||
_authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> RegisterUsesr(
|
||||
[FromBody] UserForRegistrationDto userForRegistrationDto)
|
||||
{
|
||||
#region V1
|
||||
//if (userForRegistrationDto == null || !ModelState.IsValid)
|
||||
// return BadRequest();
|
||||
|
||||
//var user = new User
|
||||
//{
|
||||
// UserName = userForRegistrationDto.Name,
|
||||
// Email = userForRegistrationDto.Email,
|
||||
// PhoneNumber = userForRegistrationDto.PhoneNumber,
|
||||
// Address = userForRegistrationDto.HomeAddress
|
||||
//};
|
||||
|
||||
//var result = await _userManager.CreateAsync(user, userForRegistrationDto.Password);
|
||||
//if (!result.Succeeded)
|
||||
//{
|
||||
// var errors = result.Errors.Select(e => e.Description);
|
||||
// return BadRequest(new ResponseDto { Errors = errors });
|
||||
//}
|
||||
|
||||
//await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
//var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
|
||||
//var param = new Dictionary<string, string>
|
||||
//{
|
||||
// {"token", token},
|
||||
// {"email", userForRegistrationDto.Email}
|
||||
//};
|
||||
|
||||
//var callback = QueryHelpers.AddQueryString(userForRegistrationDto.ClientURI, param);
|
||||
//try
|
||||
//{
|
||||
// await _emailSender.SendEmailAsync(user.Email.ToString(), "Email Confirmation token", callback);
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
//}
|
||||
|
||||
//await _userManager.AddToRoleAsync(user, userForRegistrationDto.Roles.ToString());
|
||||
|
||||
//return StatusCode(201);
|
||||
#endregion
|
||||
#region V2
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiResponse(false, ModelState));
|
||||
|
||||
var response = await _userRegistrationService.RegisterNewUserAsync(userForRegistrationDto);
|
||||
|
||||
if (response.Status)
|
||||
{
|
||||
return StatusCode(201, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(response);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Logion(
|
||||
[FromBody] UserForAuthenticationDto userForAuthentication)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(userForAuthentication.Email);
|
||||
|
||||
if (user == null )
|
||||
{
|
||||
return Unauthorized(new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid Request"
|
||||
});
|
||||
}
|
||||
|
||||
//if(!await _userManager.IsEmailConfirmedAsync(user))
|
||||
// return Unauthorized(new AuthResponseDto
|
||||
// {
|
||||
// ErrorMessage = "Email is not confirmed"
|
||||
// });
|
||||
|
||||
if(await _userManager.IsLockedOutAsync(user))
|
||||
return Unauthorized(new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "This account is locked out"
|
||||
});
|
||||
|
||||
if (!await _userManager.CheckPasswordAsync(user,
|
||||
userForAuthentication.Password))
|
||||
{
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
|
||||
if(await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
var content = $"Your account is locked out."
|
||||
+ $"if you want to reset the password, you can use Forgot password link On the login page";
|
||||
|
||||
await _emailSender.SendEmailAsync(user.Email, "Locked out account information" ,content );
|
||||
}
|
||||
|
||||
return Unauthorized(new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid Authentication"
|
||||
});
|
||||
}
|
||||
|
||||
if(await _userManager.GetTwoFactorEnabledAsync(user))
|
||||
return await GenerateOTPFor2StepVerification(user);
|
||||
|
||||
var token = await _authenticationService.GetToken(user);
|
||||
|
||||
user.RefreshToken = _authenticationService.GenerateRefreshToken();
|
||||
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
|
||||
return Ok(new AuthResponseDto
|
||||
{
|
||||
IsAuthSuccessful = true,
|
||||
Token = token,
|
||||
RefreshToken = user.RefreshToken
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GenerateOTPFor2StepVerification(User user)
|
||||
{
|
||||
var providers = await _userManager.GetValidTwoFactorProvidersAsync(user);
|
||||
if(!providers.Contains("Email"))
|
||||
{
|
||||
return Unauthorized(new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid 2-Step verification Provider"
|
||||
});
|
||||
}
|
||||
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user, "Email");
|
||||
|
||||
await _emailSender.SendEmailAsync(user.Email, "Authentication token", token);
|
||||
|
||||
return Ok(new AuthResponseDto
|
||||
{
|
||||
Is2StepVerificationRequired = true,
|
||||
Provider = "Email"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("forgotPassword")]
|
||||
public async Task<IActionResult> ForgotPassword(
|
||||
[FromBody] ForgotPasswordDto forgotPasswordDto)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(forgotPasswordDto.Email);
|
||||
if (user == null)
|
||||
return BadRequest("Invalid Request");
|
||||
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
|
||||
var param = new Dictionary<string, string>
|
||||
{
|
||||
{"token", token},
|
||||
{"email", forgotPasswordDto.Email}
|
||||
};
|
||||
|
||||
var callback = QueryHelpers.AddQueryString(forgotPasswordDto.ClientURI, param);
|
||||
await _emailSender.SendEmailAsync( user.Email.ToString() , "Reset Password Token", callback);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("resetPassword")]
|
||||
public async Task<IActionResult> ResetPassword(
|
||||
[FromBody] ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
var errorResponse = new ResetPasswordResponseDto
|
||||
{
|
||||
Errors = new string[] { "Reset Password Failed" }
|
||||
};
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(errorResponse);
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(resetPasswordDto.Email);
|
||||
if (user == null) return BadRequest(errorResponse);
|
||||
|
||||
var resetPassResult = await _userManager.ResetPasswordAsync(user,
|
||||
resetPasswordDto.Token, resetPasswordDto.Password);
|
||||
|
||||
if (!resetPassResult.Succeeded)
|
||||
{
|
||||
var errors = resetPassResult.Errors.Select(e => e.Description);
|
||||
return BadRequest(new ResetPasswordResponseDto { Errors = errors });
|
||||
}
|
||||
|
||||
await _userManager.SetLockoutEndDateAsync(user, null);
|
||||
|
||||
return Ok(new ResetPasswordResponseDto { IsResetPasswordSuccessful = true});
|
||||
}
|
||||
|
||||
[HttpGet("emailconfirmation")]
|
||||
public async Task<IActionResult> EmailConfirmaation([FromQuery] string email,
|
||||
[FromQuery] string token)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(email);
|
||||
if (user == null) return BadRequest();
|
||||
|
||||
var confimResult = await _userManager.ConfirmEmailAsync(user, token);
|
||||
if (!confimResult.Succeeded) return BadRequest();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("TwoStepVerification")]
|
||||
public async Task<IActionResult> TwoStepVerification(
|
||||
[FromBody] TwoFactorVerificationDto twoFactorVerificationDto)
|
||||
{
|
||||
if(!ModelState.IsValid) return BadRequest(
|
||||
new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid Request"
|
||||
});
|
||||
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(twoFactorVerificationDto.Email);
|
||||
if(user == null) return BadRequest(
|
||||
new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid Request"
|
||||
});
|
||||
|
||||
var validVerification = await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorVerificationDto.Provider, twoFactorVerificationDto.TwoFactorToken);
|
||||
if(!validVerification) return BadRequest(
|
||||
new AuthResponseDto
|
||||
{
|
||||
ErrorMessage = "Invalid Request"
|
||||
});
|
||||
|
||||
var token = await _authenticationService.GetToken(user);
|
||||
user.RefreshToken = _authenticationService.GenerateRefreshToken();
|
||||
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7);
|
||||
await _userManager.UpdateAsync(user);
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
|
||||
return Ok(new AuthResponseDto
|
||||
{
|
||||
IsAuthSuccessful = true,
|
||||
Token = token,
|
||||
RefreshToken = user.RefreshToken
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
43
TechHelper.Server/Controllers/ClassController.cs
Normal file
43
TechHelper.Server/Controllers/ClassController.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Entities.DTO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using TechHelper.Services;
|
||||
|
||||
namespace TechHelper.Server.Controllers
|
||||
{
|
||||
[Route("api/class")]
|
||||
[ApiController]
|
||||
public class ClassController : ControllerBase
|
||||
{
|
||||
private IClassService _classService;
|
||||
|
||||
public ClassController(IClassService classService)
|
||||
{
|
||||
_classService = classService;
|
||||
}
|
||||
|
||||
[HttpPost("userRegiste")]
|
||||
public async Task<IActionResult> UserRegisterToClass(
|
||||
[FromBody] UserRegistrationToClassDto toClass)
|
||||
{
|
||||
|
||||
var result = await _classService.UserRegister(toClass);
|
||||
|
||||
if(!result.Status) return BadRequest(result.Message);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("Create")]
|
||||
public async Task<IActionResult> Create(
|
||||
[FromBody] ClassDto classDto)
|
||||
{
|
||||
var result = await _classService.AddAsync(classDto);
|
||||
if (!result.Status) return BadRequest(result.Message);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
65
TechHelper.Server/Controllers/TokenController.cs
Normal file
65
TechHelper.Server/Controllers/TokenController.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using TechHelper.Context;
|
||||
using TechHelper.Services;
|
||||
using Entities.DTO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Entities.Contracts;
|
||||
|
||||
namespace TechHelper.Controllers
|
||||
{
|
||||
[Route("api/token")]
|
||||
[ApiController]
|
||||
public class TokenController : ControllerBase
|
||||
{
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IAuthenticationService _authenticationService;
|
||||
|
||||
public TokenController(UserManager<User> userManager, IAuthenticationService authenticationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public async Task<IActionResult> Refresh(
|
||||
[FromBody] RefreshTokenDto tokenDto)
|
||||
{
|
||||
if (tokenDto == null)
|
||||
{
|
||||
return BadRequest( new AuthResponseDto
|
||||
{
|
||||
IsAuthSuccessful = false,
|
||||
ErrorMessage = "Invalid client reques"
|
||||
} );
|
||||
}
|
||||
|
||||
var principal = _authenticationService.GetPrincipalFromExpiredToken(tokenDto.Token);
|
||||
|
||||
var userName = principal.Identity.Name;
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(userName);
|
||||
if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime <= DateTime.Now)
|
||||
{
|
||||
return BadRequest(new AuthResponseDto
|
||||
{
|
||||
IsAuthSuccessful = false,
|
||||
ErrorMessage = " Invalid client reques "
|
||||
});
|
||||
}
|
||||
|
||||
var token = await _authenticationService.GetToken(user);
|
||||
user.RefreshToken = _authenticationService.GenerateRefreshToken();
|
||||
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
return Ok(new AuthResponseDto
|
||||
{
|
||||
Token = token,
|
||||
RefreshToken = user.RefreshToken,
|
||||
IsAuthSuccessful = true
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
33
TechHelper.Server/Controllers/WeatherForecastController.cs
Normal file
33
TechHelper.Server/Controllers/WeatherForecastController.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TechHelper.Server.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet(Name = "GetWeatherForecast")]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
30
TechHelper.Server/Dockerfile
Normal file
30
TechHelper.Server/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# 请参阅 https://aka.ms/customizecontainer 以了解如何自定义调试容器,以及 Visual Studio 如何使用此 Dockerfile 生成映像以更快地进行调试。
|
||||
|
||||
# 此阶段用于在快速模式(默认为调试配置)下从 VS 运行时
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# 此阶段用于生成服务项目
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["TechHelper.Server/TechHelper.Server.csproj", "TechHelper.Server/"]
|
||||
RUN dotnet restore "./TechHelper.Server/TechHelper.Server.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/TechHelper.Server"
|
||||
RUN dotnet build "./TechHelper.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# 此阶段用于发布要复制到最终阶段的服务项目
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./TechHelper.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# 此阶段在生产中使用,或在常规模式下从 VS 运行时使用(在不使用调试配置时为默认值)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "TechHelper.Server.dll"]
|
1075
TechHelper.Server/Migrations/20250520094348_init.Designer.cs
generated
Normal file
1075
TechHelper.Server/Migrations/20250520094348_init.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
745
TechHelper.Server/Migrations/20250520094348_init.cs
Normal file
745
TechHelper.Server/Migrations/20250520094348_init.cs
Normal file
@@ -0,0 +1,745 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace TechHelper.Server.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
Name = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
NormalizedName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ConcurrencyStamp = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
RefreshToken = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
RefreshTokenExpiryTime = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
Address = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DisplayName = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
UserName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
NormalizedUserName = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Email = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
NormalizedEmail = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EmailConfirmed = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SecurityStamp = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ConcurrencyStamp = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PhoneNumber = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
RoleId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
ClaimType = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ClaimValue = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
ClaimType = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ClaimValue = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ProviderKey = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ProviderDisplayName = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
RoleId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
LoginProvider = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Name = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Value = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignments",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
title = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
description = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
due_date = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
total_points = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
||||
created_by = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
updated_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignments", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_AspNetUsers_created_by",
|
||||
column: x => x.created_by,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "classes",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
grade = table.Column<byte>(type: "tinyint unsigned", nullable: false),
|
||||
@class = table.Column<byte>(name: "class", type: "tinyint unsigned", nullable: false),
|
||||
description = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
head_teacher_id = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
updated_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.ComputedColumn),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_classes", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_classes_AspNetUsers_head_teacher_id",
|
||||
column: x => x.head_teacher_id,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "questions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
question_text = table.Column<string>(type: "longtext", maxLength: 65535, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
question_type = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
correct_answer = table.Column<string>(type: "longtext", maxLength: 65535, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
difficulty_level = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
subject_area = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
created_by = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
updated_at = table.Column<DateTime>(type: "datetime(6)", rowVersion: true, nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_questions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_questions_AspNetUsers_created_by",
|
||||
column: x => x.created_by,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_attachments",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
assignment_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
file_path = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
file_name = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
uploaded_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_attachments", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_attachments_assignments_assignment_id",
|
||||
column: x => x.assignment_id,
|
||||
principalTable: "assignments",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_group",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
assignment = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
title = table.Column<string>(type: "longtext", maxLength: 65535, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
descript = table.Column<string>(type: "longtext", maxLength: 65535, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
total_points = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
||||
number = table.Column<byte>(type: "tinyint unsigned", nullable: false),
|
||||
sub_group = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_group", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_group_assignment_group_sub_group",
|
||||
column: x => x.sub_group,
|
||||
principalTable: "assignment_group",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_group_assignments_assignment",
|
||||
column: x => x.assignment,
|
||||
principalTable: "assignments",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "submissions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
assignment_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
student_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
attempt_number = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
submission_time = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
overall_grade = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: true),
|
||||
overall_feedback = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
graded_by = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
graded_at = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false),
|
||||
status = table.Column<string>(type: "varchar(15)", maxLength: 15, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_submissions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_submissions_AspNetUsers_graded_by",
|
||||
column: x => x.graded_by,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_submissions_AspNetUsers_student_id",
|
||||
column: x => x.student_id,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_submissions_assignments_assignment_id",
|
||||
column: x => x.assignment_id,
|
||||
principalTable: "assignments",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_class",
|
||||
columns: table => new
|
||||
{
|
||||
assignment_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
class_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
assigned_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_class", x => new { x.assignment_id, x.class_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_class_assignments_assignment_id",
|
||||
column: x => x.assignment_id,
|
||||
principalTable: "assignments",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_class_classes_class_id",
|
||||
column: x => x.class_id,
|
||||
principalTable: "classes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "class_student",
|
||||
columns: table => new
|
||||
{
|
||||
class_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
student_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
enrollment_date = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_class_student", x => new { x.class_id, x.student_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_class_student_AspNetUsers_student_id",
|
||||
column: x => x.student_id,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_class_student_classes_class_id",
|
||||
column: x => x.class_id,
|
||||
principalTable: "classes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "class_teachers",
|
||||
columns: table => new
|
||||
{
|
||||
class_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
teacher_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
subject_taught = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_class_teachers", x => new { x.class_id, x.teacher_id });
|
||||
table.ForeignKey(
|
||||
name: "FK_class_teachers_AspNetUsers_teacher_id",
|
||||
column: x => x.teacher_id,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_class_teachers_classes_class_id",
|
||||
column: x => x.class_id,
|
||||
principalTable: "classes",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_questions",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
question_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
question_number = table.Column<uint>(type: "int unsigned", nullable: false),
|
||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
detail_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_questions", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_questions_assignment_group_detail_id",
|
||||
column: x => x.detail_id,
|
||||
principalTable: "assignment_group",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignment_questions_questions_question_id",
|
||||
column: x => x.question_id,
|
||||
principalTable: "questions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "submission_details",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
submission_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
student_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
assignment_question_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
student_answer = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
is_correct = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||
points_awarded = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: true),
|
||||
teacher_feedback = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
updated_at = table.Column<DateTime>(type: "datetime(6)", rowVersion: true, nullable: false),
|
||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_submission_details", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_submission_details_AspNetUsers_student_id",
|
||||
column: x => x.student_id,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_submission_details_assignment_questions_assignment_question_~",
|
||||
column: x => x.assignment_question_id,
|
||||
principalTable: "assignment_questions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_submission_details_submissions_submission_id",
|
||||
column: x => x.submission_id,
|
||||
principalTable: "submissions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "AspNetRoles",
|
||||
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ new Guid("ab2f54ba-5885-423b-b854-93bc63f8e93e"), null, "Student", "STUDENT" },
|
||||
{ new Guid("d54985cb-1616-48fd-8687-00d9c38c900d"), null, "Teacher", "TEACHER" },
|
||||
{ new Guid("e6af92bf-1745-458f-b5c6-b51458261aaf"), null, "Administrator", "ADMINISTRATOR" }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_attachments_assignment_id",
|
||||
table: "assignment_attachments",
|
||||
column: "assignment_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_class_class_id",
|
||||
table: "assignment_class",
|
||||
column: "class_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_group_assignment",
|
||||
table: "assignment_group",
|
||||
column: "assignment");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_group_sub_group",
|
||||
table: "assignment_group",
|
||||
column: "sub_group");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_questions_detail_id",
|
||||
table: "assignment_questions",
|
||||
column: "detail_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignment_questions_question_id",
|
||||
table: "assignment_questions",
|
||||
column: "question_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_created_by",
|
||||
table: "assignments",
|
||||
column: "created_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_UserId",
|
||||
table: "assignments",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_class_student_student_id",
|
||||
table: "class_student",
|
||||
column: "student_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_class_teachers_teacher_id",
|
||||
table: "class_teachers",
|
||||
column: "teacher_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_classes_head_teacher_id",
|
||||
table: "classes",
|
||||
column: "head_teacher_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_questions_created_by",
|
||||
table: "questions",
|
||||
column: "created_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submission_details_assignment_question_id",
|
||||
table: "submission_details",
|
||||
column: "assignment_question_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submission_details_student_id",
|
||||
table: "submission_details",
|
||||
column: "student_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submission_details_submission_id",
|
||||
table: "submission_details",
|
||||
column: "submission_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submissions_assignment_id",
|
||||
table: "submissions",
|
||||
column: "assignment_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submissions_graded_by",
|
||||
table: "submissions",
|
||||
column: "graded_by");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_submissions_student_id",
|
||||
table: "submissions",
|
||||
column: "student_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_attachments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_class");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "class_student");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "class_teachers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "submission_details");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "classes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_questions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "submissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_group");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "questions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
1072
TechHelper.Server/Migrations/ApplicationContextModelSnapshot.cs
Normal file
1072
TechHelper.Server/Migrations/ApplicationContextModelSnapshot.cs
Normal file
File diff suppressed because it is too large
Load Diff
118
TechHelper.Server/Program.cs
Normal file
118
TechHelper.Server/Program.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TechHelper.Context;
|
||||
using TechHelper.Repository;
|
||||
using SharedDATA.Api;
|
||||
using Entities.Configuration;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using TechHelper.Features;
|
||||
using TechHelper.Services;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers(); // <20><><EFBFBD><EFBFBD> MVC <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> API)
|
||||
|
||||
// 2. <20><><EFBFBD>ݿ<EFBFBD><DDBF><EFBFBD><EFBFBD><EFBFBD> (DbContext)
|
||||
builder.Services.AddDbContext<ApplicationContext>(options =>
|
||||
options.UseMySql(
|
||||
builder.Configuration.GetConnectionString("XSDB"),
|
||||
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("XSDB"))
|
||||
)
|
||||
).AddUnitOfWork<ApplicationContext>()
|
||||
.AddCustomRepository<Assignment, AssignmentRepository>()
|
||||
.AddCustomRepository<AssignmentAttachment, AssignmentAttachmentRepository>()
|
||||
.AddCustomRepository<AssignmentGroup, AssignmentGroupRepository>()
|
||||
.AddCustomRepository<AssignmentQuestion, AssignmentQuestionRepository>()
|
||||
.AddCustomRepository<Class, ClassRepository>()
|
||||
.AddCustomRepository<ClassStudent, ClassStudentRepository>()
|
||||
.AddCustomRepository<ClassTeacher, ClassTeacherRepository>()
|
||||
.AddCustomRepository<Question, QuestionRepository>()
|
||||
.AddCustomRepository<Submission, SubmissionRepository>();
|
||||
|
||||
builder.Services.AddAutoMapper(typeof(AutoMapperProFile).Assembly);
|
||||
|
||||
// 3. <20><><EFBFBD>÷<EFBFBD><C3B7><EFBFBD> (IOptions)
|
||||
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
|
||||
builder.Services.Configure<JwtConfiguration>(builder.Configuration.GetSection("JWTSettings"));
|
||||
|
||||
|
||||
// 4. <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD> (Identity, JWT, <20>Զ<EFBFBD><D4B6><EFBFBD> Auth)
|
||||
// <20><><EFBFBD><EFBFBD> ASP.NET Core Identity (<28><><EFBFBD><EFBFBD>Ĭ<EFBFBD>ϵ<EFBFBD> Cookie <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD>)
|
||||
builder.Services.AddIdentity<User, IdentityRole<Guid>>(opt =>
|
||||
{
|
||||
opt.User.AllowedUserNameCharacters = "";
|
||||
opt.Lockout.AllowedForNewUsers = true;
|
||||
opt.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(2);
|
||||
opt.Lockout.MaxFailedAccessAttempts = 3;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
builder.Services.Configure<DataProtectionTokenProviderOptions>(Options =>
|
||||
{
|
||||
Options.TokenLifespan = TimeSpan.FromHours(2);
|
||||
});
|
||||
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> JWT Bearer <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>
|
||||
var jwtSettings = builder.Configuration.GetSection("JWTSettings");
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // <20><><EFBFBD><EFBFBD>Ĭ<EFBFBD><C4AC><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>Ϊ JWT Bearer
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; // <20><><EFBFBD><EFBFBD>Ĭ<EFBFBD><C4AC><EFBFBD><EFBFBD>ս<EFBFBD><D5BD><EFBFBD><EFBFBD>Ϊ JWT Bearer
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true, // <20><>֤ǩ<D6A4><C7A9><EFBFBD><EFBFBD>
|
||||
ValidateAudience = true, // <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>
|
||||
ValidateLifetime = true, // <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><D0A7>
|
||||
ValidateIssuerSigningKey = true, // <20><>֤ǩ<D6A4><C7A9><EFBFBD><EFBFBD>Կ
|
||||
|
||||
ValidIssuer = jwtSettings["validIssuer"], // <20>Ϸ<EFBFBD><CFB7><EFBFBD>ǩ<EFBFBD><C7A9><EFBFBD><EFBFBD>
|
||||
ValidAudience = jwtSettings["validAudience"], // <20>Ϸ<EFBFBD><CFB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["securityKey"])) // ǩ<><C7A9><EFBFBD><EFBFBD>Կ
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
|
||||
builder.Services.AddScoped<IEmailSender, QEmailSender>();
|
||||
builder.Services.AddTransient<IUserRegistrationService, UserRegistrationService>();
|
||||
builder.Services.AddScoped<IClassService, ClassService>();
|
||||
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowSpecificOrigin",
|
||||
builder => builder
|
||||
.WithOrigins("https://localhost:7047", "http://localhost:5190")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials());
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors("AllowSpecificOrigin");
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
52
TechHelper.Server/Properties/launchSettings.json
Normal file
52
TechHelper.Server/Properties/launchSettings.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5099"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7037;http://localhost:5062"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:5708",
|
||||
"sslPort": 44368
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class AssignmentAttachmentRepository : Repository<AssignmentAttachment>, IRepository<AssignmentAttachment>
|
||||
{
|
||||
public AssignmentAttachmentRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/AssignmentClassRepository.cs
Normal file
14
TechHelper.Server/Repository/AssignmentClassRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class AssignmentClassRepository : Repository<AssignmentClass>, IRepository<AssignmentClass>
|
||||
{
|
||||
public AssignmentClassRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/AssignmentGroupRepository.cs
Normal file
14
TechHelper.Server/Repository/AssignmentGroupRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class AssignmentGroupRepository : Repository<AssignmentGroup>, IRepository<AssignmentGroup>
|
||||
{
|
||||
public AssignmentGroupRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/AssignmentQuestionRepository.cs
Normal file
14
TechHelper.Server/Repository/AssignmentQuestionRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class AssignmentQuestionRepository : Repository<AssignmentQuestion>, IRepository<AssignmentQuestion>
|
||||
{
|
||||
public AssignmentQuestionRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/AssignmentRepository.cs
Normal file
14
TechHelper.Server/Repository/AssignmentRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class AssignmentRepository : Repository<Assignment>, IRepository<Assignment>
|
||||
{
|
||||
public AssignmentRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
15
TechHelper.Server/Repository/ClassRepository.cs
Normal file
15
TechHelper.Server/Repository/ClassRepository.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Entities.Contracts;
|
||||
using Entities.DTO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class ClassRepository : Repository<Class>, IRepository<Class>
|
||||
{
|
||||
public ClassRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/ClassStudentRepository.cs
Normal file
14
TechHelper.Server/Repository/ClassStudentRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class ClassStudentRepository : Repository<ClassStudent>, IRepository<ClassStudent>
|
||||
{
|
||||
public ClassStudentRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/ClassTeacherRepository.cs
Normal file
14
TechHelper.Server/Repository/ClassTeacherRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class ClassTeacherRepository : Repository<ClassTeacher>, IRepository<ClassTeacher>
|
||||
{
|
||||
public ClassTeacherRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/QuestionRepository.cs
Normal file
14
TechHelper.Server/Repository/QuestionRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class QuestionRepository : Repository<Question>, IRepository<Question>
|
||||
{
|
||||
public QuestionRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/SubmissionDetailRepository.cs
Normal file
14
TechHelper.Server/Repository/SubmissionDetailRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class SubmissionDetailRepository : Repository<SubmissionDetail>, IRepository<SubmissionDetail>
|
||||
{
|
||||
public SubmissionDetailRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
14
TechHelper.Server/Repository/SubmissionRepository.cs
Normal file
14
TechHelper.Server/Repository/SubmissionRepository.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.Contracts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedDATA.Api;
|
||||
using TechHelper.Context;
|
||||
|
||||
namespace TechHelper.Repository
|
||||
{
|
||||
public class SubmissionRepository : Repository<Submission>, IRepository<Submission>
|
||||
{
|
||||
public SubmissionRepository(ApplicationContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
TechHelper.Server/Services/ApiResponse.cs
Normal file
23
TechHelper.Server/Services/ApiResponse.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public class ApiResponse
|
||||
{
|
||||
public ApiResponse(string message, bool status = false)
|
||||
{
|
||||
this.Message = message;
|
||||
this.Status = status;
|
||||
}
|
||||
|
||||
public ApiResponse(bool status, object result)
|
||||
{
|
||||
this.Status = status;
|
||||
this.Result = result;
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public bool Status { get; set; }
|
||||
|
||||
public object Result { get; set; }
|
||||
}
|
||||
}
|
125
TechHelper.Server/Services/AuthenticationService.cs
Normal file
125
TechHelper.Server/Services/AuthenticationService.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using TechHelper.Context;
|
||||
using Entities.Configuration;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Entities.Contracts;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public class AuthenticationService : IAuthenticationService
|
||||
{
|
||||
private readonly JwtConfiguration _jwtSettings;
|
||||
private readonly JwtSecurityTokenHandler _jwtHandler;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IClassService _classService;
|
||||
public AuthenticationService(IOptions<JwtConfiguration> jwtSettings, UserManager<User> userManager, IClassService classService)
|
||||
{
|
||||
_jwtSettings = jwtSettings.Value;
|
||||
_jwtHandler = new JwtSecurityTokenHandler();
|
||||
_userManager = userManager;
|
||||
_classService = classService;
|
||||
}
|
||||
|
||||
public async Task<string> GetToken(User user)
|
||||
{
|
||||
var signingCredentials = GetSigningCredentials();
|
||||
|
||||
var claims = await GetClaims(user);
|
||||
|
||||
var tokenOptions = GenerateTokenOptions(signingCredentials, claims);
|
||||
|
||||
return _jwtHandler.WriteToken(tokenOptions);
|
||||
}
|
||||
|
||||
private SigningCredentials GetSigningCredentials()
|
||||
{
|
||||
var key = Encoding.UTF8.GetBytes(_jwtSettings.SecurityKey);
|
||||
var secret = new SymmetricSecurityKey(key);
|
||||
|
||||
return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Claim>> GetClaims(User user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.Name, user.Email)
|
||||
};
|
||||
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
foreach (var role in roles)
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||
}
|
||||
|
||||
var classInfo = await _classService.GetUserClass(user.Id);
|
||||
if (classInfo.Status)
|
||||
{
|
||||
(byte, byte) info = ((byte, byte))classInfo.Result;
|
||||
claims.Add(new Claim("Grade", info.Item1.ToString()));
|
||||
claims.Add(new Claim("Class", info.Item2.ToString()));
|
||||
}
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, IEnumerable<Claim> claims)
|
||||
{
|
||||
var tokenOptions = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.ValidIssuer,
|
||||
audience: _jwtSettings.ValidAudience,
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(Convert.ToDouble(
|
||||
_jwtSettings.ExpiryInMinutes)),
|
||||
signingCredentials: signingCredentials);
|
||||
|
||||
return tokenOptions;
|
||||
}
|
||||
|
||||
public string GenerateRefreshToken()
|
||||
{
|
||||
var randomNumber = new byte[32];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
|
||||
{
|
||||
var tokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = true,
|
||||
ValidateIssuer = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(_jwtSettings.SecurityKey)),
|
||||
ValidateLifetime = false,
|
||||
ValidIssuer = _jwtSettings.ValidIssuer,
|
||||
ValidAudience = _jwtSettings.ValidAudience,
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
SecurityToken securityToken;
|
||||
|
||||
var principal = tokenHandler.ValidateToken(token,
|
||||
tokenValidationParameters, out securityToken);
|
||||
|
||||
var jwtSecurityToken = securityToken as JwtSecurityToken;
|
||||
if (jwtSecurityToken == null ||
|
||||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new SecurityTokenException("Invalid token");
|
||||
}
|
||||
|
||||
return principal;
|
||||
}
|
||||
}
|
||||
}
|
218
TechHelper.Server/Services/ClassService.cs
Normal file
218
TechHelper.Server/Services/ClassService.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using AutoMapper;
|
||||
using Entities.Contracts;
|
||||
using Entities.DTO;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using SharedDATA.Api;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public class ClassService : IClassService
|
||||
{
|
||||
private readonly IUnitOfWork _work;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<User> _userManager;
|
||||
|
||||
public ClassService(IUnitOfWork work, IMapper mapper, UserManager<User> userManager)
|
||||
{
|
||||
_work = work;
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> AddAsync(ClassDto model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var @class = _mapper.Map<Class>(model);
|
||||
await _work.GetRepository<Class>().InsertAsync(@class);
|
||||
|
||||
if (await _work.SaveChangesAsync() > 0)
|
||||
{
|
||||
return new ApiResponse(true, _mapper.Map<ClassDto>(@class));
|
||||
}
|
||||
return new ApiResponse("添加班级失败。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ApiResponse($"添加班级时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 IBaseService<ClassDto, int>.DeleteAsync
|
||||
public async Task<ApiResponse> DeleteAsync(Guid id) // ID 类型现在是 int
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingClass = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(
|
||||
predicate: c => c.Id == id);
|
||||
|
||||
if (existingClass == null)
|
||||
{
|
||||
return new ApiResponse("班级未找到。");
|
||||
}
|
||||
|
||||
_work.GetRepository<Class>().Delete(existingClass);
|
||||
|
||||
if (await _work.SaveChangesAsync() > 0)
|
||||
{
|
||||
return new ApiResponse(true, "班级删除成功。");
|
||||
}
|
||||
return new ApiResponse("删除班级失败。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ApiResponse($"删除班级时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 IBaseService<ClassDto, int>.GetAllAsync
|
||||
public async Task<ApiResponse> GetAllAsync(QueryParameter query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var repository = _work.GetRepository<Class>();
|
||||
|
||||
// 构建查询条件 (可根据 QueryParameter.Search 进行筛选)
|
||||
Func<IQueryable<Class>, IOrderedQueryable<Class>> orderBy = null; // 默认不排序
|
||||
if (query.Search != null && !string.IsNullOrWhiteSpace(query.Search))
|
||||
{
|
||||
// 在 Name 字段中进行模糊搜索
|
||||
var classes = await repository.GetPagedListAsync(
|
||||
orderBy: orderBy,
|
||||
pageSize: query.PageSize,
|
||||
pageIndex: query.PageIndex
|
||||
);
|
||||
var classDtosFiltered = _mapper.Map<IEnumerable<ClassDto>>(classes);
|
||||
return new ApiResponse(true, classDtosFiltered);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有搜索条件,获取所有(带分页)
|
||||
var classes = await repository.GetPagedListAsync(
|
||||
orderBy: orderBy,
|
||||
pageSize: query.PageSize,
|
||||
pageIndex: query.PageIndex
|
||||
);
|
||||
var classDtos = _mapper.Map<IEnumerable<ClassDto>>(classes);
|
||||
return new ApiResponse(true, classDtos);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ApiResponse($"获取所有班级时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 IBaseService<ClassDto, int>.GetAsync
|
||||
public async Task<ApiResponse> GetAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var @class = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(
|
||||
predicate: c => c.Id == id);
|
||||
|
||||
if (@class == null)
|
||||
{
|
||||
return new ApiResponse("班级未找到。");
|
||||
}
|
||||
|
||||
var classDto = _mapper.Map<ClassDto>(@class);
|
||||
return new ApiResponse(true, classDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ApiResponse($"获取班级时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> GetUserClass(Guid user)
|
||||
{
|
||||
var existingUserClass = await _work.GetRepository<ClassStudent>().GetFirstOrDefaultAsync(predicate: c=>c.StudentId == user);
|
||||
if(existingUserClass == null) return new ApiResponse("该学生没有班级。");
|
||||
|
||||
var classId = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(predicate: c => c.Id == existingUserClass.ClassId);
|
||||
return new ApiResponse(true, (classId.Grade, classId.Number));
|
||||
}
|
||||
|
||||
// 实现 IBaseService<ClassDto, int>.UpdateAsync
|
||||
public async Task<ApiResponse> UpdateAsync(ClassDto model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 首先通过 ID 查找现有实体
|
||||
var existingClass = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(
|
||||
predicate: c => c.Number == model.Class);
|
||||
|
||||
if (existingClass == null)
|
||||
{
|
||||
return new ApiResponse("班级未找到。");
|
||||
}
|
||||
|
||||
_mapper.Map(model, existingClass);
|
||||
_work.GetRepository<Class>().Update(existingClass);
|
||||
|
||||
if (await _work.SaveChangesAsync() > 0)
|
||||
{
|
||||
return new ApiResponse(true, _mapper.Map<ClassDto>(existingClass));
|
||||
}
|
||||
return new ApiResponse("更新班级失败。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ApiResponse($"更新班级时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> UserRegister(UserRegistrationToClassDto user)
|
||||
{
|
||||
try
|
||||
{
|
||||
var usrinfo = await _userManager.FindByEmailAsync(user.User);
|
||||
var existingClass = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(
|
||||
predicate: (c => c.Number == user.ClassId && c.Grade == user.GradeId));
|
||||
|
||||
var finduser = await _userManager.FindByEmailAsync(user.User);
|
||||
|
||||
if (existingClass == null || finduser == null || usrinfo == null)
|
||||
{
|
||||
return new ApiResponse("班级未找到。");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (user.Roles == UserRoles.Student)
|
||||
{
|
||||
var addresult = await _work.GetRepository<ClassStudent>().InsertAsync(new ClassStudent
|
||||
{
|
||||
StudentId = finduser.Id,
|
||||
ClassId = existingClass.Id
|
||||
});
|
||||
|
||||
await _userManager.AddToRoleAsync(usrinfo, UserRoles.Student.ToString());
|
||||
}
|
||||
else if (user.Roles == UserRoles.Teacher)
|
||||
{
|
||||
var classTeacher = new ClassTeacher
|
||||
{
|
||||
ClassId = existingClass.Id,
|
||||
TeacherId = existingClass.Id,
|
||||
SubjectTaught = user.SubjectArea.ToString()
|
||||
};
|
||||
await _work.GetRepository<ClassTeacher>().InsertAsync(classTeacher);
|
||||
|
||||
await _userManager.AddToRoleAsync(usrinfo, UserRoles.Teacher.ToString());
|
||||
}
|
||||
|
||||
if (await _work.SaveChangesAsync() > 0)
|
||||
{
|
||||
return new ApiResponse(true, _mapper.Map<ClassDto>(existingClass));
|
||||
}
|
||||
return new ApiResponse("班级注册失败。");
|
||||
|
||||
}
|
||||
catch (Exception ex) { return new ApiResponse($"注册进班级时发生错误: {ex.Message}"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
TechHelper.Server/Services/IAuthenticationService.cs
Normal file
14
TechHelper.Server/Services/IAuthenticationService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using TechHelper.Context;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using Entities.Contracts;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
public Task<string> GetToken(User user);
|
||||
public string GenerateRefreshToken();
|
||||
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
|
||||
}
|
||||
}
|
11
TechHelper.Server/Services/IBaseService.cs
Normal file
11
TechHelper.Server/Services/IBaseService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public interface IBaseService<T, TId>
|
||||
{
|
||||
Task<ApiResponse> GetAllAsync(QueryParameter query);
|
||||
Task<ApiResponse> GetAsync(TId id);
|
||||
Task<ApiResponse> AddAsync(T model);
|
||||
Task<ApiResponse> UpdateAsync(T model);
|
||||
Task<ApiResponse> DeleteAsync(TId id);
|
||||
}
|
||||
}
|
12
TechHelper.Server/Services/IClassService.cs
Normal file
12
TechHelper.Server/Services/IClassService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Entities.Contracts;
|
||||
using Entities.DTO;
|
||||
using System.Net;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public interface IClassService : IBaseService<ClassDto, Guid>
|
||||
{
|
||||
public Task<ApiResponse> UserRegister(UserRegistrationToClassDto user);
|
||||
public Task<ApiResponse> GetUserClass(Guid user);
|
||||
}
|
||||
}
|
14
TechHelper.Server/Services/IUserRegistrationService.cs
Normal file
14
TechHelper.Server/Services/IUserRegistrationService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Entities.DTO;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public interface IUserRegistrationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册新用户,并根据角色关联到班级。
|
||||
/// </summary>
|
||||
/// <param name="registrationDto">用户注册数据。</param>
|
||||
/// <returns>操作结果。</returns>
|
||||
Task<ApiResponse> RegisterNewUserAsync(UserForRegistrationDto registrationDto);
|
||||
}
|
||||
}
|
9
TechHelper.Server/Services/QueryParameter.cs
Normal file
9
TechHelper.Server/Services/QueryParameter.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public class QueryParameter
|
||||
{
|
||||
public int PageIndex { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public string Search { get; set; }
|
||||
}
|
||||
}
|
126
TechHelper.Server/Services/UserRegistrationService.cs
Normal file
126
TechHelper.Server/Services/UserRegistrationService.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using AutoMapper;
|
||||
using Entities.Contracts;
|
||||
using Entities.DTO;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using SharedDATA.Api;
|
||||
using System.Text;
|
||||
using TechHelper.Features;
|
||||
|
||||
namespace TechHelper.Services
|
||||
{
|
||||
public class UserRegistrationService : IUserRegistrationService
|
||||
{
|
||||
private readonly IUnitOfWork _work;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
|
||||
public UserRegistrationService(
|
||||
IUnitOfWork work,
|
||||
IMapper mapper,
|
||||
UserManager<User> userManager,
|
||||
IEmailSender emailSender)
|
||||
{
|
||||
_work = work;
|
||||
_mapper = mapper;
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> RegisterNewUserAsync(UserForRegistrationDto registrationDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingUserByEmail = await _userManager.FindByEmailAsync(registrationDto.Email);
|
||||
if (existingUserByEmail != null)
|
||||
{
|
||||
return new ApiResponse("此电子邮件地址已被注册。");
|
||||
}
|
||||
|
||||
var user = _mapper.Map<User>(registrationDto);
|
||||
|
||||
user.UserName = registrationDto.Email;
|
||||
user.DisplayName = registrationDto.Name;
|
||||
user.EmailConfirmed = false;
|
||||
|
||||
var result = await _userManager.CreateAsync(user, registrationDto.Password);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var errors = result.Errors.Select(e => e.Description).ToList();
|
||||
return new ApiResponse(false, errors);
|
||||
}
|
||||
|
||||
var existingClass = await _work.GetRepository<Class>().GetFirstOrDefaultAsync(
|
||||
predicate: c => c.Number == registrationDto.Class && c.Grade == registrationDto.Grade);
|
||||
|
||||
if (existingClass == null)
|
||||
{
|
||||
existingClass = new Class
|
||||
{
|
||||
Number = (byte)registrationDto.Class,
|
||||
Grade = (byte)registrationDto.Grade
|
||||
};
|
||||
await _work.GetRepository<Class>().InsertAsync(existingClass);
|
||||
}
|
||||
|
||||
//if (registrationDto.Roles == UserRoles.Student)
|
||||
//{
|
||||
// var classStudent = new ClassStudent
|
||||
// {
|
||||
// ClassId = existingClass.Id,
|
||||
// StudentId = user.Id
|
||||
// };
|
||||
// await _work.GetRepository<ClassStudent>().InsertAsync(classStudent);
|
||||
|
||||
// await _userManager.AddToRoleAsync(user, UserRoles.Student.ToString());
|
||||
//}
|
||||
//else if (registrationDto.Roles == UserRoles.Teacher)
|
||||
//{
|
||||
// var classTeacher = new ClassTeacher
|
||||
// {
|
||||
// ClassId = existingClass.Id,
|
||||
// TeacherId = user.Id
|
||||
// };
|
||||
// await _work.GetRepository<ClassTeacher>().InsertAsync(classTeacher);
|
||||
|
||||
// await _userManager.AddToRoleAsync(user, UserRoles.Teacher.ToString());
|
||||
//}
|
||||
|
||||
//var emailConfirmationToken = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
//var encodedToken = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(emailConfirmationToken));
|
||||
//var callbackUrl = QueryHelpers.AddQueryString(registrationDto.ClientURI, new Dictionary<string, string>
|
||||
//{
|
||||
// {"token", encodedToken},
|
||||
// {"email", user.Email}
|
||||
// });
|
||||
|
||||
//try
|
||||
//{
|
||||
// await _emailSender.SendEmailAsync(user.Email, "请确认您的邮箱", $"请点击此链接确认您的邮箱: {callbackUrl}");
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Console.Error.WriteLine($"发送邮箱确认邮件失败: {ex.Message}");
|
||||
//}
|
||||
|
||||
|
||||
if (await _work.SaveChangesAsync() > 0)
|
||||
{
|
||||
return new ApiResponse(true, "用户注册成功,班级关联和邮箱确认邮件已发送。");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ApiResponse("用户注册成功但班级关联失败。");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"注册过程中发生错误: {ex.Message}");
|
||||
return new ApiResponse($"注册过程中发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
TechHelper.Server/TechHelper.Server.csproj
Normal file
35
TechHelper.Server/TechHelper.Server.csproj
Normal file
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>cfc9a23f-c045-4d10-84d4-e096cf8502ef</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.12.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.16" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.16" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.16" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.16">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.16">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EmailLib\EmailLib.csproj" />
|
||||
<ProjectReference Include="..\Entities\Entities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
6
TechHelper.Server/TechHelper.Server.http
Normal file
6
TechHelper.Server/TechHelper.Server.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@TechHelper.Server_HostAddress = http://localhost:5062
|
||||
|
||||
GET {{TechHelper.Server_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
13
TechHelper.Server/WeatherForecast.cs
Normal file
13
TechHelper.Server/WeatherForecast.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace TechHelper.Server
|
||||
{
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
}
|
8
TechHelper.Server/appsettings.Development.json
Normal file
8
TechHelper.Server/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
27
TechHelper.Server/appsettings.json
Normal file
27
TechHelper.Server/appsettings.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"XSDB": "Server=mysql.eazygame.cn;Port=13002;Database=tech_helper;User=root;Password=wx1998WX"
|
||||
},
|
||||
"JWTSettings": {
|
||||
"securityKey": "MxcxQHVYVDQ0U3lqWkIwdjZlSGx4eFp6YnFpUGxodmc5Y3hPZk5vWm9MZEg2Y0I=",
|
||||
"validIssuer": "CodeMazeAPI",
|
||||
"validAudience": "http://localhost:5099",
|
||||
"expiryInMinutes": 5
|
||||
},
|
||||
"ApiConfiguration": {
|
||||
"BaseAddress": "http://localhost:5099"
|
||||
},
|
||||
"EmailConfiguration": {
|
||||
"From": "1468441589@qq.com",
|
||||
"Password": "pfxhtoztjimtbahc",
|
||||
"SmtpHost": "smtp.qq.com",
|
||||
"Port": 587
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
Reference in New Issue
Block a user