课程约束:可限定校区、教学楼、指定教室、允许上课日、最早/最晚节次。 无教室课程:教室可为空,但仍校验教师和班级冲突。 作息维护:按学期维护节次、上下课时间和启用状态。 发布保护:发布前重新检查全部约束,并阻止学时未排满的课表发布。 工作台升级:增加“排课规则与作息”“自动排课”入口,自动生成后仍支持手工微调。 同时兼容 SQLite 和 MySQL,已生成正式迁移。
561 lines
21 KiB
C#
561 lines
21 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Domain.Common;
|
|
using Jiaowu.Api.Domain.Identity;
|
|
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) : ControllerBase
|
|
{
|
|
private const string Administrators =
|
|
$"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";
|
|
|
|
[HttpGet("campuses")]
|
|
public async Task<ActionResult<IReadOnlyCollection<Campus>>> GetCampuses(
|
|
CancellationToken cancellationToken) =>
|
|
await db.Campuses.AsNoTracking()
|
|
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
|
.ToListAsync(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 db.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
[HttpGet("colleges")]
|
|
public async Task<ActionResult<object>> GetColleges(CancellationToken cancellationToken) =>
|
|
Ok(await db.Colleges.AsNoTracking()
|
|
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id, x.Code, x.Name, x.ShortName, x.CampusId,
|
|
CampusName = x.Campus != null ? x.Campus.Name : null,
|
|
x.IsEnabled, x.SortOrder
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
|
|
[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 db.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
[HttpGet("majors")]
|
|
public async Task<ActionResult<object>> GetMajors(CancellationToken cancellationToken) =>
|
|
Ok(await db.Majors.AsNoTracking()
|
|
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id, x.Code, x.Name, x.CollegeId,
|
|
CollegeName = x.College!.Name,
|
|
x.DegreeType, x.SchoolingYears, x.IsEnabled, x.SortOrder
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
|
|
[HttpGet("course-categories")]
|
|
public async Task<ActionResult<IReadOnlyCollection<CourseCategory>>> GetCourseCategories(
|
|
CancellationToken cancellationToken) =>
|
|
await db.CourseCategories.AsNoTracking()
|
|
.OrderBy(x => x.SortOrder)
|
|
.ThenBy(x => x.Code)
|
|
.ToListAsync(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 db.SaveChangesAsync(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 db.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
[HttpGet("classes")]
|
|
public async Task<ActionResult<object>> GetClasses(CancellationToken cancellationToken) =>
|
|
Ok(await db.AdministrativeClasses.AsNoTracking()
|
|
.OrderByDescending(x => x.Grade).ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id, x.Code, x.Name, x.MajorId,
|
|
MajorName = x.Major!.Name,
|
|
CollegeName = x.Major.College!.Name,
|
|
x.Grade, x.CounselorUserId, x.CounselorName,
|
|
x.IsEnabled, x.SortOrder
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
|
|
[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 db.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
[HttpGet("terms")]
|
|
public async Task<ActionResult<IReadOnlyCollection<AcademicTerm>>> GetTerms(
|
|
CancellationToken cancellationToken) =>
|
|
await db.AcademicTerms.AsNoTracking()
|
|
.OrderByDescending(x => x.StartDate)
|
|
.ToListAsync(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)
|
|
await db.AcademicTerms.ExecuteUpdateAsync(
|
|
setters => setters.SetProperty(x => x.IsCurrent, false),
|
|
cancellationToken);
|
|
|
|
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 = request.IsCurrent,
|
|
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.IsCurrent)
|
|
await db.AcademicTerms.Where(x => x.Id != id).ExecuteUpdateAsync(
|
|
setters => setters.SetProperty(x => x.IsCurrent, false),
|
|
cancellationToken);
|
|
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 db.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
[HttpGet("classrooms")]
|
|
public async Task<ActionResult<object>> GetClassrooms(CancellationToken cancellationToken) =>
|
|
Ok(await db.Classrooms.AsNoTracking()
|
|
.OrderBy(x => x.Building!.Campus!.SortOrder)
|
|
.ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id, x.Code, x.Name, x.BuildingId,
|
|
CampusId = x.Building!.CampusId,
|
|
BuildingName = x.Building!.Name,
|
|
CampusName = x.Building.Campus!.Name,
|
|
x.Capacity, x.RoomType, x.Equipment, x.IsEnabled, x.SortOrder
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
|
|
[HttpGet("buildings")]
|
|
public async Task<ActionResult<object>> GetBuildings(CancellationToken cancellationToken) =>
|
|
Ok(await db.Buildings.AsNoTracking()
|
|
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
|
.Select(x => new
|
|
{
|
|
x.Id, x.Code, x.Name, x.CampusId,
|
|
CampusName = x.Campus!.Name, x.IsEnabled, x.SortOrder
|
|
})
|
|
.ToListAsync(cancellationToken));
|
|
|
|
[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,
|
|
RoomType = request.RoomType.Trim(),
|
|
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.RoomType = request.RoomType.Trim();
|
|
entity.Equipment = request.Equipment?.Trim();
|
|
await db.SaveChangesAsync(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();
|
|
db.Remove(entity);
|
|
try
|
|
{
|
|
await db.SaveChangesAsync(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 db.SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
ModelState.AddModelError("code", "编码已存在或关联数据无效。");
|
|
return ValidationProblem(ModelState);
|
|
}
|
|
return CreatedAtAction(action, new { id = entity.Id }, entity);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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,
|
|
[Required, MaxLength(40)] string RoomType,
|
|
[MaxLength(300)] string? Equipment)
|
|
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
|