2.3.0-rc2
This commit is contained in:
@@ -1639,13 +1639,25 @@ public sealed class CourseSelectionsController(
|
|||||||
var students = await LoadTeachingTaskRosterAsync(id, cancellationToken);
|
var students = await LoadTeachingTaskRosterAsync(id, cancellationToken);
|
||||||
var bytes = ExcelWorkbookHelper.Create(
|
var bytes = ExcelWorkbookHelper.Create(
|
||||||
"教学班名单",
|
"教学班名单",
|
||||||
["学号", "姓名", "班级", "专业", "进入方式", "选课时间"],
|
[
|
||||||
|
"学号", "姓名", "班级", "专业", "联系电话", "电子邮箱",
|
||||||
|
"微信", "紧急联系人", "与本人关系", "紧急联系电话",
|
||||||
|
"特殊标记", "特殊情况说明", "进入方式", "选课时间"
|
||||||
|
],
|
||||||
students.Select(student => new List<object?>
|
students.Select(student => new List<object?>
|
||||||
{
|
{
|
||||||
student.StudentNumber,
|
student.StudentNumber,
|
||||||
student.Name,
|
student.Name,
|
||||||
student.ClassName,
|
student.ClassName,
|
||||||
student.MajorName,
|
student.MajorName,
|
||||||
|
student.Phone,
|
||||||
|
student.Email,
|
||||||
|
student.WeChat,
|
||||||
|
student.EmergencyContactName,
|
||||||
|
student.EmergencyContactRelationship,
|
||||||
|
student.EmergencyContactPhone,
|
||||||
|
student.SpecialTags,
|
||||||
|
student.SpecialNeeds,
|
||||||
student.EnrolledAt.HasValue ? "选课" : "行政班关联",
|
student.EnrolledAt.HasValue ? "选课" : "行政班关联",
|
||||||
student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-"
|
student.EnrolledAt?.ToString("yyyy-MM-dd HH:mm") ?? "-"
|
||||||
}).ToList<IReadOnlyList<object?>>());
|
}).ToList<IReadOnlyList<object?>>());
|
||||||
@@ -1754,6 +1766,14 @@ public sealed class CourseSelectionsController(
|
|||||||
x.Name,
|
x.Name,
|
||||||
x.AdministrativeClass!.Name,
|
x.AdministrativeClass!.Name,
|
||||||
x.AdministrativeClass.Major!.Name,
|
x.AdministrativeClass.Major!.Name,
|
||||||
|
x.Phone,
|
||||||
|
x.Email,
|
||||||
|
x.WeChat,
|
||||||
|
x.EmergencyContactName,
|
||||||
|
x.EmergencyContactRelationship,
|
||||||
|
x.EmergencyContactPhone,
|
||||||
|
x.SpecialTags,
|
||||||
|
x.SpecialNeeds,
|
||||||
db.CourseEnrollments
|
db.CourseEnrollments
|
||||||
.Where(enrollment =>
|
.Where(enrollment =>
|
||||||
enrollment.StudentId == x.Id &&
|
enrollment.StudentId == x.Id &&
|
||||||
@@ -2154,6 +2174,14 @@ public sealed class CourseSelectionsController(
|
|||||||
string Name,
|
string Name,
|
||||||
string ClassName,
|
string ClassName,
|
||||||
string MajorName,
|
string MajorName,
|
||||||
|
string? Phone,
|
||||||
|
string? Email,
|
||||||
|
string? WeChat,
|
||||||
|
string? EmergencyContactName,
|
||||||
|
string? EmergencyContactRelationship,
|
||||||
|
string? EmergencyContactPhone,
|
||||||
|
string? SpecialTags,
|
||||||
|
string? SpecialNeeds,
|
||||||
DateTime? EnrolledAt);
|
DateTime? EnrolledAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,12 @@ public sealed class PersonnelController(
|
|||||||
[FromQuery] PersonnelQuery query,
|
[FromQuery] PersonnelQuery query,
|
||||||
CancellationToken cancellationToken)
|
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 page = NormalizePage(query.Page);
|
||||||
var pageSize = NormalizePageSize(query.PageSize);
|
var pageSize = NormalizePageSize(query.PageSize);
|
||||||
var source = ApplyStudentScope(db.Students.AsNoTracking());
|
var source = ApplyStudentScope(db.Students.AsNoTracking());
|
||||||
@@ -268,9 +274,26 @@ public sealed class PersonnelController(
|
|||||||
x.EnrollmentDate,
|
x.EnrollmentDate,
|
||||||
x.Status,
|
x.Status,
|
||||||
x.DateOfBirth,
|
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.Phone,
|
||||||
x.Email,
|
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.UserId,
|
||||||
x.CreatedAt
|
x.CreatedAt
|
||||||
})
|
})
|
||||||
@@ -304,8 +327,26 @@ public sealed class PersonnelController(
|
|||||||
EnrollmentDate = request.EnrollmentDate,
|
EnrollmentDate = request.EnrollmentDate,
|
||||||
Status = request.Status,
|
Status = request.Status,
|
||||||
DateOfBirth = request.DateOfBirth,
|
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),
|
Phone = Normalize(request.Phone),
|
||||||
Email = Normalize(request.Email),
|
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)
|
Notes = Normalize(request.Notes)
|
||||||
};
|
};
|
||||||
db.Students.Add(entity);
|
db.Students.Add(entity);
|
||||||
@@ -343,8 +384,26 @@ public sealed class PersonnelController(
|
|||||||
entity.EnrollmentDate = request.EnrollmentDate;
|
entity.EnrollmentDate = request.EnrollmentDate;
|
||||||
entity.Status = request.Status;
|
entity.Status = request.Status;
|
||||||
entity.DateOfBirth = request.DateOfBirth;
|
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.Phone = Normalize(request.Phone);
|
||||||
entity.Email = Normalize(request.Email);
|
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);
|
entity.Notes = Normalize(request.Notes);
|
||||||
return await SaveNoContentAsync(cancellationToken);
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -522,8 +581,25 @@ public sealed record StudentRequest(
|
|||||||
DateOnly EnrollmentDate,
|
DateOnly EnrollmentDate,
|
||||||
StudentStatus Status,
|
StudentStatus Status,
|
||||||
DateOnly? DateOfBirth,
|
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,
|
[MaxLength(30)] string? Phone,
|
||||||
[EmailAddress, MaxLength(100)] string? Email,
|
[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);
|
[MaxLength(500)] string? Notes);
|
||||||
|
|
||||||
public sealed record TeacherAccountActivationRequest(
|
public sealed record TeacherAccountActivationRequest(
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public sealed class PersonnelExcelController(
|
|||||||
SystemRoles.AcademicAdmin + "," +
|
SystemRoles.AcademicAdmin + "," +
|
||||||
SystemRoles.CollegeAdmin;
|
SystemRoles.CollegeAdmin;
|
||||||
|
|
||||||
|
private const string ExportRoles =
|
||||||
|
WriteRoles + "," + SystemRoles.Counselor;
|
||||||
|
|
||||||
private static readonly string[] TeacherHeaders =
|
private static readonly string[] TeacherHeaders =
|
||||||
[
|
[
|
||||||
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
|
"工号", "姓名", "性别", "学院编码", "职称", "任职状态",
|
||||||
@@ -40,7 +43,10 @@ public sealed class PersonnelExcelController(
|
|||||||
private static readonly string[] StudentHeaders =
|
private static readonly string[] StudentHeaders =
|
||||||
[
|
[
|
||||||
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
|
"学号", "姓名", "性别", "行政班编码", "入学年级", "入学日期",
|
||||||
"学籍状态", "出生日期", "联系电话", "电子邮箱", "备注"
|
"学籍状态", "出生日期", "英文姓名", "证件号码", "国籍", "民族",
|
||||||
|
"政治面貌", "籍贯", "户籍地址", "现居住地址", "邮政编码",
|
||||||
|
"联系电话", "电子邮箱", "QQ", "微信", "紧急联系人", "与本人关系",
|
||||||
|
"紧急联系电话", "特殊标记", "特殊情况说明", "个人简介", "备注"
|
||||||
];
|
];
|
||||||
|
|
||||||
[HttpGet("{kind}/template")]
|
[HttpGet("{kind}/template")]
|
||||||
@@ -66,6 +72,7 @@ public sealed class PersonnelExcelController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{kind}/export")]
|
[HttpGet("{kind}/export")]
|
||||||
|
[Authorize(Roles = ExportRoles)]
|
||||||
public async Task<IActionResult> Export(
|
public async Task<IActionResult> Export(
|
||||||
string kind,
|
string kind,
|
||||||
[FromQuery] PersonnelQuery query,
|
[FromQuery] PersonnelQuery query,
|
||||||
@@ -96,7 +103,12 @@ 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.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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +308,25 @@ public sealed class PersonnelExcelController(
|
|||||||
entity.EnrollmentDate = enrollmentDate.Value;
|
entity.EnrollmentDate = enrollmentDate.Value;
|
||||||
entity.Status = status.Value;
|
entity.Status = status.Value;
|
||||||
entity.DateOfBirth = dateOfBirth;
|
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.Phone = Optional(row, "联系电话");
|
||||||
entity.Email = 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, "备注");
|
entity.Notes = Optional(row, "备注");
|
||||||
}
|
}
|
||||||
return new(created, updated, rows.Count);
|
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);
|
||||||
@@ -30,8 +30,25 @@ public sealed class Student : EntityBase
|
|||||||
public DateOnly EnrollmentDate { get; set; }
|
public DateOnly EnrollmentDate { get; set; }
|
||||||
public StudentStatus Status { get; set; } = StudentStatus.Active;
|
public StudentStatus Status { get; set; } = StudentStatus.Active;
|
||||||
public DateOnly? DateOfBirth { get; set; }
|
public DateOnly? DateOfBirth { get; set; }
|
||||||
|
public string? EnglishName { get; set; }
|
||||||
|
public string? IdCardNumber { get; set; }
|
||||||
|
public string? Nationality { get; set; }
|
||||||
|
public string? Ethnicity { get; set; }
|
||||||
|
public string? PoliticalStatus { get; set; }
|
||||||
|
public string? NativePlace { get; set; }
|
||||||
|
public string? HouseholdAddress { get; set; }
|
||||||
|
public string? CurrentAddress { get; set; }
|
||||||
|
public string? PostalCode { get; set; }
|
||||||
public string? Phone { get; set; }
|
public string? Phone { get; set; }
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
public string? Qq { get; set; }
|
||||||
|
public string? WeChat { get; set; }
|
||||||
|
public string? EmergencyContactName { get; set; }
|
||||||
|
public string? EmergencyContactRelationship { get; set; }
|
||||||
|
public string? EmergencyContactPhone { get; set; }
|
||||||
|
public string? SpecialTags { get; set; }
|
||||||
|
public string? SpecialNeeds { get; set; }
|
||||||
|
public string? Biography { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,8 +257,25 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
{
|
{
|
||||||
entity.Property(x => x.StudentNumber).HasMaxLength(30);
|
entity.Property(x => x.StudentNumber).HasMaxLength(30);
|
||||||
entity.Property(x => x.Name).HasMaxLength(50);
|
entity.Property(x => x.Name).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.EnglishName).HasMaxLength(100);
|
||||||
|
entity.Property(x => x.IdCardNumber).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.Nationality).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.Ethnicity).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.PoliticalStatus).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.NativePlace).HasMaxLength(100);
|
||||||
|
entity.Property(x => x.HouseholdAddress).HasMaxLength(300);
|
||||||
|
entity.Property(x => x.CurrentAddress).HasMaxLength(300);
|
||||||
|
entity.Property(x => x.PostalCode).HasMaxLength(20);
|
||||||
entity.Property(x => x.Phone).HasMaxLength(30);
|
entity.Property(x => x.Phone).HasMaxLength(30);
|
||||||
entity.Property(x => x.Email).HasMaxLength(100);
|
entity.Property(x => x.Email).HasMaxLength(100);
|
||||||
|
entity.Property(x => x.Qq).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.WeChat).HasMaxLength(60);
|
||||||
|
entity.Property(x => x.EmergencyContactName).HasMaxLength(50);
|
||||||
|
entity.Property(x => x.EmergencyContactRelationship).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.EmergencyContactPhone).HasMaxLength(30);
|
||||||
|
entity.Property(x => x.SpecialTags).HasMaxLength(300);
|
||||||
|
entity.Property(x => x.SpecialNeeds).HasMaxLength(1000);
|
||||||
|
entity.Property(x => x.Biography).HasMaxLength(1000);
|
||||||
entity.Property(x => x.Notes).HasMaxLength(500);
|
entity.Property(x => x.Notes).HasMaxLength(500);
|
||||||
entity.HasIndex(x => x.StudentNumber).IsUnique();
|
entity.HasIndex(x => x.StudentNumber).IsUnique();
|
||||||
entity.HasIndex(x => new { x.AdministrativeClassId, x.Status });
|
entity.HasIndex(x => new { x.AdministrativeClassId, x.Status });
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
"20260802_42_integrated_experiment_scheduling";
|
"20260802_42_integrated_experiment_scheduling";
|
||||||
private const string RefreshSessionsMigration =
|
private const string RefreshSessionsMigration =
|
||||||
"20260803_43_refresh_sessions";
|
"20260803_43_refresh_sessions";
|
||||||
|
private const string StudentPersonalProfileMigration =
|
||||||
|
"20260803_44_student_personal_profile";
|
||||||
|
|
||||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -599,6 +601,18 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
RefreshSessionsMigration,
|
RefreshSessionsMigration,
|
||||||
RefreshSessionsStatements,
|
RefreshSessionsStatements,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
var studentPersonalProfileExists = await db.Database
|
||||||
|
.SqlQueryRaw<int>(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS "Value"
|
||||||
|
FROM pragma_table_info('Students')
|
||||||
|
WHERE name = 'SpecialTags'
|
||||||
|
""")
|
||||||
|
.AnyAsync(value => value > 0, cancellationToken);
|
||||||
|
await ApplyMigrationAsync(
|
||||||
|
StudentPersonalProfileMigration,
|
||||||
|
studentPersonalProfileExists ? [] : StudentPersonalProfileStatements,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyMigrationAsync(
|
private async Task ApplyMigrationAsync(
|
||||||
@@ -2138,6 +2152,27 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
"""
|
"""
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly string[] StudentPersonalProfileStatements =
|
||||||
|
[
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "Biography" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "CurrentAddress" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "EmergencyContactName" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "EmergencyContactPhone" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "EmergencyContactRelationship" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "EnglishName" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "Ethnicity" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "HouseholdAddress" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "IdCardNumber" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "Nationality" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "NativePlace" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "PoliticalStatus" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "PostalCode" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "Qq" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "SpecialNeeds" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "SpecialTags" TEXT NULL;""",
|
||||||
|
"""ALTER TABLE "Students" ADD COLUMN "WeChat" TEXT NULL;"""
|
||||||
|
];
|
||||||
|
|
||||||
private static readonly string[] ApprovalTableStatements =
|
private static readonly string[] ApprovalTableStatements =
|
||||||
[
|
[
|
||||||
"""CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""",
|
"""CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""",
|
||||||
|
|||||||
+6120
File diff suppressed because it is too large
Load Diff
+205
@@ -0,0 +1,205 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class StudentPersonalProfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Biography",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(1000)",
|
||||||
|
maxLength: 1000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CurrentAddress",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(300)",
|
||||||
|
maxLength: 300,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EmergencyContactName",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EmergencyContactPhone",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(30)",
|
||||||
|
maxLength: 30,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EmergencyContactRelationship",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(30)",
|
||||||
|
maxLength: 30,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "EnglishName",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Ethnicity",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "HouseholdAddress",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(300)",
|
||||||
|
maxLength: 300,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "IdCardNumber",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(30)",
|
||||||
|
maxLength: 30,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Nationality",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "NativePlace",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(100)",
|
||||||
|
maxLength: 100,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PoliticalStatus",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PostalCode",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(20)",
|
||||||
|
maxLength: 20,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Qq",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(30)",
|
||||||
|
maxLength: 30,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SpecialNeeds",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(1000)",
|
||||||
|
maxLength: 1000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SpecialTags",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(300)",
|
||||||
|
maxLength: 300,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WeChat",
|
||||||
|
table: "Students",
|
||||||
|
type: "varchar(60)",
|
||||||
|
maxLength: 60,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Biography",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CurrentAddress",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EmergencyContactName",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EmergencyContactPhone",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EmergencyContactRelationship",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EnglishName",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Ethnicity",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "HouseholdAddress",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IdCardNumber",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Nationality",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "NativePlace",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PoliticalStatus",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PostalCode",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Qq",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SpecialNeeds",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SpecialTags",
|
||||||
|
table: "Students");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WeChat",
|
||||||
|
table: "Students");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -3492,9 +3492,17 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<Guid>("AdministrativeClassId")
|
b.Property<Guid>("AdministrativeClassId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Biography")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("CurrentAddress")
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
b.Property<DateTime?>("DateOfBirth")
|
b.Property<DateTime?>("DateOfBirth")
|
||||||
.HasColumnType("date");
|
.HasColumnType("date");
|
||||||
|
|
||||||
@@ -3502,20 +3510,56 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("varchar(100)");
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("EmergencyContactName")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("EmergencyContactPhone")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("EmergencyContactRelationship")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("EnglishName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
b.Property<DateTime>("EnrollmentDate")
|
b.Property<DateTime>("EnrollmentDate")
|
||||||
.HasColumnType("date");
|
.HasColumnType("date");
|
||||||
|
|
||||||
b.Property<int>("EnrollmentYear")
|
b.Property<int>("EnrollmentYear")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Ethnicity")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
b.Property<int>("Gender")
|
b.Property<int>("Gender")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("HouseholdAddress")
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<string>("IdCardNumber")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("varchar(50)");
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Nationality")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("NativePlace")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
b.Property<string>("Notes")
|
b.Property<string>("Notes")
|
||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("varchar(500)");
|
.HasColumnType("varchar(500)");
|
||||||
@@ -3524,6 +3568,26 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
.HasMaxLength(30)
|
.HasMaxLength(30)
|
||||||
.HasColumnType("varchar(30)");
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("PoliticalStatus")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("PostalCode")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Qq")
|
||||||
|
.HasMaxLength(30)
|
||||||
|
.HasColumnType("varchar(30)");
|
||||||
|
|
||||||
|
b.Property<string>("SpecialNeeds")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("SpecialTags")
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
b.Property<int>("Status")
|
b.Property<int>("Status")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -3538,6 +3602,10 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<Guid?>("UserId")
|
b.Property<Guid?>("UserId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("WeChat")
|
||||||
|
.HasMaxLength(60)
|
||||||
|
.HasColumnType("varchar(60)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("EnrollmentYear");
|
b.HasIndex("EnrollmentYear");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>2.3.0</Version>
|
<Version>2.3.0-rc2</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
|
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using Jiaowu.Api.Controllers;
|
||||||
|
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.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class StudentProfileControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Student_can_update_profile_without_changing_identity_or_registration()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
await using var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var user = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserName = "2026001",
|
||||||
|
NormalizedUserName = "2026001",
|
||||||
|
DisplayName = "原姓名"
|
||||||
|
};
|
||||||
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
|
var major = new Major
|
||||||
|
{
|
||||||
|
Code = "080901",
|
||||||
|
Name = "计算机科学与技术",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
DegreeType = "工学学士"
|
||||||
|
};
|
||||||
|
var administrativeClass = new AdministrativeClass
|
||||||
|
{
|
||||||
|
Code = "CS2026-01",
|
||||||
|
Name = "计科 2026-1 班",
|
||||||
|
MajorId = major.Id,
|
||||||
|
Grade = 2026
|
||||||
|
};
|
||||||
|
var student = new Student
|
||||||
|
{
|
||||||
|
StudentNumber = "2026001",
|
||||||
|
Name = "原姓名",
|
||||||
|
UserId = user.Id,
|
||||||
|
AdministrativeClassId = administrativeClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 1),
|
||||||
|
Status = StudentStatus.Active
|
||||||
|
};
|
||||||
|
db.AddRange(user, college);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Add(major);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Add(administrativeClass);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Add(student);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new StudentProfileController(
|
||||||
|
db,
|
||||||
|
new StudentDataScope(user.Id),
|
||||||
|
new NoOpCache());
|
||||||
|
var request = new StudentProfileUpdateRequest(
|
||||||
|
Gender.Female,
|
||||||
|
new DateOnly(2008, 3, 12),
|
||||||
|
" Alice ",
|
||||||
|
"370000000000000000",
|
||||||
|
"中国",
|
||||||
|
"汉族",
|
||||||
|
"共青团员",
|
||||||
|
"山东济南",
|
||||||
|
"户籍地址",
|
||||||
|
"现住地址",
|
||||||
|
"250000",
|
||||||
|
"13800000000",
|
||||||
|
"student@example.edu.cn",
|
||||||
|
"123456",
|
||||||
|
"alice-wechat",
|
||||||
|
"家长",
|
||||||
|
"母亲",
|
||||||
|
"13900000000",
|
||||||
|
"听力支持, 走读",
|
||||||
|
"上课时需要靠前座位",
|
||||||
|
"个人简介");
|
||||||
|
|
||||||
|
Assert.IsType<NoContentResult>(await controller.Update(
|
||||||
|
request,
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
db.ChangeTracker.Clear();
|
||||||
|
var updated = await db.Students.SingleAsync();
|
||||||
|
Assert.Equal("2026001", updated.StudentNumber);
|
||||||
|
Assert.Equal("原姓名", updated.Name);
|
||||||
|
Assert.Equal(administrativeClass.Id, updated.AdministrativeClassId);
|
||||||
|
Assert.Equal(StudentStatus.Active, updated.Status);
|
||||||
|
Assert.Equal("Alice", updated.EnglishName);
|
||||||
|
Assert.Equal("13800000000", updated.Phone);
|
||||||
|
Assert.Equal("听力支持, 走读", updated.SpecialTags);
|
||||||
|
Assert.Equal("上课时需要靠前座位", updated.SpecialNeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Missing_linked_student_returns_not_found()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
await using var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
var controller = new StudentProfileController(
|
||||||
|
db,
|
||||||
|
new StudentDataScope(Guid.NewGuid()),
|
||||||
|
new NoOpCache());
|
||||||
|
|
||||||
|
var result = await controller.Get(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<NotFoundResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoOpCache : IAppCache
|
||||||
|
{
|
||||||
|
public Task<T> GetOrCreateAsync<T>(
|
||||||
|
string key,
|
||||||
|
Func<CancellationToken, Task<T>> factory,
|
||||||
|
AppCacheProfile profile,
|
||||||
|
IReadOnlyCollection<string> tags,
|
||||||
|
CancellationToken cancellationToken) => factory(cancellationToken);
|
||||||
|
|
||||||
|
public ValueTask RemoveByTagAsync(
|
||||||
|
string tag,
|
||||||
|
CancellationToken cancellationToken = default) => ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
userId,
|
||||||
|
"测试学生",
|
||||||
|
null,
|
||||||
|
DataScope.Self,
|
||||||
|
new HashSet<string>([SystemRoles.Student]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using ClosedXML.Excel;
|
||||||
|
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.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class TeachingTaskRosterProfileTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Assigned_teacher_receives_contact_and_special_fields_in_roster_and_export()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
await using var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var teacherUser = User("T001", "任课教师");
|
||||||
|
var studentUser = User("S001", "学生甲");
|
||||||
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
|
var major = new Major
|
||||||
|
{
|
||||||
|
Code = "080901", Name = "计算机科学与技术",
|
||||||
|
CollegeId = college.Id, DegreeType = "工学学士"
|
||||||
|
};
|
||||||
|
var administrativeClass = new AdministrativeClass
|
||||||
|
{
|
||||||
|
Code = "CS2026-01", Name = "计科一班", MajorId = major.Id, Grade = 2026
|
||||||
|
};
|
||||||
|
var teacher = new Teacher
|
||||||
|
{
|
||||||
|
TeacherNumber = "T001", Name = "任课教师", CollegeId = college.Id,
|
||||||
|
UserId = teacherUser.Id
|
||||||
|
};
|
||||||
|
var student = new Student
|
||||||
|
{
|
||||||
|
StudentNumber = "S001", Name = "学生甲", UserId = studentUser.Id,
|
||||||
|
AdministrativeClassId = administrativeClass.Id,
|
||||||
|
EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1),
|
||||||
|
Phone = "13800000000", Email = "s@example.edu.cn", WeChat = "student-wechat",
|
||||||
|
EmergencyContactName = "家长", EmergencyContactRelationship = "母亲",
|
||||||
|
EmergencyContactPhone = "13900000000", SpecialTags = "走读",
|
||||||
|
SpecialNeeds = "需留意交通延误"
|
||||||
|
};
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-1", Name = "2026-2027-1", AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2027, 1, 20)
|
||||||
|
};
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "CS101", Name = "程序设计", CollegeId = college.Id,
|
||||||
|
Credits = 4, TotalHours = 64, LectureHours = 48, PracticeHours = 16,
|
||||||
|
Nature = CourseNature.MajorRequired,
|
||||||
|
AssessmentMethod = AssessmentMethod.Examination
|
||||||
|
};
|
||||||
|
db.AddRange(teacherUser, studentUser, college);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.AddRange(major, teacher, term, course);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Add(administrativeClass);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
db.Add(student);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var task = new TeachingTask
|
||||||
|
{
|
||||||
|
TaskNumber = "2026-CS101-01", Name = "程序设计教学班",
|
||||||
|
AcademicTermId = term.Id, CourseId = course.Id, Capacity = 40,
|
||||||
|
Status = TeachingTaskStatus.Published,
|
||||||
|
Teachers = [new TeachingTaskTeacher { TeacherId = teacher.Id }],
|
||||||
|
Classes = [new TeachingTaskClass { AdministrativeClassId = administrativeClass.Id }]
|
||||||
|
};
|
||||||
|
db.Add(task);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new CourseSelectionsController(
|
||||||
|
db,
|
||||||
|
new TeacherDataScope(teacherUser.Id));
|
||||||
|
var rosterResult = Assert.IsType<OkObjectResult>(
|
||||||
|
await controller.GetTeachingTaskRoster(task.Id, CancellationToken.None));
|
||||||
|
var students = Assert.IsAssignableFrom<System.Collections.IEnumerable>(
|
||||||
|
rosterResult.Value!.GetType().GetProperty("Students")!.GetValue(rosterResult.Value));
|
||||||
|
var row = Assert.Single(students.Cast<object>());
|
||||||
|
Assert.Equal("13800000000", Property(row, "Phone"));
|
||||||
|
Assert.Equal("走读", Property(row, "SpecialTags"));
|
||||||
|
Assert.Equal("需留意交通延误", Property(row, "SpecialNeeds"));
|
||||||
|
|
||||||
|
var export = Assert.IsType<FileContentResult>(
|
||||||
|
await controller.ExportTeachingTaskRoster(task.Id, CancellationToken.None));
|
||||||
|
using var stream = new MemoryStream(export.FileContents);
|
||||||
|
using var workbook = new XLWorkbook(stream);
|
||||||
|
var sheet = workbook.Worksheet("教学班名单");
|
||||||
|
Assert.Equal("联系电话", sheet.Cell(1, 5).GetString());
|
||||||
|
Assert.Equal("特殊标记", sheet.Cell(1, 11).GetString());
|
||||||
|
Assert.Equal("13800000000", sheet.Cell(2, 5).GetString());
|
||||||
|
Assert.Equal("走读", sheet.Cell(2, 11).GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Property(object value, string name) =>
|
||||||
|
value.GetType().GetProperty(name)!.GetValue(value)?.ToString();
|
||||||
|
|
||||||
|
private static ApplicationUser User(string userName, string displayName) => new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(), UserName = userName,
|
||||||
|
NormalizedUserName = userName.ToUpperInvariant(), DisplayName = displayName
|
||||||
|
};
|
||||||
|
|
||||||
|
private sealed class TeacherDataScope(Guid userId) : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
userId, "任课教师", null, DataScope.Self,
|
||||||
|
new HashSet<string>([SystemRoles.Teacher]));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.3.0-rc1",
|
"version": "2.3.0-rc2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
|||||||
key: 'student-status',
|
key: 'student-status',
|
||||||
label: '学籍管理',
|
label: '学籍管理',
|
||||||
items: [
|
items: [
|
||||||
|
...whenVisible(
|
||||||
|
isStudent.value,
|
||||||
|
{ path: '/my-profile', label: '个人信息' },
|
||||||
|
),
|
||||||
...whenVisible(
|
...whenVisible(
|
||||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student']),
|
||||||
{ path: '/student-status-changes', label: isStudent.value ? '学籍异动' : '异动审核' },
|
{ path: '/student-status-changes', label: isStudent.value ? '学籍异动' : '异动审核' },
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ const router = createRouter({
|
|||||||
name: 'account',
|
name: 'account',
|
||||||
component: () => import('../views/AccountView.vue'),
|
component: () => import('../views/AccountView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'my-profile',
|
||||||
|
name: 'my-profile',
|
||||||
|
component: () => import('../views/StudentProfileView.vue'),
|
||||||
|
meta: { roles: ['Student'] },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'base-data',
|
path: 'base-data',
|
||||||
redirect: '/base-data/organization',
|
redirect: '/base-data/organization',
|
||||||
|
|||||||
@@ -133,8 +133,25 @@ function resetForm(row?: any) {
|
|||||||
enrollmentDate: `${currentYear}-09-01`,
|
enrollmentDate: `${currentYear}-09-01`,
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
dateOfBirth: '',
|
dateOfBirth: '',
|
||||||
|
englishName: '',
|
||||||
|
idCardNumber: '',
|
||||||
|
nationality: '',
|
||||||
|
ethnicity: '',
|
||||||
|
politicalStatus: '',
|
||||||
|
nativePlace: '',
|
||||||
|
householdAddress: '',
|
||||||
|
currentAddress: '',
|
||||||
|
postalCode: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
qq: '',
|
||||||
|
weChat: '',
|
||||||
|
emergencyContactName: '',
|
||||||
|
emergencyContactRelationship: '',
|
||||||
|
emergencyContactPhone: '',
|
||||||
|
specialTags: '',
|
||||||
|
specialNeeds: '',
|
||||||
|
biography: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
}, row ?? {})
|
}, row ?? {})
|
||||||
}
|
}
|
||||||
@@ -516,7 +533,7 @@ watch(active, async () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="620px">
|
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${pageTitle}`" width="min(860px, 94vw)">
|
||||||
<el-form label-position="top" class="entity-form">
|
<el-form label-position="top" class="entity-form">
|
||||||
<template v-if="active === 'teachers'">
|
<template v-if="active === 'teachers'">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
@@ -580,6 +597,38 @@ watch(active, async () => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
|
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
|
<el-divider content-position="left">个人资料</el-divider>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="英文姓名"><el-input v-model="form.englishName" /></el-form-item>
|
||||||
|
<el-form-item label="证件号码"><el-input v-model="form.idCardNumber" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="国籍"><el-input v-model="form.nationality" /></el-form-item>
|
||||||
|
<el-form-item label="民族"><el-input v-model="form.ethnicity" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="政治面貌"><el-input v-model="form.politicalStatus" /></el-form-item>
|
||||||
|
<el-form-item label="籍贯"><el-input v-model="form.nativePlace" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="户籍地址"><el-input v-model="form.householdAddress" /></el-form-item>
|
||||||
|
<el-form-item label="现居住地址"><el-input v-model="form.currentAddress" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="邮政编码"><el-input v-model="form.postalCode" /></el-form-item>
|
||||||
|
<el-form-item label="微信"><el-input v-model="form.weChat" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="QQ"><el-input v-model="form.qq" /></el-form-item>
|
||||||
|
<el-form-item label="紧急联系人"><el-input v-model="form.emergencyContactName" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="与本人关系"><el-input v-model="form.emergencyContactRelationship" /></el-form-item>
|
||||||
|
<el-form-item label="紧急联系电话"><el-input v-model="form.emergencyContactPhone" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="特殊标记"><el-input v-model="form.specialTags" placeholder="多个标记可用逗号分隔" /></el-form-item>
|
||||||
|
<el-form-item label="特殊情况说明"><el-input v-model="form.specialNeeds" type="textarea" :rows="3" /></el-form-item>
|
||||||
|
<el-form-item label="个人简介"><el-input v-model="form.biography" type="textarea" :rows="3" /></el-form-item>
|
||||||
</template>
|
</template>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item>
|
<el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item>
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Check, EditPen, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const editing = ref(false)
|
||||||
|
const profile = reactive<Record<string, any>>({})
|
||||||
|
const form = reactive<Record<string, any>>({})
|
||||||
|
|
||||||
|
const genderLabels: Record<string, string> = {
|
||||||
|
Unknown: '未设置', Male: '男', Female: '女',
|
||||||
|
}
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
Active: '在籍', Suspended: '休学', Graduated: '毕业', Withdrawn: '退学',
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableFields = [
|
||||||
|
'gender', 'dateOfBirth', 'englishName', 'idCardNumber', 'nationality',
|
||||||
|
'ethnicity', 'politicalStatus', 'nativePlace', 'householdAddress',
|
||||||
|
'currentAddress', 'postalCode', 'phone', 'email', 'qq', 'weChat',
|
||||||
|
'emergencyContactName', 'emergencyContactRelationship',
|
||||||
|
'emergencyContactPhone', 'specialTags', 'specialNeeds', 'biography',
|
||||||
|
]
|
||||||
|
|
||||||
|
function copyToForm() {
|
||||||
|
for (const key of editableFields) form[key] = profile[key] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
Object.assign(profile, (await http.get('/student/profile')).data)
|
||||||
|
copyToForm()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
copyToForm()
|
||||||
|
editing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
copyToForm()
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await http.put('/student/profile', form)
|
||||||
|
ElMessage.success('个人信息已保存,无需审核。')
|
||||||
|
editing.value = false
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="profile-page" v-loading="loading">
|
||||||
|
<section class="profile-hero">
|
||||||
|
<div>
|
||||||
|
<span>STUDENT PROFILE</span>
|
||||||
|
<h2>我的个人信息</h2>
|
||||||
|
<p>完善个人资料、联系方式与需要任课教师关注的特殊情况。</p>
|
||||||
|
</div>
|
||||||
|
<div class="profile-actions">
|
||||||
|
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||||
|
<el-button v-if="!editing" type="primary" :icon="EditPen" @click="startEdit">修改信息</el-button>
|
||||||
|
<template v-else>
|
||||||
|
<el-button @click="cancelEdit">取消</el-button>
|
||||||
|
<el-button type="primary" :icon="Check" :loading="saving" @click="save">直接保存</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="联系电话、电子邮箱、微信、紧急联系人以及特殊标记/说明会向你的任课教师展示,并写入教学班名单导出文件。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="identity-strip">
|
||||||
|
<div><span>学号</span><b>{{ profile.studentNumber }}</b><small>不可自行修改</small></div>
|
||||||
|
<div><span>姓名</span><b>{{ profile.name }}</b><small>不可自行修改</small></div>
|
||||||
|
<div><span>学院 / 专业</span><b>{{ profile.collegeName }}</b><small>{{ profile.majorName }}</small></div>
|
||||||
|
<div><span>行政班 / 学籍</span><b>{{ profile.className }}</b><small>{{ statusLabels[profile.status] }} · {{ profile.enrollmentYear }} 级</small></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-form :model="form" label-position="top" class="profile-form" :disabled="!editing">
|
||||||
|
<article class="profile-card">
|
||||||
|
<header><span>01</span><div><h3>身份与背景</h3><p>除学号和姓名外,可直接维护常用身份信息。</p></div></header>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="英文姓名"><el-input v-model="form.englishName" maxlength="100" /></el-form-item>
|
||||||
|
<el-form-item label="性别"><el-select v-model="form.gender"><el-option v-for="(label, value) in genderLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="出生日期"><el-date-picker v-model="form.dateOfBirth" value-format="YYYY-MM-DD" /></el-form-item>
|
||||||
|
<el-form-item label="证件号码"><el-input v-model="form.idCardNumber" maxlength="30" /></el-form-item>
|
||||||
|
<el-form-item label="国籍"><el-input v-model="form.nationality" maxlength="50" /></el-form-item>
|
||||||
|
<el-form-item label="民族"><el-input v-model="form.ethnicity" maxlength="50" /></el-form-item>
|
||||||
|
<el-form-item label="政治面貌"><el-input v-model="form.politicalStatus" maxlength="50" /></el-form-item>
|
||||||
|
<el-form-item label="籍贯"><el-input v-model="form.nativePlace" maxlength="100" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="profile-card">
|
||||||
|
<header><span>02</span><div><h3>联系方式与地址</h3><p>任课教师仅能看到本区的电话、邮箱和微信。</p></div></header>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="联系电话"><el-input v-model="form.phone" maxlength="30" /></el-form-item>
|
||||||
|
<el-form-item label="电子邮箱"><el-input v-model="form.email" maxlength="100" /></el-form-item>
|
||||||
|
<el-form-item label="微信"><el-input v-model="form.weChat" maxlength="60" /></el-form-item>
|
||||||
|
<el-form-item label="QQ"><el-input v-model="form.qq" maxlength="30" /></el-form-item>
|
||||||
|
<el-form-item label="邮政编码"><el-input v-model="form.postalCode" maxlength="20" /></el-form-item>
|
||||||
|
<el-form-item label="户籍地址" class="span-2"><el-input v-model="form.householdAddress" maxlength="300" /></el-form-item>
|
||||||
|
<el-form-item label="现居住地址" class="span-2"><el-input v-model="form.currentAddress" maxlength="300" /></el-form-item>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="profile-card attention-card">
|
||||||
|
<header><span>03</span><div><h3>紧急联系与特殊标记</h3><p>本区内容会向任课教师展示,请填写真实且必要的信息。</p></div></header>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="紧急联系人"><el-input v-model="form.emergencyContactName" maxlength="50" /></el-form-item>
|
||||||
|
<el-form-item label="与本人关系"><el-input v-model="form.emergencyContactRelationship" maxlength="30" /></el-form-item>
|
||||||
|
<el-form-item label="紧急联系电话"><el-input v-model="form.emergencyContactPhone" maxlength="30" /></el-form-item>
|
||||||
|
<el-form-item label="特殊标记"><el-input v-model="form.specialTags" maxlength="300" placeholder="多个标记可用逗号分隔" /></el-form-item>
|
||||||
|
<el-form-item label="特殊情况说明" class="span-2"><el-input v-model="form.specialNeeds" type="textarea" :rows="4" maxlength="1000" show-word-limit placeholder="例如学习支持、健康与安全方面需教师留意的事项" /></el-form-item>
|
||||||
|
<el-form-item label="个人简介" class="span-2"><el-input v-model="form.biography" type="textarea" :rows="4" maxlength="1000" show-word-limit /></el-form-item>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page { display: grid; gap: 18px; }
|
||||||
|
.profile-hero { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 26px 30px; border-radius: 18px; color: white; background: linear-gradient(125deg, #173a50, #176b87 62%, #2e9b91); box-shadow: 0 18px 45px rgba(23, 58, 80, .16); }
|
||||||
|
.profile-hero span { color: #8ee4d8; font-size: 11px; font-weight: 800; letter-spacing: .12em; }
|
||||||
|
.profile-hero h2 { margin: 7px 0 5px; font-size: 30px; font-family: "STZhongsong", "Songti SC", serif; }
|
||||||
|
.profile-hero p { margin: 0; color: rgba(255,255,255,.75); }
|
||||||
|
.profile-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.identity-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--line); background: white; }
|
||||||
|
.identity-strip > div { display: grid; gap: 5px; padding: 18px 20px; border-right: 1px solid var(--line); }
|
||||||
|
.identity-strip > div:last-child { border-right: 0; }
|
||||||
|
.identity-strip span, .identity-strip small { color: var(--muted); font-size: 12px; }
|
||||||
|
.identity-strip b { color: #263445; }
|
||||||
|
.profile-form { display: grid; gap: 18px; }
|
||||||
|
.profile-card { padding: 24px; border: 1px solid var(--line); border-radius: 14px; background: white; box-shadow: 0 10px 28px rgba(35,57,78,.05); }
|
||||||
|
.profile-card > header { display: flex; gap: 14px; margin-bottom: 22px; padding-bottom: 16px; border-bottom: 1px solid #edf0f3; }
|
||||||
|
.profile-card > header > span { color: var(--teal); font: 800 12px/1 Consolas, monospace; }
|
||||||
|
.profile-card h3 { margin: 0 0 5px; color: #263445; }
|
||||||
|
.profile-card p { margin: 0; color: var(--muted); font-size: 12px; }
|
||||||
|
.attention-card { border-top: 3px solid #d89b36; }
|
||||||
|
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 18px; }
|
||||||
|
.span-2 { grid-column: span 2; }
|
||||||
|
:deep(.el-form.is-disabled .el-input__wrapper), :deep(.el-form.is-disabled .el-textarea__inner) { background: #f8fafb; color: #364655; }
|
||||||
|
@media (max-width: 900px) { .identity-strip { grid-template-columns: repeat(2, 1fr); } .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
|
@media (max-width: 620px) { .profile-hero { align-items: flex-start; flex-direction: column; padding: 22px; } .profile-actions { justify-content: flex-start; } .identity-strip, .form-grid { grid-template-columns: 1fr; } .identity-strip > div { border-right: 0; border-bottom: 1px solid var(--line); } .span-2 { grid-column: auto; } .profile-card { padding: 18px; } }
|
||||||
|
</style>
|
||||||
@@ -118,6 +118,31 @@ onMounted(async () => {
|
|||||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||||
<el-table-column prop="className" label="班级" min-width="130" />
|
<el-table-column prop="className" label="班级" min-width="130" />
|
||||||
<el-table-column prop="majorName" label="专业" min-width="150" />
|
<el-table-column prop="majorName" label="专业" min-width="150" />
|
||||||
|
<el-table-column label="联系方式" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="contact-cell">
|
||||||
|
<span>{{ row.phone || '未填写电话' }}</span>
|
||||||
|
<small>{{ row.email || row.weChat ? [row.email, row.weChat && `微信 ${row.weChat}`].filter(Boolean).join(' · ') : '未填写邮箱或微信' }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="紧急联系人" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="contact-cell">
|
||||||
|
<span>{{ row.emergencyContactName || '—' }}<template v-if="row.emergencyContactRelationship">({{ row.emergencyContactRelationship }})</template></span>
|
||||||
|
<small>{{ row.emergencyContactPhone || '未填写联系电话' }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="特殊标记" min-width="210">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="special-cell">
|
||||||
|
<el-tag v-if="row.specialTags" type="warning" effect="light">{{ row.specialTags }}</el-tag>
|
||||||
|
<span v-else>—</span>
|
||||||
|
<small v-if="row.specialNeeds">{{ row.specialNeeds }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="名单来源" width="210">
|
<el-table-column label="名单来源" width="210">
|
||||||
<template #default="{ row }">{{ row.enrolledAt ? `选课 · ${new Date(row.enrolledAt).toLocaleString('zh-CN')}` : '行政班关联' }}</template>
|
<template #default="{ row }">{{ row.enrolledAt ? `选课 · ${new Date(row.enrolledAt).toLocaleString('zh-CN')}` : '行政班关联' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -156,6 +181,9 @@ onMounted(async () => {
|
|||||||
.roster-head h3 { margin: 5px 0 4px; font-size: 18px; }
|
.roster-head h3 { margin: 5px 0 4px; font-size: 18px; }
|
||||||
.roster-head p { color: var(--muted); font-size: 12px; margin: 0; }
|
.roster-head p { color: var(--muted); font-size: 12px; margin: 0; }
|
||||||
.roster-head span { color: var(--teal); font-size: 10px; font-weight: 700; }
|
.roster-head span { color: var(--teal); font-size: 10px; font-weight: 700; }
|
||||||
|
.contact-cell, .special-cell { display: grid; gap: 4px; line-height: 1.35; }
|
||||||
|
.contact-cell small, .special-cell small { color: var(--muted); white-space: normal; }
|
||||||
|
.special-cell .el-tag { width: fit-content; max-width: 100%; white-space: normal; height: auto; padding-block: 3px; }
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.roster-workspace { grid-template-columns: 1fr; }
|
.roster-workspace { grid-template-columns: 1fr; }
|
||||||
.roster-task-list { max-height: 220px; border-right: none; border-bottom: 1px solid var(--line); }
|
.roster-task-list { max-height: 220px; border-right: none; border-bottom: 1px solid var(--line); }
|
||||||
|
|||||||
Reference in New Issue
Block a user