成绩通知:仅在某门课程成绩正式发布后自动发送,按成绩单学生名单通知,不展示具体分数;发布状态与通知同一次提交。
手动发信:移除“成绩通知”选项,只能发送普通通知,原有角色与范围权限保持不变。 选课通知:管理员关闭一轮选课时,自动向每位参与学生发送一条汇总通知,包含最终选中课程及候补未成功课程;候补失效、轮次关闭和通知生成保持原子性。 关闭选课界面会明确提示发送通知,并显示实际通知人数。
This commit is contained in:
@@ -218,38 +218,67 @@ public sealed class CourseSelectionsController(
|
||||
if (round.Status != CourseSelectionRoundStatus.Open)
|
||||
return ConflictProblem("只有开放中的选课批次可以关闭。");
|
||||
|
||||
var waitlisted = await db.CourseEnrollments
|
||||
var enrollments = await db.CourseEnrollments
|
||||
.Include(x => x.Student)
|
||||
.Include(x => x.CourseSelectionOffering)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.Where(x =>
|
||||
x.CourseSelectionOffering!.CourseSelectionRoundId == id &&
|
||||
x.Status == CourseEnrollmentStatus.Waitlisted)
|
||||
x.CourseSelectionOffering!.CourseSelectionRoundId == id)
|
||||
.ToListAsync(cancellationToken);
|
||||
var waitlisted = enrollments
|
||||
.Where(x => x.Status == CourseEnrollmentStatus.Waitlisted)
|
||||
.ToList();
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var enrollment in waitlisted)
|
||||
{
|
||||
enrollment.Status = CourseEnrollmentStatus.Expired;
|
||||
enrollment.WithdrawnAt = now;
|
||||
if (enrollment.Student!.UserId is Guid userId)
|
||||
}
|
||||
|
||||
var participantGroups = enrollments
|
||||
.Where(x => x.Student!.UserId.HasValue)
|
||||
.GroupBy(x => new
|
||||
{
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = userId,
|
||||
Title = "课程候补已结束",
|
||||
Content =
|
||||
$"“{enrollment.CourseSelectionOffering!.TeachingTask!.Course!.Name}”" +
|
||||
"选课批次已关闭,本次候补未获得名额。",
|
||||
LinkUrl = "/course-selections"
|
||||
});
|
||||
}
|
||||
x.StudentId,
|
||||
UserId = x.Student!.UserId!.Value
|
||||
})
|
||||
.ToList();
|
||||
foreach (var participant in participantGroups)
|
||||
{
|
||||
var selectedCourses = participant
|
||||
.Where(x => x.Status == CourseEnrollmentStatus.Enrolled)
|
||||
.Select(x => x.CourseSelectionOffering!.TeachingTask!.Course!.Name)
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToArray();
|
||||
var unsuccessfulCourses = participant
|
||||
.Where(x => x.Status == CourseEnrollmentStatus.Expired)
|
||||
.Select(x => x.CourseSelectionOffering!.TeachingTask!.Course!.Name)
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToArray();
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = participant.Key.UserId,
|
||||
Title = $"{round.Name}结果",
|
||||
Content = BuildRoundResultContent(
|
||||
round.Name,
|
||||
selectedCourses,
|
||||
unsuccessfulCourses),
|
||||
Category = NotificationCategory.CourseSelection,
|
||||
LinkUrl = "/course-selections"
|
||||
});
|
||||
}
|
||||
|
||||
round.Status = CourseSelectionRoundStatus.Closed;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
return Ok(new { ExpiredWaitlistCount = waitlisted.Count });
|
||||
return Ok(new
|
||||
{
|
||||
ExpiredWaitlistCount = waitlisted.Count,
|
||||
NotifiedStudentCount = participantGroups.Count
|
||||
});
|
||||
},
|
||||
cancellationToken,
|
||||
IsolationLevel.Serializable);
|
||||
@@ -940,6 +969,7 @@ public sealed class CourseSelectionsController(
|
||||
Content =
|
||||
$"管理员已将你移出“{enrollment.CourseSelectionOffering!.TeachingTask!.Course!.Name}”" +
|
||||
"候补队列。",
|
||||
Category = NotificationCategory.CourseSelection,
|
||||
LinkUrl = "/course-selections"
|
||||
});
|
||||
}
|
||||
@@ -1835,6 +1865,7 @@ public sealed class CourseSelectionsController(
|
||||
Content =
|
||||
$"“{offering.TeachingTask!.Course!.Name}”候补未能递补:" +
|
||||
eligibility.Error,
|
||||
Category = NotificationCategory.CourseSelection,
|
||||
LinkUrl = "/course-selections"
|
||||
});
|
||||
}
|
||||
@@ -1869,6 +1900,7 @@ public sealed class CourseSelectionsController(
|
||||
Content =
|
||||
$"“{offering.TeachingTask.Course!.Name}”已释放名额," +
|
||||
"你已自动进入正式选课名单。",
|
||||
Category = NotificationCategory.CourseSelection,
|
||||
LinkUrl = "/course-selections"
|
||||
});
|
||||
}
|
||||
@@ -1918,6 +1950,39 @@ public sealed class CourseSelectionsController(
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildRoundResultContent(
|
||||
string roundName,
|
||||
IReadOnlyCollection<string> selectedCourses,
|
||||
IReadOnlyCollection<string> unsuccessfulCourses)
|
||||
{
|
||||
var parts = new List<string> { $"{roundName}已结束。" };
|
||||
parts.Add(selectedCourses.Count == 0
|
||||
? "本轮未选中课程。"
|
||||
: $"最终选中 {selectedCourses.Count} 门:{FormatCourseNames(selectedCourses)}。");
|
||||
if (unsuccessfulCourses.Count > 0)
|
||||
{
|
||||
parts.Add(
|
||||
$"候补未成功 {unsuccessfulCourses.Count} 门:" +
|
||||
$"{FormatCourseNames(unsuccessfulCourses)}。");
|
||||
}
|
||||
|
||||
return string.Concat(parts);
|
||||
}
|
||||
|
||||
private static string FormatCourseNames(IReadOnlyCollection<string> courses)
|
||||
{
|
||||
const int visibleCount = 8;
|
||||
const int visibleNameLength = 40;
|
||||
var names = courses.Take(visibleCount)
|
||||
.Select(name => name.Length <= visibleNameLength
|
||||
? $"《{name}》"
|
||||
: $"《{name[..visibleNameLength]}…》");
|
||||
var result = string.Join("、", names);
|
||||
return courses.Count > visibleCount
|
||||
? $"{result}等 {courses.Count} 门课程"
|
||||
: result;
|
||||
}
|
||||
|
||||
private ActionResult ProfileNotFound() =>
|
||||
ConflictProblem("当前账号未关联有效学生档案,请联系教务管理员。");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user