Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/BaseDataController.cs
T

892 lines
33 KiB
C#

using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Common;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize]
[Route("api/base-data")]
public sealed class BaseDataController(AppDbContext db, IAppCache cache) : ControllerBase
{
private const string Administrators =
$"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";
[HttpGet("campuses")]
public async Task<ActionResult<IReadOnlyCollection<Campus>>> GetCampuses(
CancellationToken cancellationToken) =>
await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("campuses"),
token => db.Campuses.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
[HttpPost("campuses")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Campus>> CreateCampus(
CatalogRequest request,
CancellationToken cancellationToken)
{
var entity = new Campus
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
Address = request.Description?.Trim(),
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetCampuses", cancellationToken);
}
[HttpPut("campuses/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Campus>> UpdateCampus(
Guid id,
CatalogRequest request,
CancellationToken cancellationToken)
{
var entity = await db.Campuses.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
ApplyCatalog(entity, request);
entity.Address = request.Description?.Trim();
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpGet("colleges")]
public async Task<ActionResult<object>> GetColleges(CancellationToken cancellationToken)
{
var result = await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("colleges"),
token => db.Colleges.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
.Select(x => new CollegeListItem(
x.Id,
x.Code,
x.Name,
x.ShortName,
x.CampusId,
x.Campus != null ? x.Campus.Name : null,
x.IsEnabled,
x.SortOrder))
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
return Ok(result);
}
[HttpPost("colleges")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<College>> CreateCollege(
CollegeRequest request,
CancellationToken cancellationToken)
{
if (request.CampusId.HasValue &&
!await db.Campuses.AnyAsync(x => x.Id == request.CampusId, cancellationToken))
{
return ValidationProblem("所选校区不存在。");
}
var entity = new College
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
ShortName = request.ShortName?.Trim(),
CampusId = request.CampusId,
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetColleges", cancellationToken);
}
[HttpPut("colleges/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<College>> UpdateCollege(
Guid id,
CollegeRequest request,
CancellationToken cancellationToken)
{
var entity = await db.Colleges.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
ApplyCatalog(entity, request);
entity.ShortName = request.ShortName?.Trim();
entity.CampusId = request.CampusId;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpGet("majors")]
public async Task<ActionResult<object>> GetMajors(CancellationToken cancellationToken)
{
var result = await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("majors"),
token => db.Majors.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
.Select(x => new MajorListItem(
x.Id,
x.Code,
x.Name,
x.CollegeId,
x.College!.Name,
x.DegreeType,
x.SchoolingYears,
x.IsEnabled,
x.SortOrder))
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
return Ok(result);
}
[HttpGet("course-categories")]
public async Task<ActionResult<IReadOnlyCollection<CourseCategory>>> GetCourseCategories(
CancellationToken cancellationToken) =>
await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("course-categories"),
token => db.CourseCategories.AsNoTracking()
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.Code)
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
[HttpPost("course-categories")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<CourseCategory>> CreateCourseCategory(
CatalogRequest request,
CancellationToken cancellationToken)
{
var entity = new CourseCategory
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, nameof(GetCourseCategories), cancellationToken);
}
[HttpPut("course-categories/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<CourseCategory>> UpdateCourseCategory(
Guid id,
CatalogRequest request,
CancellationToken cancellationToken)
{
var entity = await db.CourseCategories.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
ApplyCatalog(entity, request);
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpPost("majors")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Major>> CreateMajor(
MajorRequest request,
CancellationToken cancellationToken)
{
if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
return ValidationProblem("所选学院不存在。");
var entity = new Major
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
CollegeId = request.CollegeId,
DegreeType = request.DegreeType.Trim(),
SchoolingYears = request.SchoolingYears,
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetMajors", cancellationToken);
}
[HttpPut("majors/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Major>> UpdateMajor(
Guid id,
MajorRequest request,
CancellationToken cancellationToken)
{
var entity = await db.Majors.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
ApplyCatalog(entity, request);
entity.CollegeId = request.CollegeId;
entity.DegreeType = request.DegreeType.Trim();
entity.SchoolingYears = request.SchoolingYears;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpGet("classes")]
public async Task<ActionResult<object>> GetClasses(
int? page,
int? pageSize,
string? keyword,
Guid? collegeId,
Guid? majorId,
int? grade,
bool? isEnabled,
CancellationToken cancellationToken)
{
if (page.HasValue || pageSize.HasValue)
{
if (page is < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var source = db.AdministrativeClasses.AsNoTracking();
if (collegeId.HasValue)
source = source.Where(x => x.Major!.CollegeId == collegeId.Value);
if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId.Value);
if (grade.HasValue) source = source.Where(x => x.Grade == grade.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.Major!.Name.Contains(keyword) ||
x.Major.College!.Name.Contains(keyword));
}
var total = await source.CountAsync(cancellationToken);
var items = await source
.OrderByDescending(x => x.Grade).ThenBy(x => x.Code)
.Skip((page!.Value - 1) * pageSize!.Value)
.Take(pageSize.Value)
.Select(x => new AdministrativeClassListItem(
x.Id, x.Code, x.Name, x.MajorId, x.Major!.Name,
x.Major.College!.Name, x.Grade, x.CounselorUserId,
x.CounselorName, x.IsEnabled, x.SortOrder))
.ToListAsync(cancellationToken);
return Ok(new PagedResult<AdministrativeClassListItem>(
items, total, page.Value, pageSize.Value));
}
var result = await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("classes"),
token => db.AdministrativeClasses.AsNoTracking()
.OrderByDescending(x => x.Grade).ThenBy(x => x.Code)
.Select(x => new AdministrativeClassListItem(
x.Id,
x.Code,
x.Name,
x.MajorId,
x.Major!.Name,
x.Major.College!.Name,
x.Grade,
x.CounselorUserId,
x.CounselorName,
x.IsEnabled,
x.SortOrder))
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
return Ok(result);
}
[HttpGet("counselors")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<object>> GetCounselors(CancellationToken cancellationToken) =>
Ok(await db.Users.AsNoTracking()
.Where(user =>
user.IsEnabled &&
db.UserRoles.Any(userRole =>
userRole.UserId == user.Id &&
db.Roles.Any(role =>
role.Id == userRole.RoleId &&
role.Name == SystemRoles.Counselor)))
.OrderBy(x => x.DisplayName)
.Select(x => new
{
x.Id,
x.DisplayName,
x.StaffNumber,
x.CollegeId
})
.ToListAsync(cancellationToken));
[HttpPost("classes")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AdministrativeClass>> CreateClass(
ClassRequest request,
CancellationToken cancellationToken)
{
if (!await db.Majors.AnyAsync(x => x.Id == request.MajorId, cancellationToken))
return ValidationProblem("所选专业不存在。");
var counselorName = await GetCounselorNameAsync(
request.CounselorUserId,
cancellationToken);
if (request.CounselorUserId.HasValue && counselorName is null)
return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");
var entity = new AdministrativeClass
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
MajorId = request.MajorId,
Grade = request.Grade,
CounselorUserId = request.CounselorUserId,
CounselorName = counselorName,
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetClasses", cancellationToken);
}
[HttpPut("classes/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AdministrativeClass>> UpdateClass(
Guid id,
ClassRequest request,
CancellationToken cancellationToken)
{
var entity = await db.AdministrativeClasses.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
var counselorName = await GetCounselorNameAsync(
request.CounselorUserId,
cancellationToken);
if (request.CounselorUserId.HasValue && counselorName is null)
return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");
ApplyCatalog(entity, request);
entity.MajorId = request.MajorId;
entity.Grade = request.Grade;
entity.CounselorUserId = request.CounselorUserId;
entity.CounselorName = counselorName;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpGet("terms")]
public async Task<ActionResult<IReadOnlyCollection<AcademicTerm>>> GetTerms(
CancellationToken cancellationToken) =>
await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("terms"),
token => db.AcademicTerms.AsNoTracking()
.OrderByDescending(x => x.IsCurrent)
.ThenBy(x => x.IsArchived)
.ThenByDescending(x => x.StartDate)
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
[HttpPost("terms")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AcademicTerm>> CreateTerm(
TermRequest request,
CancellationToken cancellationToken)
{
if (request.EndDate <= request.StartDate)
return ValidationProblem("学期结束日期必须晚于开始日期。");
if (request.IsCurrent && !request.IsEnabled)
return ValidationProblem("当前学期必须保持启用。");
var hasCurrentTerm = await db.AcademicTerms
.AnyAsync(x => x.IsCurrent, cancellationToken);
var shouldBeCurrent = request.IsCurrent || (!hasCurrentTerm && request.IsEnabled);
if (shouldBeCurrent)
{
var currentTerms = await db.AcademicTerms
.Where(x => x.IsCurrent)
.ToListAsync(cancellationToken);
foreach (var currentTerm in currentTerms)
currentTerm.IsCurrent = false;
}
var entity = new AcademicTerm
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
AcademicYear = request.AcademicYear.Trim(),
Season = request.Season,
StartDate = request.StartDate,
EndDate = request.EndDate,
IsCurrent = shouldBeCurrent,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetTerms", cancellationToken);
}
[HttpPut("terms/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AcademicTerm>> UpdateTerm(
Guid id,
TermRequest request,
CancellationToken cancellationToken)
{
var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (request.EndDate <= request.StartDate)
return ValidationProblem("学期结束日期必须晚于开始日期。");
if (entity.IsCurrent && !request.IsCurrent)
return ConflictProblem("不能直接取消当前学期,请将另一个学期设为当前。");
if (request.IsCurrent && entity.IsArchived)
return ConflictProblem("已归档学期不能直接设为当前,请先撤销归档。");
if (request.IsCurrent && !request.IsEnabled)
return ValidationProblem("当前学期必须保持启用。");
if (request.IsCurrent)
{
var currentTerms = await db.AcademicTerms
.Where(x => x.Id != id && x.IsCurrent)
.ToListAsync(cancellationToken);
foreach (var currentTerm in currentTerms)
currentTerm.IsCurrent = false;
}
ApplyCatalog(entity, request);
entity.AcademicYear = request.AcademicYear.Trim();
entity.Season = request.Season;
entity.StartDate = request.StartDate;
entity.EndDate = request.EndDate;
entity.IsCurrent = request.IsCurrent;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpPost("terms/{id:guid}/set-current")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AcademicTerm>> SetCurrentTerm(
Guid id,
CancellationToken cancellationToken)
{
var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (entity.IsArchived)
return ConflictProblem("已归档学期不能直接设为当前,请先撤销归档。");
if (!entity.IsEnabled)
return ConflictProblem("停用学期不能设为当前,请先启用。");
if (entity.IsCurrent) return entity;
var currentTerms = await db.AcademicTerms
.Where(x => x.Id != id && x.IsCurrent)
.ToListAsync(cancellationToken);
foreach (var currentTerm in currentTerms)
currentTerm.IsCurrent = false;
entity.IsCurrent = true;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpPost("terms/{id:guid}/archive")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AcademicTerm>> ArchiveTerm(
Guid id,
CancellationToken cancellationToken)
{
var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (entity.IsCurrent)
return ConflictProblem("当前学期不能归档,请先切换到新的当前学期。");
if (entity.IsArchived) return entity;
entity.IsArchived = true;
entity.ArchivedAt = DateTime.UtcNow;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpPost("terms/{id:guid}/unarchive")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<AcademicTerm>> UnarchiveTerm(
Guid id,
CancellationToken cancellationToken)
{
var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
if (!entity.IsArchived) return entity;
entity.IsArchived = false;
entity.ArchivedAt = null;
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpGet("classrooms")]
public async Task<ActionResult<object>> GetClassrooms(
int? page,
int? pageSize,
string? keyword,
Guid? campusId,
Guid? buildingId,
int? minimumCapacity,
TeachingVenueNature? teachingVenueNature,
bool? isEnabled,
CancellationToken cancellationToken)
{
if (page.HasValue || pageSize.HasValue)
{
if (page is < 1 || pageSize is < 1 or > 100)
return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。");
var source = db.Classrooms.AsNoTracking();
if (campusId.HasValue)
source = source.Where(x => x.Building!.CampusId == campusId.Value);
if (buildingId.HasValue) source = source.Where(x => x.BuildingId == buildingId.Value);
if (minimumCapacity.HasValue)
source = source.Where(x => x.Capacity >= minimumCapacity.Value);
if (teachingVenueNature.HasValue)
source = source.Where(x => (x.TeachingVenueNature & teachingVenueNature.Value) != 0);
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.Building!.Name.Contains(keyword) ||
x.Building.Campus!.Name.Contains(keyword));
}
var total = await source.CountAsync(cancellationToken);
var items = await source
.OrderBy(x => x.Building!.Campus!.SortOrder).ThenBy(x => x.Code)
.Skip((page!.Value - 1) * pageSize!.Value)
.Take(pageSize.Value)
.Select(x => new ClassroomListItem(
x.Id, x.Code, x.Name, x.BuildingId, x.Building!.CampusId,
x.Building.Name, x.Building.Campus!.Name, x.Capacity,
x.Floor, x.RoomType, x.TeachingVenueNature, x.Equipment,
x.IsEnabled, x.SortOrder))
.ToListAsync(cancellationToken);
return Ok(new PagedResult<ClassroomListItem>(
items, total, page.Value, pageSize.Value));
}
var result = await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("classrooms"),
token => db.Classrooms.AsNoTracking()
.OrderBy(x => x.Building!.Campus!.SortOrder)
.ThenBy(x => x.Code)
.Select(x => new ClassroomListItem(
x.Id,
x.Code,
x.Name,
x.BuildingId,
x.Building!.CampusId,
x.Building.Name,
x.Building.Campus!.Name,
x.Capacity,
x.Floor,
x.RoomType,
x.TeachingVenueNature,
x.Equipment,
x.IsEnabled,
x.SortOrder))
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
return Ok(result);
}
[HttpGet("buildings")]
public async Task<ActionResult<object>> GetBuildings(CancellationToken cancellationToken)
{
var result = await cache.GetOrCreateAsync(
AppCacheKeys.BaseData("buildings"),
token => db.Buildings.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
.Select(x => new BuildingListItem(
x.Id,
x.Code,
x.Name,
x.CampusId,
x.Campus!.Name,
x.IsEnabled,
x.SortOrder))
.ToListAsync(token),
AppCacheProfile.ReferenceData,
[AppCacheTags.BaseData],
cancellationToken);
return Ok(result);
}
[HttpPost("buildings")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Building>> CreateBuilding(
BuildingRequest request,
CancellationToken cancellationToken)
{
if (!await db.Campuses.AnyAsync(x => x.Id == request.CampusId, cancellationToken))
return ValidationProblem("所选校区不存在。");
var entity = new Building
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
CampusId = request.CampusId,
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetBuildings", cancellationToken);
}
[HttpPost("classrooms")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Classroom>> CreateClassroom(
ClassroomRequest request,
CancellationToken cancellationToken)
{
if (!await db.Buildings.AnyAsync(x => x.Id == request.BuildingId, cancellationToken))
return ValidationProblem("所选教学楼不存在。");
var entity = new Classroom
{
Code = request.Code.Trim(),
Name = request.Name.Trim(),
BuildingId = request.BuildingId,
Capacity = request.Capacity,
Floor = request.Floor,
RoomType = request.RoomType.Trim(),
TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature,
Equipment = request.Equipment?.Trim(),
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
return await CreateAsync(entity, "GetClassrooms", cancellationToken);
}
[HttpPut("classrooms/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<ActionResult<Classroom>> UpdateClassroom(
Guid id,
ClassroomRequest request,
CancellationToken cancellationToken)
{
var entity = await db.Classrooms.FindAsync([id], cancellationToken);
if (entity is null) return NotFound();
ApplyCatalog(entity, request);
entity.BuildingId = request.BuildingId;
entity.Capacity = request.Capacity;
entity.Floor = request.Floor;
entity.RoomType = request.RoomType.Trim();
entity.TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature;
entity.Equipment = request.Equipment?.Trim();
await SaveAndInvalidateAsync(cancellationToken);
return entity;
}
[HttpDelete("{kind}/{id:guid}")]
[Authorize(Roles = Administrators)]
public async Task<IActionResult> Delete(
string kind,
Guid id,
CancellationToken cancellationToken)
{
object? entity = kind.ToLowerInvariant() switch
{
"campuses" => await db.Campuses.FindAsync([id], cancellationToken),
"colleges" => await db.Colleges.FindAsync([id], cancellationToken),
"majors" => await db.Majors.FindAsync([id], cancellationToken),
"classes" => await db.AdministrativeClasses.FindAsync([id], cancellationToken),
"terms" => await db.AcademicTerms.FindAsync([id], cancellationToken),
"buildings" => await db.Buildings.FindAsync([id], cancellationToken),
"classrooms" => await db.Classrooms.FindAsync([id], cancellationToken),
"course-categories" => await db.CourseCategories.FindAsync([id], cancellationToken),
_ => null
};
if (entity is null) return NotFound();
if (entity is AcademicTerm { IsCurrent: true })
return ConflictProblem("当前学期不能删除,请先切换到其他学期。");
db.Remove(entity);
try
{
await SaveAndInvalidateAsync(cancellationToken);
return NoContent();
}
catch (DbUpdateException)
{
return Conflict(new ProblemDetails
{
Title = "无法删除",
Detail = "该数据已被其他业务引用,请先停用,或移除关联数据后再删除。",
Status = StatusCodes.Status409Conflict
});
}
}
private async Task<ActionResult<TEntity>> CreateAsync<TEntity>(
TEntity entity,
string action,
CancellationToken cancellationToken)
where TEntity : EntityBase
{
db.Add(entity);
try
{
await SaveAndInvalidateAsync(cancellationToken);
}
catch (DbUpdateException)
{
ModelState.AddModelError("code", "编码已存在或关联数据无效。");
return ValidationProblem(ModelState);
}
return CreatedAtAction(action, new { id = entity.Id }, entity);
}
private async Task SaveAndInvalidateAsync(CancellationToken cancellationToken)
{
await db.SaveChangesAsync(cancellationToken);
await cache.RemoveByTagAsync(AppCacheTags.BaseData, cancellationToken);
}
private async Task<string?> GetCounselorNameAsync(
Guid? userId,
CancellationToken cancellationToken)
{
if (!userId.HasValue) return null;
return await db.Users.AsNoTracking()
.Where(user =>
user.Id == userId &&
user.IsEnabled &&
db.UserRoles.Any(userRole =>
userRole.UserId == user.Id &&
db.Roles.Any(role =>
role.Id == userRole.RoleId &&
role.Name == SystemRoles.Counselor)))
.Select(x => x.DisplayName)
.FirstOrDefaultAsync(cancellationToken);
}
private static void ApplyCatalog(CatalogEntity entity, CatalogRequest request)
{
entity.Code = request.Code.Trim();
entity.Name = request.Name.Trim();
entity.SortOrder = request.SortOrder;
entity.IsEnabled = request.IsEnabled;
}
private ObjectResult ConflictProblem(string detail) =>
Conflict(new ProblemDetails
{
Title = "当前操作无法完成",
Detail = detail,
Status = StatusCodes.Status409Conflict
});
}
public record CatalogRequest(
[Required, MaxLength(40)] string Code,
[Required, MaxLength(100)] string Name,
int SortOrder = 0,
bool IsEnabled = true,
[MaxLength(300)] string? Description = null);
public sealed record CollegeRequest(
string Code, string Name, int SortOrder, bool IsEnabled,
Guid? CampusId,
[MaxLength(50)] string? ShortName)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
public sealed record MajorRequest(
string Code, string Name, int SortOrder, bool IsEnabled,
Guid CollegeId,
[Required, MaxLength(30)] string DegreeType,
[Range(1, 8)] int SchoolingYears)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
public sealed record ClassRequest(
string Code, string Name, int SortOrder, bool IsEnabled,
Guid MajorId,
[Range(2000, 2200)] int Grade,
Guid? CounselorUserId)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
public sealed record TermRequest(
string Code, string Name, bool IsEnabled,
[Required, MaxLength(20)] string AcademicYear,
TermSeason Season,
DateOnly StartDate,
DateOnly EndDate,
bool IsCurrent)
: CatalogRequest(Code, Name, 0, IsEnabled);
public sealed record BuildingRequest(
string Code, string Name, int SortOrder, bool IsEnabled,
Guid CampusId)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
public sealed record ClassroomRequest(
string Code, string Name, int SortOrder, bool IsEnabled,
Guid BuildingId,
[Range(1, 1000)] int Capacity,
[Range(1, 99)] int Floor,
[Required, MaxLength(40)] string RoomType,
TeachingVenueNature TeachingVenueNature,
[MaxLength(300)] string? Equipment)
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
public sealed record CollegeListItem(
Guid Id,
string Code,
string Name,
string? ShortName,
Guid? CampusId,
string? CampusName,
bool IsEnabled,
int SortOrder);
public sealed record MajorListItem(
Guid Id,
string Code,
string Name,
Guid CollegeId,
string CollegeName,
string DegreeType,
int SchoolingYears,
bool IsEnabled,
int SortOrder);
public sealed record AdministrativeClassListItem(
Guid Id,
string Code,
string Name,
Guid MajorId,
string MajorName,
string CollegeName,
int Grade,
Guid? CounselorUserId,
string? CounselorName,
bool IsEnabled,
int SortOrder);
public sealed record BuildingListItem(
Guid Id,
string Code,
string Name,
Guid CampusId,
string CampusName,
bool IsEnabled,
int SortOrder);
public sealed record ClassroomListItem(
Guid Id,
string Code,
string Name,
Guid BuildingId,
Guid CampusId,
string BuildingName,
string CampusName,
int Capacity,
int Floor,
string RoomType,
TeachingVenueNature TeachingVenueNature,
string? Equipment,
bool IsEnabled,
int SortOrder);