升级
This commit is contained in:
1 parent
d67a07f23e
commit
27c3944c28
12 files changed
+781
-14
No files matched your search
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
权限采用后端强制校验的角色与数据范围模型。多角色账号按 `All > College > Class > Self` 取最高数据范围:校级角色可访问全校数据,院系管理员限定本学院,辅导员通过稳定的账号 ID 绑定所带行政班,教师和学生限定本人及当前教学关系;前端菜单和路由限制仅作为交互辅助,不替代 API 授权。
|
权限采用后端强制校验的角色与数据范围模型。多角色账号按 `All > College > Class > Self` 取最高数据范围:校级角色可访问全校数据,院系管理员限定本学院,辅导员通过稳定的账号 ID 绑定所带行政班,教师和学生限定本人及当前教学关系;前端菜单和路由限制仅作为交互辅助,不替代 API 授权。
|
||||||
|
|
||||||
|
学生和教师档案是登录账号的人员主数据:新增或 Excel 导入档案时,系统以学号/工号自动创建同名登录账号并分配学生/教师角色,后续修改姓名、编号、学院或在籍/在职状态时同步账号。`AspNetUsers` 仅作为 ASP.NET Core Identity 的内部安全存储,负责密码哈希、登录锁定、角色和令牌,不需要再手工重复建立学生、教师用户。
|
||||||
|
|
||||||
## 本地开发:热更新模式
|
## 本地开发:热更新模式
|
||||||
|
|
||||||
本地开发固定使用 SQLite。首次启动会自动创建 `src/Jiaowu.Api/data/jiaowu-dev.sqlite` 并写入演示组织数据。
|
本地开发固定使用 SQLite。首次启动会自动创建 `src/Jiaowu.Api/data/jiaowu-dev.sqlite` 并写入演示组织数据。
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace Jiaowu.Api.Controllers;
|
|||||||
[Route("api/personnel")]
|
[Route("api/personnel")]
|
||||||
public sealed class PersonnelController(
|
public sealed class PersonnelController(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
ICurrentUserDataScope currentUserDataScope,
|
||||||
|
PersonnelAccountService personnelAccountService) : ControllerBase
|
||||||
{
|
{
|
||||||
private const string ReadRoles =
|
private const string ReadRoles =
|
||||||
SystemRoles.SuperAdmin + "," +
|
SystemRoles.SuperAdmin + "," +
|
||||||
@@ -103,7 +104,14 @@ public sealed class PersonnelController(
|
|||||||
Notes = Normalize(request.Notes)
|
Notes = Normalize(request.Notes)
|
||||||
};
|
};
|
||||||
db.Teachers.Add(entity);
|
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}")]
|
[HttpPut("teachers/{id:guid}")]
|
||||||
@@ -130,6 +138,18 @@ public sealed class PersonnelController(
|
|||||||
entity.Phone = Normalize(request.Phone);
|
entity.Phone = Normalize(request.Phone);
|
||||||
entity.Email = Normalize(request.Email);
|
entity.Email = Normalize(request.Email);
|
||||||
entity.Notes = Normalize(request.Notes);
|
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);
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +259,14 @@ public sealed class PersonnelController(
|
|||||||
Notes = Normalize(request.Notes)
|
Notes = Normalize(request.Notes)
|
||||||
};
|
};
|
||||||
db.Students.Add(entity);
|
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}")]
|
[HttpPut("students/{id:guid}")]
|
||||||
@@ -276,6 +303,18 @@ public sealed class PersonnelController(
|
|||||||
entity.Phone = Normalize(request.Phone);
|
entity.Phone = Normalize(request.Phone);
|
||||||
entity.Email = Normalize(request.Email);
|
entity.Email = Normalize(request.Email);
|
||||||
entity.Notes = Normalize(request.Notes);
|
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);
|
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)
|
private async Task<ActionResult> SaveNoContentAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -393,6 +465,14 @@ public sealed class PersonnelController(
|
|||||||
Status = StatusCodes.Status409Conflict
|
Status = StatusCodes.Status409Conflict
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private ActionResult AccountProblem(string detail) =>
|
||||||
|
Conflict(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "登录账号创建失败",
|
||||||
|
Detail = detail,
|
||||||
|
Status = StatusCodes.Status409Conflict
|
||||||
|
});
|
||||||
|
|
||||||
private static string? Normalize(string? value) =>
|
private static string? Normalize(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
@@ -422,7 +502,8 @@ public sealed record TeacherRequest(
|
|||||||
bool IsExternal,
|
bool IsExternal,
|
||||||
[MaxLength(30)] string? Phone,
|
[MaxLength(30)] string? Phone,
|
||||||
[EmailAddress, MaxLength(100)] string? Email,
|
[EmailAddress, MaxLength(100)] string? Email,
|
||||||
[MaxLength(500)] string? Notes);
|
[MaxLength(500)] string? Notes,
|
||||||
|
[MinLength(8), MaxLength(100)] string? InitialPassword = null);
|
||||||
|
|
||||||
public sealed record StudentRequest(
|
public sealed record StudentRequest(
|
||||||
[Required, MaxLength(30)] string StudentNumber,
|
[Required, MaxLength(30)] string StudentNumber,
|
||||||
@@ -435,4 +516,5 @@ public sealed record StudentRequest(
|
|||||||
DateOnly? DateOfBirth,
|
DateOnly? DateOfBirth,
|
||||||
[MaxLength(30)] string? Phone,
|
[MaxLength(30)] string? Phone,
|
||||||
[EmailAddress, MaxLength(100)] string? Email,
|
[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")]
|
[Route("api/personnel")]
|
||||||
public sealed class PersonnelExcelController(
|
public sealed class PersonnelExcelController(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
ICurrentUserDataScope currentUserDataScope,
|
||||||
|
PersonnelAccountService personnelAccountService) : ControllerBase
|
||||||
{
|
{
|
||||||
private const string ReadRoles =
|
private const string ReadRoles =
|
||||||
SystemRoles.SuperAdmin + "," +
|
SystemRoles.SuperAdmin + "," +
|
||||||
@@ -32,13 +33,13 @@ public sealed class PersonnelExcelController(
|
|||||||
private static readonly string[] TeacherHeaders =
|
private static readonly string[] TeacherHeaders =
|
||||||
[
|
[
|
||||||
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
|
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
|
||||||
"入职日期", "教师类别", "联系电话", "电子邮箱", "备注"
|
"入职日期", "教师类别", "联系电话", "电子邮箱", "备注", "初始密码"
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] StudentHeaders =
|
private static readonly string[] StudentHeaders =
|
||||||
[
|
[
|
||||||
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
|
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
|
||||||
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注"
|
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注", "初始密码"
|
||||||
];
|
];
|
||||||
|
|
||||||
[HttpGet("{kind}/template")]
|
[HttpGet("{kind}/template")]
|
||||||
@@ -56,6 +57,7 @@ public sealed class PersonnelExcelController(
|
|||||||
? "工号是唯一标识;学院编码必须已存在。"
|
? "工号是唯一标识;学院编码必须已存在。"
|
||||||
: "学号是唯一标识;行政班编码必须已存在。",
|
: "学号是唯一标识;行政班编码必须已存在。",
|
||||||
"编号已存在时更新档案,不存在时新增档案。",
|
"编号已存在时更新档案,不存在时新增档案。",
|
||||||
|
"新增人员或补建账号时必须填写至少 8 位初始密码;已有关联账号时可留空。",
|
||||||
"日期填写为 yyyy-MM-dd;不适用的可选字段可以留空。",
|
"日期填写为 yyyy-MM-dd;不适用的可选字段可以留空。",
|
||||||
"整批数据会先校验,任一行有误时均不会写入。"
|
"整批数据会先校验,任一行有误时均不会写入。"
|
||||||
]);
|
]);
|
||||||
@@ -81,7 +83,7 @@ public sealed class PersonnelExcelController(
|
|||||||
.Select(x => Row(
|
.Select(x => Row(
|
||||||
x.TeacherNumber, x.Name, GenderName(x.Gender), x.College!.Code,
|
x.TeacherNumber, x.Name, GenderName(x.Gender), x.College!.Code,
|
||||||
x.Title, TeacherStatusName(x.Status), x.HireDate,
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -94,7 +96,7 @@ public sealed class PersonnelExcelController(
|
|||||||
.Select(x => Row(
|
.Select(x => Row(
|
||||||
x.StudentNumber, x.Name, GenderName(x.Gender),
|
x.StudentNumber, x.Name, GenderName(x.Gender),
|
||||||
x.AdministrativeClass!.Code, x.EnrollmentYear, x.EnrollmentDate,
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +214,12 @@ public sealed class PersonnelExcelController(
|
|||||||
entity.Phone = Optional(row, "联系电话");
|
entity.Phone = Optional(row, "联系电话");
|
||||||
entity.Email = Optional(row, "电子邮箱");
|
entity.Email = Optional(row, "电子邮箱");
|
||||||
entity.Notes = 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);
|
return new(created, updated, rows.Count);
|
||||||
}
|
}
|
||||||
@@ -284,6 +292,12 @@ public sealed class PersonnelExcelController(
|
|||||||
entity.Phone = Optional(row, "联系电话");
|
entity.Phone = Optional(row, "联系电话");
|
||||||
entity.Email = Optional(row, "电子邮箱");
|
entity.Email = Optional(row, "电子邮箱");
|
||||||
entity.Notes = 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);
|
return new(created, updated, rows.Count);
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user