1
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public sealed class AuthController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ITokenService tokenService) : ControllerBase
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<LoginResponse>> Login(LoginRequest request)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(request.UserName);
|
||||
if (user is null || !user.IsEnabled)
|
||||
{
|
||||
return Unauthorized(new ProblemDetails
|
||||
{
|
||||
Title = "登录失败",
|
||||
Detail = "账号或密码不正确,或账号已停用。",
|
||||
Status = StatusCodes.Status401Unauthorized
|
||||
});
|
||||
}
|
||||
|
||||
if (await userManager.IsLockedOutAsync(user) ||
|
||||
!await userManager.CheckPasswordAsync(user, request.Password))
|
||||
{
|
||||
await userManager.AccessFailedAsync(user);
|
||||
return Unauthorized(new ProblemDetails
|
||||
{
|
||||
Title = "登录失败",
|
||||
Detail = "账号或密码不正确,或账号已停用。",
|
||||
Status = StatusCodes.Status401Unauthorized
|
||||
});
|
||||
}
|
||||
|
||||
await userManager.ResetAccessFailedCountAsync(user);
|
||||
user.LastLoginAt = DateTime.UtcNow;
|
||||
await userManager.UpdateAsync(user);
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
|
||||
return new LoginResponse(
|
||||
tokenService.Create(user, roles),
|
||||
new CurrentUserResponse(
|
||||
user.Id,
|
||||
user.UserName!,
|
||||
user.DisplayName,
|
||||
roles,
|
||||
user.CollegeId));
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("me")]
|
||||
public async Task<ActionResult<CurrentUserResponse>> Me()
|
||||
{
|
||||
var id = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var user = id is null ? null : await userManager.FindByIdAsync(id);
|
||||
if (user is null || !user.IsEnabled)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
return new CurrentUserResponse(
|
||||
user.Id,
|
||||
user.UserName!,
|
||||
user.DisplayName,
|
||||
await userManager.GetRolesAsync(user),
|
||||
user.CollegeId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LoginRequest(
|
||||
[Required, MaxLength(100)] string UserName,
|
||||
[Required, MaxLength(100)] string Password);
|
||||
|
||||
public sealed record LoginResponse(string Token, CurrentUserResponse User);
|
||||
|
||||
public sealed record CurrentUserResponse(
|
||||
Guid Id,
|
||||
string UserName,
|
||||
string DisplayName,
|
||||
IEnumerable<string> Roles,
|
||||
Guid? CollegeId);
|
||||
@@ -0,0 +1,468 @@
|
||||
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));
|
||||
|
||||
[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.CounselorName, x.IsEnabled, x.SortOrder
|
||||
})
|
||||
.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 entity = new AdministrativeClass
|
||||
{
|
||||
Code = request.Code.Trim(),
|
||||
Name = request.Name.Trim(),
|
||||
MajorId = request.MajorId,
|
||||
Grade = request.Grade,
|
||||
CounselorName = request.CounselorName?.Trim(),
|
||||
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();
|
||||
ApplyCatalog(entity, request);
|
||||
entity.MajorId = request.MajorId;
|
||||
entity.Grade = request.Grade;
|
||||
entity.CounselorName = request.CounselorName?.Trim();
|
||||
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,
|
||||
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),
|
||||
_ => 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 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,
|
||||
[MaxLength(50)] string? CounselorName)
|
||||
: 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);
|
||||
@@ -0,0 +1,36 @@
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/dashboard")]
|
||||
public sealed class DashboardController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<object>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
var currentTerm = await db.AcademicTerms
|
||||
.AsNoTracking()
|
||||
.Where(x => x.IsCurrent)
|
||||
.Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return new
|
||||
{
|
||||
CurrentTerm = currentTerm,
|
||||
Counts = new
|
||||
{
|
||||
Campuses = await db.Campuses.CountAsync(cancellationToken),
|
||||
Colleges = await db.Colleges.CountAsync(cancellationToken),
|
||||
Majors = await db.Majors.CountAsync(cancellationToken),
|
||||
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
||||
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
||||
Users = await db.Users.CountAsync(cancellationToken)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Roles = SystemRoles.SuperAdmin)]
|
||||
[Route("api/users")]
|
||||
public sealed class UsersController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
RoleManager<ApplicationRole> roleManager) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<object>> GetUsers(CancellationToken cancellationToken)
|
||||
{
|
||||
var users = await userManager.Users.AsNoTracking()
|
||||
.OrderBy(x => x.UserName)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id, x.UserName, x.DisplayName, x.StaffNumber,
|
||||
x.CollegeId, x.IsEnabled, x.LastLoginAt, x.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var user in users)
|
||||
{
|
||||
var identityUser = await userManager.FindByIdAsync(user.Id.ToString());
|
||||
result.Add(new
|
||||
{
|
||||
user.Id, user.UserName, user.DisplayName, user.StaffNumber,
|
||||
user.CollegeId, user.IsEnabled, user.LastLoginAt, user.CreatedAt,
|
||||
Roles = identityUser is null
|
||||
? []
|
||||
: await userManager.GetRolesAsync(identityUser)
|
||||
});
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("roles")]
|
||||
public async Task<ActionResult<object>> GetRoles(CancellationToken cancellationToken) =>
|
||||
Ok(await roleManager.Roles.AsNoTracking()
|
||||
.OrderBy(x => x.Name)
|
||||
.Select(x => new { x.Name, x.Description, x.DataScope })
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Create(CreateUserRequest request)
|
||||
{
|
||||
var invalidRoles = request.Roles
|
||||
.Except(SystemRoles.All, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (invalidRoles.Length > 0)
|
||||
return ValidationProblem($"无效角色:{string.Join("、", invalidRoles)}");
|
||||
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = request.UserName.Trim(),
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
StaffNumber = request.StaffNumber?.Trim(),
|
||||
CollegeId = request.CollegeId,
|
||||
LockoutEnabled = true,
|
||||
IsEnabled = true
|
||||
};
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
if (!result.Succeeded)
|
||||
return IdentityValidationProblem(result);
|
||||
|
||||
result = await userManager.AddToRolesAsync(user, request.Roles);
|
||||
if (!result.Succeeded)
|
||||
return IdentityValidationProblem(result);
|
||||
|
||||
return CreatedAtAction(nameof(GetUsers), new { id = user.Id }, new { user.Id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/status")]
|
||||
public async Task<IActionResult> SetStatus(Guid id, SetUserStatusRequest request)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(id.ToString());
|
||||
if (user is null) return NotFound();
|
||||
if (User.FindFirstValue(ClaimTypes.NameIdentifier) == id.ToString() &&
|
||||
!request.IsEnabled)
|
||||
return ValidationProblem("不能停用当前登录账号。");
|
||||
|
||||
user.IsEnabled = request.IsEnabled;
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
return result.Succeeded ? NoContent() : IdentityValidationProblem(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/roles")]
|
||||
public async Task<IActionResult> SetRoles(Guid id, SetRolesRequest request)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(id.ToString());
|
||||
if (user is null) return NotFound();
|
||||
|
||||
var invalidRoles = request.Roles
|
||||
.Except(SystemRoles.All, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
if (invalidRoles.Length > 0)
|
||||
return ValidationProblem($"无效角色:{string.Join("、", invalidRoles)}");
|
||||
|
||||
var existing = await userManager.GetRolesAsync(user);
|
||||
if (User.FindFirstValue(ClaimTypes.NameIdentifier) == id.ToString() &&
|
||||
existing.Contains(SystemRoles.SuperAdmin) &&
|
||||
!request.Roles.Contains(SystemRoles.SuperAdmin))
|
||||
{
|
||||
return ValidationProblem("不能移除当前账号的超级管理员角色。");
|
||||
}
|
||||
|
||||
var removeResult = await userManager.RemoveFromRolesAsync(
|
||||
user,
|
||||
existing.Except(request.Roles));
|
||||
if (!removeResult.Succeeded) return IdentityValidationProblem(removeResult);
|
||||
var addResult = await userManager.AddToRolesAsync(
|
||||
user,
|
||||
request.Roles.Except(existing));
|
||||
return addResult.Succeeded ? NoContent() : IdentityValidationProblem(addResult);
|
||||
}
|
||||
|
||||
private ActionResult IdentityValidationProblem(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
ModelState.AddModelError(error.Code, error.Description);
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CreateUserRequest(
|
||||
[Required, MaxLength(50)] string UserName,
|
||||
[Required, MaxLength(50)] string DisplayName,
|
||||
[Required, MinLength(8), MaxLength(100)] string Password,
|
||||
[MaxLength(30)] string? StaffNumber,
|
||||
Guid? CollegeId,
|
||||
[MinLength(1)] string[] Roles);
|
||||
|
||||
public sealed record SetUserStatusRequest(bool IsEnabled);
|
||||
public sealed record SetRolesRequest([MinLength(1)] string[] Roles);
|
||||
Reference in New Issue
Block a user