升级
This commit is contained in:
@@ -15,7 +15,8 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Route("api/personnel")]
|
||||
public sealed class PersonnelController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
ICurrentUserDataScope currentUserDataScope,
|
||||
PersonnelAccountService personnelAccountService) : ControllerBase
|
||||
{
|
||||
private const string ReadRoles =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
@@ -103,7 +104,14 @@ public sealed class PersonnelController(
|
||||
Notes = Normalize(request.Notes)
|
||||
};
|
||||
db.Teachers.Add(entity);
|
||||
return await SaveCreatedAsync(entity.Id, cancellationToken);
|
||||
return await CreateWithAccountAsync(
|
||||
entity.Id,
|
||||
request.InitialPassword,
|
||||
() => personnelAccountService.EnsureTeacherAccountAsync(
|
||||
entity,
|
||||
request.InitialPassword,
|
||||
cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("teachers/{id:guid}")]
|
||||
@@ -130,6 +138,18 @@ public sealed class PersonnelController(
|
||||
entity.Phone = Normalize(request.Phone);
|
||||
entity.Email = Normalize(request.Email);
|
||||
entity.Notes = Normalize(request.Notes);
|
||||
if (!entity.UserId.HasValue &&
|
||||
(string.IsNullOrWhiteSpace(request.InitialPassword) ||
|
||||
request.InitialPassword.Length < 8))
|
||||
return ValidationProblem("该教师档案尚未开通账号,请填写至少 8 位初始密码。");
|
||||
if (entity.UserId.HasValue || !string.IsNullOrWhiteSpace(request.InitialPassword))
|
||||
{
|
||||
var account = await personnelAccountService.EnsureTeacherAccountAsync(
|
||||
entity,
|
||||
request.InitialPassword,
|
||||
cancellationToken);
|
||||
if (!account.Success) return AccountProblem(account.Error!);
|
||||
}
|
||||
return await SaveNoContentAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -239,7 +259,14 @@ public sealed class PersonnelController(
|
||||
Notes = Normalize(request.Notes)
|
||||
};
|
||||
db.Students.Add(entity);
|
||||
return await SaveCreatedAsync(entity.Id, cancellationToken);
|
||||
return await CreateWithAccountAsync(
|
||||
entity.Id,
|
||||
request.InitialPassword,
|
||||
() => personnelAccountService.EnsureStudentAccountAsync(
|
||||
entity,
|
||||
request.InitialPassword,
|
||||
cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("students/{id:guid}")]
|
||||
@@ -276,6 +303,18 @@ public sealed class PersonnelController(
|
||||
entity.Phone = Normalize(request.Phone);
|
||||
entity.Email = Normalize(request.Email);
|
||||
entity.Notes = Normalize(request.Notes);
|
||||
if (!entity.UserId.HasValue &&
|
||||
(string.IsNullOrWhiteSpace(request.InitialPassword) ||
|
||||
request.InitialPassword.Length < 8))
|
||||
return ValidationProblem("该学生档案尚未开通账号,请填写至少 8 位初始密码。");
|
||||
if (entity.UserId.HasValue || !string.IsNullOrWhiteSpace(request.InitialPassword))
|
||||
{
|
||||
var account = await personnelAccountService.EnsureStudentAccountAsync(
|
||||
entity,
|
||||
request.InitialPassword,
|
||||
cancellationToken);
|
||||
if (!account.Success) return AccountProblem(account.Error!);
|
||||
}
|
||||
return await SaveNoContentAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -372,6 +411,39 @@ public sealed class PersonnelController(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ActionResult> CreateWithAccountAsync(
|
||||
Guid id,
|
||||
string? initialPassword,
|
||||
Func<Task<PersonnelAccountResult>> createAccount,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(initialPassword) || initialPassword.Length < 8)
|
||||
return ValidationProblem("新增人员时必须填写至少 8 位初始密码。");
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var account = await createAccount();
|
||||
if (!account.Success)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return AccountProblem(account.Error!);
|
||||
}
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return Created(string.Empty, new
|
||||
{
|
||||
id,
|
||||
account.UserId,
|
||||
account.UserName
|
||||
});
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
return ConflictProblem("编号已存在,或关联数据无效。");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ActionResult> SaveNoContentAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -393,6 +465,14 @@ public sealed class PersonnelController(
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
private ActionResult AccountProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "登录账号创建失败",
|
||||
Detail = detail,
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
@@ -422,7 +502,8 @@ public sealed record TeacherRequest(
|
||||
bool IsExternal,
|
||||
[MaxLength(30)] string? Phone,
|
||||
[EmailAddress, MaxLength(100)] string? Email,
|
||||
[MaxLength(500)] string? Notes);
|
||||
[MaxLength(500)] string? Notes,
|
||||
[MinLength(8), MaxLength(100)] string? InitialPassword = null);
|
||||
|
||||
public sealed record StudentRequest(
|
||||
[Required, MaxLength(30)] string StudentNumber,
|
||||
@@ -435,4 +516,5 @@ public sealed record StudentRequest(
|
||||
DateOnly? DateOfBirth,
|
||||
[MaxLength(30)] string? Phone,
|
||||
[EmailAddress, MaxLength(100)] string? Email,
|
||||
[MaxLength(500)] string? Notes);
|
||||
[MaxLength(500)] string? Notes,
|
||||
[MinLength(8), MaxLength(100)] string? InitialPassword = null);
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace Jiaowu.Api.Controllers;
|
||||
[Route("api/personnel")]
|
||||
public sealed class PersonnelExcelController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
ICurrentUserDataScope currentUserDataScope,
|
||||
PersonnelAccountService personnelAccountService) : ControllerBase
|
||||
{
|
||||
private const string ReadRoles =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
@@ -32,13 +33,13 @@ public sealed class PersonnelExcelController(
|
||||
private static readonly string[] TeacherHeaders =
|
||||
[
|
||||
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
|
||||
"入职日期", "教师类别", "联系电话", "电子邮箱", "备注"
|
||||
"入职日期", "教师类别", "联系电话", "电子邮箱", "备注", "初始密码"
|
||||
];
|
||||
|
||||
private static readonly string[] StudentHeaders =
|
||||
[
|
||||
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
|
||||
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注"
|
||||
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注", "初始密码"
|
||||
];
|
||||
|
||||
[HttpGet("{kind}/template")]
|
||||
@@ -56,6 +57,7 @@ public sealed class PersonnelExcelController(
|
||||
? "工号是唯一标识;学院编码必须已存在。"
|
||||
: "学号是唯一标识;行政班编码必须已存在。",
|
||||
"编号已存在时更新档案,不存在时新增档案。",
|
||||
"新增人员或补建账号时必须填写至少 8 位初始密码;已有关联账号时可留空。",
|
||||
"日期填写为 yyyy-MM-dd;不适用的可选字段可以留空。",
|
||||
"整批数据会先校验,任一行有误时均不会写入。"
|
||||
]);
|
||||
@@ -81,7 +83,7 @@ public sealed class PersonnelExcelController(
|
||||
.Select(x => Row(
|
||||
x.TeacherNumber, x.Name, GenderName(x.Gender), x.College!.Code,
|
||||
x.Title, TeacherStatusName(x.Status), x.HireDate,
|
||||
x.IsExternal ? "外聘" : "校内", x.Phone, x.Email, x.Notes))
|
||||
x.IsExternal ? "外聘" : "校内", x.Phone, x.Email, x.Notes, null))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
@@ -94,7 +96,7 @@ public sealed class PersonnelExcelController(
|
||||
.Select(x => Row(
|
||||
x.StudentNumber, x.Name, GenderName(x.Gender),
|
||||
x.AdministrativeClass!.Code, x.EnrollmentYear, x.EnrollmentDate,
|
||||
StudentStatusName(x.Status), x.DateOfBirth, x.Phone, x.Email, x.Notes))
|
||||
StudentStatusName(x.Status), x.DateOfBirth, x.Phone, x.Email, x.Notes, null))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -212,6 +214,12 @@ public sealed class PersonnelExcelController(
|
||||
entity.Phone = Optional(row, "联系电话");
|
||||
entity.Email = Optional(row, "电子邮箱");
|
||||
entity.Notes = Optional(row, "备注");
|
||||
var account = await personnelAccountService.EnsureTeacherAccountAsync(
|
||||
entity,
|
||||
Optional(row, "初始密码"),
|
||||
cancellationToken);
|
||||
if (!account.Success)
|
||||
errors.Add($"第 {row.RowNumber} 行:{account.Error}");
|
||||
}
|
||||
return new(created, updated, rows.Count);
|
||||
}
|
||||
@@ -284,6 +292,12 @@ public sealed class PersonnelExcelController(
|
||||
entity.Phone = Optional(row, "联系电话");
|
||||
entity.Email = Optional(row, "电子邮箱");
|
||||
entity.Notes = Optional(row, "备注");
|
||||
var account = await personnelAccountService.EnsureStudentAccountAsync(
|
||||
entity,
|
||||
Optional(row, "初始密码"),
|
||||
cancellationToken);
|
||||
if (!account.Success)
|
||||
errors.Add($"第 {row.RowNumber} 行:{account.Error}");
|
||||
}
|
||||
return new(created, updated, rows.Count);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
using System.Security.Claims;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
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]
|
||||
[Route("api/timetables")]
|
||||
public sealed class TimetablesController(AppDbContext db) : ControllerBase
|
||||
{
|
||||
[HttpGet("options")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
var terms = await db.AcademicTerms.AsNoTracking()
|
||||
.Where(x => x.IsEnabled)
|
||||
.OrderByDescending(x => x.StartDate)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.AcademicYear,
|
||||
x.Season,
|
||||
x.StartDate,
|
||||
x.EndDate,
|
||||
x.IsCurrent,
|
||||
HasPublishedTimetable = db.SchedulePlans.Any(plan =>
|
||||
plan.AcademicTermId == x.Id &&
|
||||
plan.Status == SchedulePlanStatus.Published)
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var defaultTermId = terms.FirstOrDefault(x => x.IsCurrent)?.Id
|
||||
?? terms.FirstOrDefault(x => x.HasPublishedTimetable)?.Id;
|
||||
var classes = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled)
|
||||
.OrderByDescending(x => x.Grade)
|
||||
.ThenBy(x => x.Code)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.Grade,
|
||||
MajorName = x.Major!.Name,
|
||||
CollegeName = x.Major.College!.Name,
|
||||
HasPublishedTimetable = defaultTermId.HasValue &&
|
||||
db.ScheduleEntries.Any(entry =>
|
||||
entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
|
||||
entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||
entry.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == x.Id))
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
return Ok(new { Terms = terms, Classes = classes });
|
||||
}
|
||||
|
||||
[HttpGet("classes/{classId:guid}")]
|
||||
[AllowAnonymous]
|
||||
public Task<ActionResult> GetClassTimetable(
|
||||
Guid classId,
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken) =>
|
||||
BuildTimetableAsync(classId, academicTermId, null, cancellationToken);
|
||||
|
||||
[HttpGet("mine")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
public async Task<ActionResult> GetMyTimetable(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
|
||||
return Unauthorized();
|
||||
var student = await db.Students.AsNoTracking()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.AdministrativeClassId,
|
||||
x.StudentNumber,
|
||||
x.Name
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (student is null)
|
||||
return Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "学生档案未关联",
|
||||
Detail = "当前登录账号没有关联学生档案,请联系教务管理员。",
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
return await BuildTimetableAsync(
|
||||
student.AdministrativeClassId,
|
||||
academicTermId,
|
||||
student.Id,
|
||||
cancellationToken,
|
||||
new { student.StudentNumber, student.Name });
|
||||
}
|
||||
|
||||
private async Task<ActionResult> BuildTimetableAsync(
|
||||
Guid classId,
|
||||
Guid? academicTermId,
|
||||
Guid? studentId,
|
||||
CancellationToken cancellationToken,
|
||||
object? student = null)
|
||||
{
|
||||
var administrativeClass = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.Id == classId && x.IsEnabled)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.Grade,
|
||||
MajorName = x.Major!.Name,
|
||||
CollegeName = x.Major.College!.Name
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (administrativeClass is null) return NotFound();
|
||||
|
||||
var termQuery = db.AcademicTerms.AsNoTracking().Where(x => x.IsEnabled);
|
||||
if (academicTermId.HasValue)
|
||||
termQuery = termQuery.Where(x => x.Id == academicTermId);
|
||||
else
|
||||
termQuery = termQuery.OrderByDescending(x => x.IsCurrent)
|
||||
.ThenByDescending(x => x.StartDate);
|
||||
var term = await termQuery
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Name,
|
||||
x.AcademicYear,
|
||||
x.Season,
|
||||
x.StartDate,
|
||||
x.EndDate,
|
||||
x.IsCurrent
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (term is null) return NotFound();
|
||||
|
||||
var plan = await db.SchedulePlans.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.AcademicTermId == term.Id &&
|
||||
x.Status == SchedulePlanStatus.Published)
|
||||
.Select(x => new { x.Id, x.Name, x.Version, x.PublishedAt })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var slots = await db.ScheduleTimeSlots.AsNoTracking()
|
||||
.Where(x => x.AcademicTermId == term.Id && x.IsEnabled)
|
||||
.OrderBy(x => x.PeriodNumber)
|
||||
.Select(x => new { x.PeriodNumber, x.Name, x.StartsAt, x.EndsAt })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (plan is null)
|
||||
return Ok(new
|
||||
{
|
||||
Term = term,
|
||||
Class = administrativeClass,
|
||||
Student = student,
|
||||
Plan = (object?)null,
|
||||
Slots = slots,
|
||||
Entries = Array.Empty<object>()
|
||||
});
|
||||
|
||||
var entries = db.ScheduleEntries.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.SchedulePlanId == plan.Id &&
|
||||
x.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == classId));
|
||||
if (studentId.HasValue)
|
||||
{
|
||||
var selectedTaskIds = db.CourseEnrollments.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.StudentId == studentId.Value &&
|
||||
x.Status == CourseEnrollmentStatus.Enrolled &&
|
||||
x.CourseSelectionOffering!.CourseSelectionRound!.AcademicTermId == term.Id)
|
||||
.Select(x => x.CourseSelectionOffering!.TeachingTaskId);
|
||||
entries = db.ScheduleEntries.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.SchedulePlanId == plan.Id &&
|
||||
(x.TeachingTask!.Classes.Any(item =>
|
||||
item.AdministrativeClassId == classId) ||
|
||||
selectedTaskIds.Contains(x.TeachingTaskId)));
|
||||
}
|
||||
|
||||
var result = await entries
|
||||
.OrderBy(x => x.DayOfWeek)
|
||||
.ThenBy(x => x.StartPeriod)
|
||||
.ThenBy(x => x.TeachingTask!.Course!.Code)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.TeachingTaskId,
|
||||
TaskNumber = x.TeachingTask!.TaskNumber,
|
||||
TaskName = x.TeachingTask.Name,
|
||||
CourseCode = x.TeachingTask.Course!.Code,
|
||||
CourseName = x.TeachingTask.Course.Name,
|
||||
TeacherNames = x.TeachingTask.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
ClassNames = x.TeachingTask.Classes
|
||||
.Select(item => item.AdministrativeClass!.Name),
|
||||
ClassroomName = x.Classroom == null ? "不占用教室" : x.Classroom.Name,
|
||||
BuildingName = x.Classroom == null ? null : x.Classroom.Building!.Name,
|
||||
CampusName = x.Classroom == null
|
||||
? null
|
||||
: x.Classroom.Building!.Campus!.Name,
|
||||
x.DayOfWeek,
|
||||
x.StartPeriod,
|
||||
x.PeriodCount,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeekPattern,
|
||||
x.Notes
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Term = term,
|
||||
Class = administrativeClass,
|
||||
Student = student,
|
||||
Plan = plan,
|
||||
Slots = slots,
|
||||
Entries = result
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Auth;
|
||||
|
||||
public sealed class PersonnelAccountService(
|
||||
AppDbContext db,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
public Task<PersonnelAccountResult> EnsureTeacherAccountAsync(
|
||||
Teacher teacher,
|
||||
string? initialPassword,
|
||||
CancellationToken cancellationToken) =>
|
||||
EnsureAccountAsync(
|
||||
teacher.Id,
|
||||
teacher.UserId,
|
||||
teacher.TeacherNumber,
|
||||
teacher.Name,
|
||||
teacher.CollegeId,
|
||||
teacher.Status == TeacherStatus.Active,
|
||||
SystemRoles.Teacher,
|
||||
initialPassword,
|
||||
userId => teacher.UserId = userId,
|
||||
cancellationToken);
|
||||
|
||||
public async Task<PersonnelAccountResult> EnsureStudentAccountAsync(
|
||||
Student student,
|
||||
string? initialPassword,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var collegeId = await db.AdministrativeClasses.AsNoTracking()
|
||||
.Where(x => x.Id == student.AdministrativeClassId)
|
||||
.Select(x => (Guid?)x.Major!.CollegeId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (!collegeId.HasValue)
|
||||
return PersonnelAccountResult.Failed("学生所在行政班或学院不存在。");
|
||||
|
||||
return await EnsureAccountAsync(
|
||||
student.Id,
|
||||
student.UserId,
|
||||
student.StudentNumber,
|
||||
student.Name,
|
||||
collegeId.Value,
|
||||
student.Status == StudentStatus.Active,
|
||||
SystemRoles.Student,
|
||||
initialPassword,
|
||||
userId => student.UserId = userId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PersonnelAccountResult> EnsureAccountAsync(
|
||||
Guid profileId,
|
||||
Guid? linkedUserId,
|
||||
string number,
|
||||
string displayName,
|
||||
Guid collegeId,
|
||||
bool isEnabled,
|
||||
string role,
|
||||
string? initialPassword,
|
||||
Action<Guid> linkProfile,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedNumber = number.Trim();
|
||||
ApplicationUser? user = null;
|
||||
if (linkedUserId.HasValue)
|
||||
user = await userManager.FindByIdAsync(linkedUserId.Value.ToString());
|
||||
user ??= await userManager.FindByNameAsync(normalizedNumber);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(initialPassword))
|
||||
return PersonnelAccountResult.Failed("请填写至少 8 位初始密码,以便同时创建登录账号。");
|
||||
|
||||
user = new ApplicationUser
|
||||
{
|
||||
UserName = normalizedNumber,
|
||||
DisplayName = displayName.Trim(),
|
||||
StaffNumber = normalizedNumber,
|
||||
CollegeId = collegeId,
|
||||
IsEnabled = isEnabled,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
var createResult = await userManager.CreateAsync(user, initialPassword);
|
||||
if (!createResult.Succeeded) return Failed(createResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
var occupied = role == SystemRoles.Teacher
|
||||
? await db.Teachers.AsNoTracking().AnyAsync(
|
||||
x =>
|
||||
x.Id != profileId &&
|
||||
x.UserId == user.Id &&
|
||||
x.TeacherNumber != normalizedNumber,
|
||||
cancellationToken)
|
||||
: await db.Students.AsNoTracking().AnyAsync(
|
||||
x =>
|
||||
x.Id != profileId &&
|
||||
x.UserId == user.Id &&
|
||||
x.StudentNumber != normalizedNumber,
|
||||
cancellationToken);
|
||||
if (occupied)
|
||||
return PersonnelAccountResult.Failed($"登录账号“{normalizedNumber}”已关联其他人员档案。");
|
||||
|
||||
user.UserName = normalizedNumber;
|
||||
user.DisplayName = displayName.Trim();
|
||||
user.StaffNumber = normalizedNumber;
|
||||
user.CollegeId = collegeId;
|
||||
user.IsEnabled = isEnabled;
|
||||
var updateResult = await userManager.UpdateAsync(user);
|
||||
if (!updateResult.Succeeded) return Failed(updateResult);
|
||||
}
|
||||
|
||||
if (!await userManager.IsInRoleAsync(user, role))
|
||||
{
|
||||
var roleResult = await userManager.AddToRoleAsync(user, role);
|
||||
if (!roleResult.Succeeded) return Failed(roleResult);
|
||||
}
|
||||
|
||||
linkProfile(user.Id);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return PersonnelAccountResult.Succeeded(user.Id, normalizedNumber);
|
||||
}
|
||||
|
||||
private static PersonnelAccountResult Failed(IdentityResult result) =>
|
||||
PersonnelAccountResult.Failed(string.Join(
|
||||
";",
|
||||
result.Errors.Select(x => x.Description)));
|
||||
}
|
||||
|
||||
public sealed record PersonnelAccountResult(
|
||||
bool Success,
|
||||
Guid? UserId,
|
||||
string? UserName,
|
||||
string? Error)
|
||||
{
|
||||
public static PersonnelAccountResult Succeeded(Guid userId, string userName) =>
|
||||
new(true, userId, userName, null);
|
||||
|
||||
public static PersonnelAccountResult Failed(string error) =>
|
||||
new(false, null, null, error);
|
||||
}
|
||||
@@ -85,6 +85,7 @@ builder.Services.Configure<JwtOptions>(
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<ICurrentUserDataScope, CurrentUserDataScope>();
|
||||
builder.Services.AddScoped<PersonnelAccountService>();
|
||||
builder.Services.AddScoped<DatabaseInitializer>();
|
||||
builder.Services.AddScoped<DevelopmentSqliteMigrator>();
|
||||
builder.Services.AddScoped<DevelopmentDemoDataSeeder>();
|
||||
|
||||
Reference in New Issue
Block a user