using System.ComponentModel.DataAnnotations; using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Controllers; [ApiController] [Authorize] [Route("api/courses")] public sealed class CoursesController( AppDbContext db, ICurrentUserDataScope currentUserDataScope) : ControllerBase { private const string WriteRoles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin + "," + SystemRoles.CollegeAdmin; [HttpGet] public async Task>> Get( int page = 1, int pageSize = 20, string? keyword = null, Guid? collegeId = null, CourseNature? nature = null, bool? isEnabled = null, CancellationToken cancellationToken = default) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 10, 100); var source = ScopedCourses().AsNoTracking(); if (collegeId.HasValue) source = source.Where(x => x.CollegeId == collegeId.Value); if (nature.HasValue) source = source.Where(x => x.Nature == nature.Value); if (isEnabled.HasValue) source = source.Where(x => x.IsEnabled == isEnabled.Value); if (!string.IsNullOrWhiteSpace(keyword)) { keyword = keyword.Trim(); source = source.Where(x => x.Code.Contains(keyword) || x.Name.Contains(keyword) || (x.EnglishName != null && x.EnglishName.Contains(keyword))); } var total = await source.CountAsync(cancellationToken); var items = await source .OrderBy(x => x.SortOrder) .ThenBy(x => x.Code) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => new { x.Id, x.Code, x.Name, x.EnglishName, x.CollegeId, CollegeName = x.College!.Name, x.Credits, x.TotalHours, x.LectureHours, x.PracticeHours, x.Nature, x.AssessmentMethod, x.Description, x.IsEnabled, x.SortOrder, x.CreatedAt }) .ToListAsync(cancellationToken); return Ok(new PagedResult(items, total, page, pageSize)); } [HttpPost] [Authorize(Roles = WriteRoles)] public async Task Create( CourseRequest request, CancellationToken cancellationToken) { var validation = await ValidateAsync(request, cancellationToken); if (validation is not null) return validation; var entity = new Course { Code = request.Code.Trim(), Name = request.Name.Trim(), EnglishName = Normalize(request.EnglishName), CollegeId = request.CollegeId, Credits = request.Credits, TotalHours = request.TotalHours, LectureHours = request.LectureHours, PracticeHours = request.PracticeHours, Nature = request.Nature, AssessmentMethod = request.AssessmentMethod, Description = Normalize(request.Description), IsEnabled = request.IsEnabled, SortOrder = request.SortOrder }; db.Courses.Add(entity); return await SaveAsync(entity.Id, true, cancellationToken); } [HttpPut("{id:guid}")] [Authorize(Roles = WriteRoles)] public async Task Update( Guid id, CourseRequest request, CancellationToken cancellationToken) { var entity = await db.Courses.FindAsync([id], cancellationToken); if (entity is null) return NotFound(); if (!CanAccessCollege(entity.CollegeId)) return Forbid(); var validation = await ValidateAsync(request, cancellationToken); if (validation is not null) return validation; entity.Code = request.Code.Trim(); entity.Name = request.Name.Trim(); entity.EnglishName = Normalize(request.EnglishName); entity.CollegeId = request.CollegeId; entity.Credits = request.Credits; entity.TotalHours = request.TotalHours; entity.LectureHours = request.LectureHours; entity.PracticeHours = request.PracticeHours; entity.Nature = request.Nature; entity.AssessmentMethod = request.AssessmentMethod; entity.Description = Normalize(request.Description); entity.IsEnabled = request.IsEnabled; entity.SortOrder = request.SortOrder; return await SaveAsync(entity.Id, false, cancellationToken); } [HttpDelete("{id:guid}")] [Authorize(Roles = WriteRoles)] public async Task Delete(Guid id, CancellationToken cancellationToken) { var entity = await db.Courses.FindAsync([id], cancellationToken); if (entity is null) return NotFound(); if (!CanAccessCollege(entity.CollegeId)) return Forbid(); db.Courses.Remove(entity); return await SaveAsync(id, false, cancellationToken); } private async Task ValidateAsync( CourseRequest request, CancellationToken cancellationToken) { if (!CanAccessCollege(request.CollegeId)) return Forbid(); if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken)) return ValidationProblem("所选学院不存在。"); if (request.LectureHours + request.PracticeHours > request.TotalHours) return ValidationProblem("讲授学时与实践学时之和不能超过总学时。"); return null; } private IQueryable ScopedCourses() { var scope = currentUserDataScope.Current; var source = db.Courses.AsQueryable(); if (scope.Scope == DataScope.All) return source; if (scope.Scope == DataScope.College) return source.Where(x => x.CollegeId == scope.RestrictedCollegeId); if (scope.Scope == DataScope.Class) { return source.Where(course => db.TeachingTasks.Any(task => task.CourseId == course.Id && task.Classes.Any(item => item.AdministrativeClass!.CounselorUserId == scope.UserId))); } var userId = scope.UserId; var isTeacher = scope.IsInRole(SystemRoles.Teacher); var isStudent = scope.IsInRole(SystemRoles.Student); return source.Where(course => isTeacher && db.TeachingTasks.Any(task => task.CourseId == course.Id && task.Teachers.Any(item => item.Teacher!.UserId == userId)) || isStudent && db.TeachingTasks.Any(task => task.CourseId == course.Id && task.Classes.Any(item => item.AdministrativeClass!.Students.Any(student => student.UserId == userId)))); } private bool CanAccessCollege(Guid collegeId) => currentUserDataScope.Current.CanAccessCollege(collegeId); private async Task SaveAsync( Guid id, bool created, CancellationToken cancellationToken) { try { await db.SaveChangesAsync(cancellationToken); return created ? Created(string.Empty, new { id }) : NoContent(); } catch (DbUpdateException) { return Conflict(new ProblemDetails { Title = "无法完成操作", Detail = "课程编码已存在,或课程已被其他教学业务引用。", Status = StatusCodes.Status409Conflict }); } } private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } public sealed record CourseRequest( [Required, MaxLength(40)] string Code, [Required, MaxLength(100)] string Name, [MaxLength(150)] string? EnglishName, Guid CollegeId, [Range(typeof(decimal), "0.1", "99")] decimal Credits, [Range(1, 1000)] int TotalHours, [Range(0, 1000)] int LectureHours, [Range(0, 1000)] int PracticeHours, CourseNature Nature, AssessmentMethod AssessmentMethod, [MaxLength(1000)] string? Description, bool IsEnabled, int SortOrder);