9 Commits
64 changed files with 44509 additions and 1277 deletions

No files matched your search

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Grades; using Jiaowu.Api.Infrastructure.Grades;
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Persistence;
@@ -250,6 +251,13 @@ public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope s
// Auto-apply: update grade record // Auto-apply: update grade record
gm.GradeRecord!.TotalScore = gm.RequestedScore; gm.GradeRecord!.TotalScore = gm.RequestedScore;
gm.GradeRecord.GradePoint = GradeCalculator.CalculateGradePoint(gm.RequestedScore); gm.GradeRecord.GradePoint = GradeCalculator.CalculateGradePoint(gm.RequestedScore);
var statisticsJob = new CourseGradeStatisticsRefreshJob
{
GradeSheetId = gm.GradeRecord.GradeSheetId
};
db.CourseGradeStatisticsRefreshJobs.Add(statisticsJob);
db.BackgroundJobOutboxMessages.Add(BackgroundJobOutboxMessage.Create(
BackgroundJobKind.CourseGradeStatisticsRefresh, statisticsJob.Id));
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
await NotificationService.SendAsync( await NotificationService.SendAsync(
db, db,
@@ -491,6 +491,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
x.Building.Campus!.Name, x.Building.Campus!.Name,
x.Capacity, x.Capacity,
x.RoomType, x.RoomType,
x.TeachingVenueNature,
x.Equipment, x.Equipment,
x.IsEnabled, x.IsEnabled,
x.SortOrder)) x.SortOrder))
@@ -557,6 +558,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
BuildingId = request.BuildingId, BuildingId = request.BuildingId,
Capacity = request.Capacity, Capacity = request.Capacity,
RoomType = request.RoomType.Trim(), RoomType = request.RoomType.Trim(),
TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature,
Equipment = request.Equipment?.Trim(), Equipment = request.Equipment?.Trim(),
SortOrder = request.SortOrder, SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled IsEnabled = request.IsEnabled
@@ -577,6 +581,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
entity.BuildingId = request.BuildingId; entity.BuildingId = request.BuildingId;
entity.Capacity = request.Capacity; entity.Capacity = request.Capacity;
entity.RoomType = request.RoomType.Trim(); entity.RoomType = request.RoomType.Trim();
entity.TeachingVenueNature = request.TeachingVenueNature == 0
? TeachingVenueNature.GeneralClassroom
: request.TeachingVenueNature;
entity.Equipment = request.Equipment?.Trim(); entity.Equipment = request.Equipment?.Trim();
await SaveAndInvalidateAsync(cancellationToken); await SaveAndInvalidateAsync(cancellationToken);
return entity; return entity;
@@ -728,6 +735,7 @@ public sealed record ClassroomRequest(
Guid BuildingId, Guid BuildingId,
[Range(1, 1000)] int Capacity, [Range(1, 1000)] int Capacity,
[Required, MaxLength(40)] string RoomType, [Required, MaxLength(40)] string RoomType,
TeachingVenueNature TeachingVenueNature,
[MaxLength(300)] string? Equipment) [MaxLength(300)] string? Equipment)
: CatalogRequest(Code, Name, SortOrder, IsEnabled); : CatalogRequest(Code, Name, SortOrder, IsEnabled);
@@ -784,6 +792,7 @@ public sealed record ClassroomListItem(
string CampusName, string CampusName,
int Capacity, int Capacity,
string RoomType, string RoomType,
TeachingVenueNature TeachingVenueNature,
string? Equipment, string? Equipment,
bool IsEnabled, bool IsEnabled,
int SortOrder); int SortOrder);
@@ -27,7 +27,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"], ["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"], ["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"], ["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"], ["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "教学场地性质", "设备", "排序", "状态"],
["course-categories"] = ["编码", "名称", "排序", "状态"] ["course-categories"] = ["编码", "名称", "排序", "状态"]
}; };
@@ -70,7 +70,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
IReadOnlyList<ExcelRow> rows; IReadOnlyList<ExcelRow> rows;
try try
{ {
rows = await ExcelWorkbookHelper.ReadAsync(file, headers, cancellationToken); var requiredHeaders = kind.Equals("classrooms", StringComparison.OrdinalIgnoreCase)
? headers.Where(x => x != "教学场地性质").ToArray()
: headers;
rows = await ExcelWorkbookHelper.ReadAsync(file, requiredHeaders, cancellationToken);
} }
catch (InvalidDataException exception) catch (InvalidDataException exception)
{ {
@@ -165,6 +168,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
"classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building) "classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
.OrderBy(x => x.Code).ToListAsync(cancellationToken)) .OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType, .Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
VenueNatureName(x.TeachingVenueNature),
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(), x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
"course-categories" => (await db.CourseCategories.AsNoTracking() "course-categories" => (await db.CourseCategories.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code) .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
@@ -473,8 +477,9 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
var buildingCode = Required(row, "所属教学楼编码", errors); var buildingCode = Required(row, "所属教学楼编码", errors);
var capacity = ParseInt(row, "容量", 1, 1000, errors); var capacity = ParseInt(row, "容量", 1, 1000, errors);
var roomType = Required(row, "教室类型", errors); var roomType = Required(row, "教室类型", errors);
var venueNature = ParseVenueNature(row, roomType, errors);
if (code is null || name is null || buildingCode is null || if (code is null || name is null || buildingCode is null ||
capacity is null || roomType is null) continue; capacity is null || roomType is null || venueNature is null) continue;
if (!buildings.TryGetValue(buildingCode, out var building)) if (!buildings.TryGetValue(buildingCode, out var building))
{ {
errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。"); errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。");
@@ -488,7 +493,8 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
Code = code, Code = code,
Name = name, Name = name,
BuildingId = building.Id, BuildingId = building.Id,
RoomType = roomType RoomType = roomType,
TeachingVenueNature = venueNature.Value
}; };
db.Classrooms.Add(entity); db.Classrooms.Add(entity);
existing[code] = entity; existing[code] = entity;
@@ -499,6 +505,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
entity.BuildingId = building.Id; entity.BuildingId = building.Id;
entity.Capacity = capacity.Value; entity.Capacity = capacity.Value;
entity.RoomType = roomType; entity.RoomType = roomType;
entity.TeachingVenueNature = venueNature.Value;
entity.Equipment = Optional(row, "设备"); entity.Equipment = Optional(row, "设备");
} }
return new(created, updated, rows.Count); return new(created, updated, rows.Count);
@@ -597,6 +604,57 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
return true; return true;
} }
private static TeachingVenueNature? ParseVenueNature(
ExcelRow row,
string? roomType,
List<string> errors)
{
var value = Optional(row, "教学场地性质");
if (value is null) return InferVenueNature(roomType ?? string.Empty);
var result = (TeachingVenueNature)0;
foreach (var part in value.Split(['、', ',', ',', ';', ';'],
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
result |= part switch
{
"普通教室" => TeachingVenueNature.GeneralClassroom,
"实验室" => TeachingVenueNature.Laboratory,
"实训室" => TeachingVenueNature.TrainingRoom,
"计算机机房" or "机房" => TeachingVenueNature.ComputerLab,
"语音室" => TeachingVenueNature.LanguageLab,
"体育场地" => TeachingVenueNature.SportsVenue,
"艺术场地" => TeachingVenueNature.ArtsVenue,
_ => (TeachingVenueNature)0
};
if (part is not ("普通教室" or "实验室" or "实训室" or "计算机机房" or "机房" or "语音室" or "体育场地" or "艺术场地"))
errors.Add($"第 {row.RowNumber} 行:“教学场地性质”包含不支持的值“{part}”。");
}
return result == 0 ? null : result;
}
private static TeachingVenueNature InferVenueNature(string roomType) =>
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory | TeachingVenueNature.ComputerLab
: roomType.Contains("语音", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory | TeachingVenueNature.LanguageLab
: roomType.Contains("实训", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.TrainingRoom
: roomType.Contains("实验", StringComparison.OrdinalIgnoreCase)
? TeachingVenueNature.Laboratory
: TeachingVenueNature.GeneralClassroom;
private static string VenueNatureName(TeachingVenueNature value) => string.Join("、",
new[]
{
(TeachingVenueNature.GeneralClassroom, "普通教室"),
(TeachingVenueNature.Laboratory, "实验室"),
(TeachingVenueNature.TrainingRoom, "实训室"),
(TeachingVenueNature.ComputerLab, "计算机机房"),
(TeachingVenueNature.LanguageLab, "语音室"),
(TeachingVenueNature.SportsVenue, "体育场地"),
(TeachingVenueNature.ArtsVenue, "艺术场地")
}.Where(x => (value & x.Item1) != 0).Select(x => x.Item2));
private static bool ParseBoolean( private static bool ParseBoolean(
ExcelRow row, string header, bool defaultValue, List<string> errors) ExcelRow row, string header, bool defaultValue, List<string> errors)
{ {
Loaded 3 of 64 files, more files were not shown because too many files have changed in this diff. Show more