2.3.0-rc2

This commit is contained in:
2026-08-04 10:48:08 +08:00 Unverified
parent 6735a6d3fc
commit 10b51eb08a
19 changed files with 7292 additions and 7 deletions
@@ -1639,13 +1639,25 @@ public sealed class CourseSelectionsController(
var students = await LoadTeachingTaskRosterAsync(id, cancellationToken);
var bytes = ExcelWorkbookHelper.Create(
"教学班名单",
["学号", "姓名", "班级", "专业", "进入方式", "选课时间"],
[
"学号", "姓名", "班级", "专业", "联系电话", "电子邮箱",
"微信", "紧急联系人", "与本人关系", "紧急联系电话",
"特殊标记", "特殊情况说明", "进入方式", "选课时间"
],
students.Select(student => new List<object?>
{
student.StudentNumber,
student.Name,
student.ClassName,
student.MajorName,
student.Phone,
student.Email,
student.WeChat,
student.EmergencyContactName,
student.EmergencyContactRelationship,
student.EmergencyContactPhone,
student.SpecialTags,
student.SpecialNeeds,
student.EnrolledAt.HasValue ? "选课" : "行政班关联",
student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-"
}).ToList<IReadOnlyList<object?>>());
@@ -1754,6 +1766,14 @@ public sealed class CourseSelectionsController(
x.Name,
x.AdministrativeClass!.Name,
x.AdministrativeClass.Major!.Name,
x.Phone,
x.Email,
x.WeChat,
x.EmergencyContactName,
x.EmergencyContactRelationship,
x.EmergencyContactPhone,
x.SpecialTags,
x.SpecialNeeds,
db.CourseEnrollments
.Where(enrollment =>
enrollment.StudentId == x.Id &&
@@ -2154,6 +2174,14 @@ public sealed class CourseSelectionsController(
string Name,
string ClassName,
string MajorName,
string? Phone,
string? Email,
string? WeChat,
string? EmergencyContactName,
string? EmergencyContactRelationship,
string? EmergencyContactPhone,
string? SpecialTags,
string? SpecialNeeds,
DateTime? EnrolledAt);
}
@@ -224,6 +224,12 @@ public sealed class PersonnelController(
[FromQuery] PersonnelQuery query,
CancellationToken cancellationToken)
{
var roles = currentUserDataScope.Current.Roles;
var canReadFullProfile = roles.Any(role =>
role is SystemRoles.SuperAdmin or
SystemRoles.AcademicAdmin or
SystemRoles.CollegeAdmin or
SystemRoles.Counselor);
var page = NormalizePage(query.Page);
var pageSize = NormalizePageSize(query.PageSize);
var source = ApplyStudentScope(db.Students.AsNoTracking());
@@ -268,9 +274,26 @@ public sealed class PersonnelController(
x.EnrollmentDate,
x.Status,
x.DateOfBirth,
EnglishName = canReadFullProfile ? x.EnglishName : null,
IdCardNumber = canReadFullProfile ? x.IdCardNumber : null,
Nationality = canReadFullProfile ? x.Nationality : null,
Ethnicity = canReadFullProfile ? x.Ethnicity : null,
PoliticalStatus = canReadFullProfile ? x.PoliticalStatus : null,
NativePlace = canReadFullProfile ? x.NativePlace : null,
HouseholdAddress = canReadFullProfile ? x.HouseholdAddress : null,
CurrentAddress = canReadFullProfile ? x.CurrentAddress : null,
PostalCode = canReadFullProfile ? x.PostalCode : null,
x.Phone,
x.Email,
x.Notes,
Qq = canReadFullProfile ? x.Qq : null,
x.WeChat,
x.EmergencyContactName,
x.EmergencyContactRelationship,
x.EmergencyContactPhone,
x.SpecialTags,
x.SpecialNeeds,
Biography = canReadFullProfile ? x.Biography : null,
Notes = canReadFullProfile ? x.Notes : null,
x.UserId,
x.CreatedAt
})
@@ -304,8 +327,26 @@ public sealed class PersonnelController(
EnrollmentDate = request.EnrollmentDate,
Status = request.Status,
DateOfBirth = request.DateOfBirth,
EnglishName = Normalize(request.EnglishName),
IdCardNumber = Normalize(request.IdCardNumber),
Nationality = Normalize(request.Nationality),
Ethnicity = Normalize(request.Ethnicity),
PoliticalStatus = Normalize(request.PoliticalStatus),
NativePlace = Normalize(request.NativePlace),
HouseholdAddress = Normalize(request.HouseholdAddress),
CurrentAddress = Normalize(request.CurrentAddress),
PostalCode = Normalize(request.PostalCode),
Phone = Normalize(request.Phone),
Email = Normalize(request.Email),
Qq = Normalize(request.Qq),
WeChat = Normalize(request.WeChat),
EmergencyContactName = Normalize(request.EmergencyContactName),
EmergencyContactRelationship = Normalize(
request.EmergencyContactRelationship),
EmergencyContactPhone = Normalize(request.EmergencyContactPhone),
SpecialTags = Normalize(request.SpecialTags),
SpecialNeeds = Normalize(request.SpecialNeeds),
Biography = Normalize(request.Biography),
Notes = Normalize(request.Notes)
};
db.Students.Add(entity);
@@ -343,8 +384,26 @@ public sealed class PersonnelController(
entity.EnrollmentDate = request.EnrollmentDate;
entity.Status = request.Status;
entity.DateOfBirth = request.DateOfBirth;
entity.EnglishName = Normalize(request.EnglishName);
entity.IdCardNumber = Normalize(request.IdCardNumber);
entity.Nationality = Normalize(request.Nationality);
entity.Ethnicity = Normalize(request.Ethnicity);
entity.PoliticalStatus = Normalize(request.PoliticalStatus);
entity.NativePlace = Normalize(request.NativePlace);
entity.HouseholdAddress = Normalize(request.HouseholdAddress);
entity.CurrentAddress = Normalize(request.CurrentAddress);
entity.PostalCode = Normalize(request.PostalCode);
entity.Phone = Normalize(request.Phone);
entity.Email = Normalize(request.Email);
entity.Qq = Normalize(request.Qq);
entity.WeChat = Normalize(request.WeChat);
entity.EmergencyContactName = Normalize(request.EmergencyContactName);
entity.EmergencyContactRelationship = Normalize(
request.EmergencyContactRelationship);
entity.EmergencyContactPhone = Normalize(request.EmergencyContactPhone);
entity.SpecialTags = Normalize(request.SpecialTags);
entity.SpecialNeeds = Normalize(request.SpecialNeeds);
entity.Biography = Normalize(request.Biography);
entity.Notes = Normalize(request.Notes);
return await SaveNoContentAsync(cancellationToken);
}
@@ -522,8 +581,25 @@ public sealed record StudentRequest(
DateOnly EnrollmentDate,
StudentStatus Status,
DateOnly? DateOfBirth,
[MaxLength(100)] string? EnglishName,
[MaxLength(30)] string? IdCardNumber,
[MaxLength(50)] string? Nationality,
[MaxLength(50)] string? Ethnicity,
[MaxLength(50)] string? PoliticalStatus,
[MaxLength(100)] string? NativePlace,
[MaxLength(300)] string? HouseholdAddress,
[MaxLength(300)] string? CurrentAddress,
[MaxLength(20)] string? PostalCode,
[MaxLength(30)] string? Phone,
[EmailAddress, MaxLength(100)] string? Email,
[MaxLength(30)] string? Qq,
[MaxLength(60)] string? WeChat,
[MaxLength(50)] string? EmergencyContactName,
[MaxLength(30)] string? EmergencyContactRelationship,
[MaxLength(30)] string? EmergencyContactPhone,
[MaxLength(300)] string? SpecialTags,
[MaxLength(1000)] string? SpecialNeeds,
[MaxLength(1000)] string? Biography,
[MaxLength(500)] string? Notes);
public sealed record TeacherAccountActivationRequest(
@@ -31,6 +31,9 @@ public sealed class PersonnelExcelController(
SystemRoles.AcademicAdmin + "," +
SystemRoles.CollegeAdmin;
private const string ExportRoles =
WriteRoles + "," + SystemRoles.Counselor;
private static readonly string[] TeacherHeaders =
[
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
@@ -40,7 +43,10 @@ public sealed class PersonnelExcelController(
private static readonly string[] StudentHeaders =
[
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注"
"学籍状态", "出生日期", "英文姓名", "证件号码", "国籍", "民族",
"政治面貌", "籍贯", "户籍地址", "现居住地址", "邮政编码",
"联系电话", "电子邮箱", "QQ", "微信", "紧急联系人", "与本人关系",
"紧急联系电话", "特殊标记", "特殊情况说明", "个人简介", "备注"
];
[HttpGet("{kind}/template")]
@@ -66,6 +72,7 @@ public sealed class PersonnelExcelController(
}
[HttpGet("{kind}/export")]
[Authorize(Roles = ExportRoles)]
public async Task<IActionResult> Export(
string kind,
[FromQuery] PersonnelQuery query,
@@ -96,7 +103,12 @@ 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.EnglishName,
x.IdCardNumber, x.Nationality, x.Ethnicity, x.PoliticalStatus,
x.NativePlace, x.HouseholdAddress, x.CurrentAddress, x.PostalCode,
x.Phone, x.Email, x.Qq, x.WeChat, x.EmergencyContactName,
x.EmergencyContactRelationship, x.EmergencyContactPhone,
x.SpecialTags, x.SpecialNeeds, x.Biography, x.Notes))
.ToList();
}
@@ -296,8 +308,25 @@ public sealed class PersonnelExcelController(
entity.EnrollmentDate = enrollmentDate.Value;
entity.Status = status.Value;
entity.DateOfBirth = dateOfBirth;
entity.EnglishName = Optional(row, "英文姓名");
entity.IdCardNumber = Optional(row, "证件号码");
entity.Nationality = Optional(row, "国籍");
entity.Ethnicity = Optional(row, "民族");
entity.PoliticalStatus = Optional(row, "政治面貌");
entity.NativePlace = Optional(row, "籍贯");
entity.HouseholdAddress = Optional(row, "户籍地址");
entity.CurrentAddress = Optional(row, "现居住地址");
entity.PostalCode = Optional(row, "邮政编码");
entity.Phone = Optional(row, "联系电话");
entity.Email = Optional(row, "电子邮箱");
entity.Qq = Optional(row, "QQ");
entity.WeChat = Optional(row, "微信");
entity.EmergencyContactName = Optional(row, "紧急联系人");
entity.EmergencyContactRelationship = Optional(row, "与本人关系");
entity.EmergencyContactPhone = Optional(row, "紧急联系电话");
entity.SpecialTags = Optional(row, "特殊标记");
entity.SpecialNeeds = Optional(row, "特殊情况说明");
entity.Biography = Optional(row, "个人简介");
entity.Notes = Optional(row, "备注");
}
return new(created, updated, rows.Count);
@@ -0,0 +1,158 @@
using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize(Roles = SystemRoles.Student)]
[Route("api/student/profile")]
public sealed class StudentProfileController(
AppDbContext db,
ICurrentUserDataScope currentUserDataScope,
IAppCache cache) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<StudentProfileDto>> Get(
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var profile = await db.Students.AsNoTracking()
.Where(x => x.UserId == userId)
.Select(x => new StudentProfileDto(
x.StudentNumber,
x.Name,
x.AdministrativeClass!.Name,
x.AdministrativeClass.Major!.Name,
x.AdministrativeClass.Major.College!.Name,
x.EnrollmentYear,
x.EnrollmentDate,
x.Status,
x.Gender,
x.DateOfBirth,
x.EnglishName,
x.IdCardNumber,
x.Nationality,
x.Ethnicity,
x.PoliticalStatus,
x.NativePlace,
x.HouseholdAddress,
x.CurrentAddress,
x.PostalCode,
x.Phone,
x.Email,
x.Qq,
x.WeChat,
x.EmergencyContactName,
x.EmergencyContactRelationship,
x.EmergencyContactPhone,
x.SpecialTags,
x.SpecialNeeds,
x.Biography))
.SingleOrDefaultAsync(cancellationToken);
return profile is null ? NotFound() : Ok(profile);
}
[HttpPut]
public async Task<IActionResult> Update(
StudentProfileUpdateRequest request,
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
var student = await db.Students.SingleOrDefaultAsync(
x => x.UserId == userId,
cancellationToken);
if (student is null) return NotFound();
student.Gender = request.Gender;
student.DateOfBirth = request.DateOfBirth;
student.EnglishName = Normalize(request.EnglishName);
student.IdCardNumber = Normalize(request.IdCardNumber);
student.Nationality = Normalize(request.Nationality);
student.Ethnicity = Normalize(request.Ethnicity);
student.PoliticalStatus = Normalize(request.PoliticalStatus);
student.NativePlace = Normalize(request.NativePlace);
student.HouseholdAddress = Normalize(request.HouseholdAddress);
student.CurrentAddress = Normalize(request.CurrentAddress);
student.PostalCode = Normalize(request.PostalCode);
student.Phone = Normalize(request.Phone);
student.Email = Normalize(request.Email);
student.Qq = Normalize(request.Qq);
student.WeChat = Normalize(request.WeChat);
student.EmergencyContactName = Normalize(request.EmergencyContactName);
student.EmergencyContactRelationship = Normalize(
request.EmergencyContactRelationship);
student.EmergencyContactPhone = Normalize(request.EmergencyContactPhone);
student.SpecialTags = Normalize(request.SpecialTags);
student.SpecialNeeds = Normalize(request.SpecialNeeds);
student.Biography = Normalize(request.Biography);
await db.SaveChangesAsync(cancellationToken);
await cache.RemoveByTagAsync(AppCacheTags.Analytics, cancellationToken);
return NoContent();
}
private static string? Normalize(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
public sealed record StudentProfileDto(
string StudentNumber,
string Name,
string ClassName,
string MajorName,
string CollegeName,
int EnrollmentYear,
DateOnly EnrollmentDate,
StudentStatus Status,
Gender Gender,
DateOnly? DateOfBirth,
string? EnglishName,
string? IdCardNumber,
string? Nationality,
string? Ethnicity,
string? PoliticalStatus,
string? NativePlace,
string? HouseholdAddress,
string? CurrentAddress,
string? PostalCode,
string? Phone,
string? Email,
string? Qq,
string? WeChat,
string? EmergencyContactName,
string? EmergencyContactRelationship,
string? EmergencyContactPhone,
string? SpecialTags,
string? SpecialNeeds,
string? Biography);
public sealed record StudentProfileUpdateRequest(
Gender Gender,
DateOnly? DateOfBirth,
[MaxLength(100)] string? EnglishName,
[MaxLength(30)] string? IdCardNumber,
[MaxLength(50)] string? Nationality,
[MaxLength(50)] string? Ethnicity,
[MaxLength(50)] string? PoliticalStatus,
[MaxLength(100)] string? NativePlace,
[MaxLength(300)] string? HouseholdAddress,
[MaxLength(300)] string? CurrentAddress,
[MaxLength(20)] string? PostalCode,
[MaxLength(30)] string? Phone,
[EmailAddress, MaxLength(100)] string? Email,
[MaxLength(30)] string? Qq,
[MaxLength(60)] string? WeChat,
[MaxLength(50)] string? EmergencyContactName,
[MaxLength(30)] string? EmergencyContactRelationship,
[MaxLength(30)] string? EmergencyContactPhone,
[MaxLength(300)] string? SpecialTags,
[MaxLength(1000)] string? SpecialNeeds,
[MaxLength(1000)] string? Biography);