教师档案增加“激活账号”:管理员设置初始密码,服务端校验管理权限、学院数据范围、在职状态、工号冲突,并自动关联教师角色与档案。
已发布课表增加“发布后修改”:创建修订草稿,旧课表继续生效;修订版重新发布后再替换旧版本。 修复 390px 窄屏下课表页头按钮溢出。
This commit is contained in:
1 parent
ce65fe82d9
commit
db91988264
5 files changed
+333
-11
No files matched your search
@@ -5,6 +5,7 @@ using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -15,7 +16,8 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Route("api/personnel")]
|
||||
public sealed class PersonnelController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
ICurrentUserDataScope currentUserDataScope,
|
||||
UserManager<ApplicationUser> userManager) : ControllerBase
|
||||
{
|
||||
private const string ReadRoles =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
@@ -149,6 +151,61 @@ public sealed class PersonnelController(
|
||||
return await SaveNoContentAsync(cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("teachers/{id:guid}/activate-account")]
|
||||
[Authorize(Roles = WriteRoles)]
|
||||
public async Task<ActionResult> ActivateTeacherAccount(
|
||||
Guid id,
|
||||
TeacherAccountActivationRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var teacher = await db.Teachers.FindAsync([id], cancellationToken);
|
||||
if (teacher is null) return NotFound();
|
||||
if (!CanAccessCollege(teacher.CollegeId)) return Forbid();
|
||||
if (teacher.Status != TeacherStatus.Active)
|
||||
{
|
||||
return ConflictProblem("仅在职教师可以激活登录账号。");
|
||||
}
|
||||
if (teacher.UserId.HasValue)
|
||||
{
|
||||
return ConflictProblem("该教师档案已经关联登录账号。");
|
||||
}
|
||||
|
||||
var userName = teacher.TeacherNumber.Trim();
|
||||
if (await userManager.FindByNameAsync(userName) is not null)
|
||||
{
|
||||
return ConflictProblem("该工号已有登录账号但未正确关联,请到账号管理中核对。");
|
||||
}
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = userName,
|
||||
DisplayName = teacher.Name,
|
||||
StaffNumber = userName,
|
||||
CollegeId = teacher.CollegeId,
|
||||
IsEnabled = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
var result = await userManager.CreateAsync(user, request.Password);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return IdentityValidationProblem(result);
|
||||
}
|
||||
|
||||
result = await userManager.AddToRoleAsync(user, SystemRoles.Teacher);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return IdentityValidationProblem(result);
|
||||
}
|
||||
|
||||
teacher.UserId = user.Id;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return Ok(new { user.Id, UserName = userName });
|
||||
}
|
||||
|
||||
[HttpGet("students")]
|
||||
public async Task<ActionResult<PagedResult<object>>> GetStudents(
|
||||
[FromQuery] PersonnelQuery query,
|
||||
@@ -393,6 +450,13 @@ public sealed class PersonnelController(
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
private ActionResult IdentityValidationProblem(IdentityResult result)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
ModelState.AddModelError(error.Code, error.Description);
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
@@ -436,3 +500,6 @@ public sealed record StudentRequest(
|
||||
[MaxLength(30)] string? Phone,
|
||||
[EmailAddress, MaxLength(100)] string? Email,
|
||||
[MaxLength(500)] string? Notes);
|
||||
|
||||
public sealed record TeacherAccountActivationRequest(
|
||||
[Required, MinLength(8), MaxLength(100)] string Password);
|
||||
@@ -0,0 +1,90 @@
|
||||
using Jiaowu.Api.Controllers;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jiaowu.Api.Tests;
|
||||
|
||||
public sealed class PersonnelControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ActivateTeacherAccount_CreatesAndLinksTeacherLogin()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddDbContext<AppDbContext>(options => options.UseSqlite(connection));
|
||||
services
|
||||
.AddIdentityCore<ApplicationUser>(options =>
|
||||
{
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireNonAlphanumeric = true;
|
||||
})
|
||||
.AddRoles<ApplicationRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>();
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
await using var scope = provider.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
var roleManager = scope.ServiceProvider
|
||||
.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||
Assert.True((await roleManager.CreateAsync(new ApplicationRole
|
||||
{
|
||||
Name = SystemRoles.Teacher,
|
||||
Description = "教师"
|
||||
})).Succeeded);
|
||||
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var teacher = new Teacher
|
||||
{
|
||||
TeacherNumber = "T2026999",
|
||||
Name = "测试教师",
|
||||
CollegeId = college.Id,
|
||||
Status = TeacherStatus.Active
|
||||
};
|
||||
db.AddRange(college, teacher);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var userManager = scope.ServiceProvider
|
||||
.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var controller = new PersonnelController(
|
||||
db,
|
||||
new TestDataScope(college.Id),
|
||||
userManager);
|
||||
|
||||
var result = await controller.ActivateTeacherAccount(
|
||||
teacher.Id,
|
||||
new TeacherAccountActivationRequest("Teacher@123"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
var user = await userManager.FindByNameAsync(teacher.TeacherNumber);
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal(teacher.Name, user.DisplayName);
|
||||
Assert.Equal(teacher.CollegeId, user.CollegeId);
|
||||
Assert.True(await userManager.IsInRoleAsync(user, SystemRoles.Teacher));
|
||||
await db.Entry(teacher).ReloadAsync();
|
||||
Assert.Equal(user.Id, teacher.UserId);
|
||||
}
|
||||
|
||||
private sealed class TestDataScope(Guid collegeId) : ICurrentUserDataScope
|
||||
{
|
||||
public CurrentUserScope Current { get; } = new(
|
||||
Guid.NewGuid(),
|
||||
"测试管理员",
|
||||
collegeId,
|
||||
DataScope.College,
|
||||
new HashSet<string>([SystemRoles.CollegeAdmin]));
|
||||
}
|
||||
}
|
||||
@@ -1070,6 +1070,8 @@ button { cursor: pointer; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page-intro { align-items: flex-start; flex-direction: column; }
|
||||
.page-actions { width: 100%; flex-wrap: wrap; }
|
||||
.option-filter-grid.course,
|
||||
.option-filter-grid.course.compact,
|
||||
.option-filter-grid.classes { grid-template-columns: 1fr; }
|
||||
|
||||
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user