添加项目文件。

This commit is contained in:
SpecialX
2025-05-23 19:03:00 +08:00
parent 6fa7679fd3
commit d36fef2bbb
185 changed files with 13413 additions and 0 deletions

View 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());
}
}
}

View 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();
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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 记录也级联删除
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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。
}
}
}

View File

@@ -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` 中进行
}
}
}

View File

@@ -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"); // 如果经常按名称搜索
}
}
}

View File

@@ -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。
}
}
}

View File

@@ -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。
}
}
}

View 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` 中进行
}
}
}

View 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());
}
}
}

View File

@@ -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` 中进行
}
}
}

View File

@@ -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 记录也级联删除。
}
}
}

View 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
}
}
}

View 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
});
}
}
}

View 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();
}
}
}

View 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
});
}
}
}

View 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();
}
}
}

View 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"]

File diff suppressed because it is too large Load Diff

View 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");
}
}
}

File diff suppressed because it is too large Load Diff

View 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();

View 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
}
}
}

View File

@@ -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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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)
{
}
}
}

View 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; }
}
}

View 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;
}
}
}

View 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}"); }
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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; }
}
}

View 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}");
}
}
}
}

View 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>

View File

@@ -0,0 +1,6 @@
@TechHelper.Server_HostAddress = http://localhost:5062
GET {{TechHelper.Server_HostAddress}}/weatherforecast/
Accept: application/json
###

View 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; }
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View 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": "*"
}