成绩通知:仅在某门课程成绩正式发布后自动发送,按成绩单学生名单通知,不展示具体分数;发布状态与通知同一次提交。

手动发信:移除“成绩通知”选项,只能发送普通通知,原有角色与范围权限保持不变。
选课通知:管理员关闭一轮选课时,自动向每位参与学生发送一条汇总通知,包含最终选中课程及候补未成功课程;候补失效、轮次关闭和通知生成保持原子性。
关闭选课界面会明确提示发送通知,并显示实际通知人数。
This commit is contained in:
2026-07-26 19:12:42 +08:00 Unverified
parent ecc6546f3a
commit b0679a253d
20 changed files with 6804 additions and 210 deletions
@@ -95,18 +95,31 @@ public sealed class CourseSelectionsControllerTests
var adminController = new CourseSelectionsController(
db,
new AdminDataScope());
Assert.IsType<OkObjectResult>(await adminController.CloseRound(
var closeResult = Assert.IsType<OkObjectResult>(await adminController.CloseRound(
data.RoundId,
CancellationToken.None));
Assert.Equal(2, ReadIntProperty(closeResult.Value, "NotifiedStudentCount"));
db.ChangeTracker.Clear();
var waitlist = await db.CourseEnrollments.SingleAsync(
x => x.StudentId == data.FirstWaiterStudentId);
Assert.Equal(CourseEnrollmentStatus.Expired, waitlist.Status);
Assert.NotNull(waitlist.WithdrawnAt);
Assert.True(await db.Notifications.AnyAsync(x =>
x.UserId == data.FirstWaiterUserId &&
x.Title == "课程候补已结束"));
var resultNotifications = await db.Notifications
.Where(x => x.Title == "第一轮选课结果")
.OrderBy(x => x.UserId)
.ToListAsync();
Assert.Equal(2, resultNotifications.Count);
var selectedNotification = Assert.Single(resultNotifications
.Where(x => x.UserId == data.EnrolledUserId));
Assert.Contains("最终选中 1 门", selectedNotification.Content);
Assert.Contains("《程序设计基础》", selectedNotification.Content);
var waitlistNotification = Assert.Single(resultNotifications
.Where(x => x.UserId == data.FirstWaiterUserId));
Assert.Contains("本轮未选中课程", waitlistNotification.Content);
Assert.Contains("候补未成功 1 门", waitlistNotification.Content);
Assert.Equal(NotificationCategory.CourseSelection, waitlistNotification.Category);
Assert.Equal("/course-selections", waitlistNotification.LinkUrl);
}
[Fact]
@@ -337,6 +350,14 @@ public sealed class CourseSelectionsControllerTests
DisplayName = displayName
};
private static int ReadIntProperty(object? value, string propertyName)
{
Assert.NotNull(value);
var property = value.GetType().GetProperty(propertyName);
Assert.NotNull(property);
return Assert.IsType<int>(property.GetValue(value));
}
private static Student CreateStudent(
string number,
string name,