Files
Academic-Affairs-System/src/Jiaowu.Api/Controllers/BaseDataExcelController.cs
T
2026-08-09 12:30:33 +08:00

710 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Common;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Excel;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Controllers;
[ApiController]
[Authorize(Roles = Administrators)]
[Route("api/base-data")]
public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : ControllerBase
{
private const string Administrators =
$"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";
private static readonly IReadOnlyDictionary<string, string[]> Headers =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
["campuses"] = ["编码", "名称", "地址", "排序", "状态"],
["colleges"] = ["编码", "名称", "简称", "所属校区编码", "排序", "状态"],
["majors"] = ["编码", "名称", "所属学院编码", "学位类型", "学制", "排序", "状态"],
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "教学场地性质", "设备", "排序", "状态"],
["course-categories"] = ["编码", "名称", "排序", "状态"]
};
[HttpGet("{kind}/template")]
public IActionResult DownloadTemplate(string kind)
{
if (!TryGetHeaders(kind, out var headers)) return NotFound();
var bytes = ExcelWorkbookHelper.Create(
"数据导入",
headers,
[],
[
"请勿修改第一行列名,不要合并单元格。",
"编码是唯一标识;编码已存在时更新,不存在时新增。",
"关联字段填写对应数据的编码,请先导入上级基础数据。",
"状态填写“启用”或“停用”;是/否字段填写“是”或“否”。",
"整批数据会先校验,任一行有误时均不会写入。"
]);
return File(bytes, ExcelWorkbookHelper.ContentType, $"{KindName(kind)}导入模板.xlsx");
}
[HttpGet("{kind}/export")]
public async Task<IActionResult> Export(string kind, CancellationToken cancellationToken)
{
if (!TryGetHeaders(kind, out var headers)) return NotFound();
var rows = await GetExportRowsAsync(kind, cancellationToken);
var bytes = ExcelWorkbookHelper.Create(KindName(kind), headers, rows);
return File(bytes, ExcelWorkbookHelper.ContentType,
$"{KindName(kind)}-{DateTime.Now:yyyyMMdd-HHmm}.xlsx");
}
[HttpPost("{kind}/import")]
[RequestSizeLimit(10 * 1024 * 1024)]
public async Task<ActionResult<ExcelImportResult>> Import(
string kind,
IFormFile file,
CancellationToken cancellationToken)
{
if (!TryGetHeaders(kind, out var headers)) return NotFound();
IReadOnlyList<ExcelRow> rows;
try
{
var requiredHeaders = kind.Equals("classrooms", StringComparison.OrdinalIgnoreCase)
? headers.Where(x => x != "教学场地性质").ToArray()
: headers;
rows = await ExcelWorkbookHelper.ReadAsync(file, requiredHeaders, cancellationToken);
}
catch (InvalidDataException exception)
{
return ValidationProblem(exception.Message);
}
if (rows.Count == 0) return ValidationProblem("Excel 中没有可导入的数据。");
return await db.ExecuteInRetriableTransactionAsync<
ActionResult<ExcelImportResult>>(
async transaction =>
{
var errors = new List<string>();
ExcelImportResult result;
try
{
result = kind.ToLowerInvariant() switch
{
"campuses" => await ImportCampusesAsync(
rows, errors, cancellationToken),
"colleges" => await ImportCollegesAsync(
rows, errors, cancellationToken),
"majors" => await ImportMajorsAsync(
rows, errors, cancellationToken),
"classes" => await ImportClassesAsync(
rows, errors, cancellationToken),
"terms" => await ImportTermsAsync(
rows, errors, cancellationToken),
"buildings" => await ImportBuildingsAsync(
rows, errors, cancellationToken),
"classrooms" => await ImportClassroomsAsync(
rows, errors, cancellationToken),
"course-categories" => await ImportCourseCategoriesAsync(
rows, errors, cancellationToken),
_ => throw new InvalidOperationException()
};
if (errors.Count > 0)
{
await transaction.RollbackAsync(cancellationToken);
return ImportValidationProblem(errors);
}
await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
await cache.RemoveByTagAsync(
AppCacheTags.BaseData,
cancellationToken);
return Ok(result);
}
catch (DbUpdateException)
{
await transaction.RollbackAsync(cancellationToken);
return Conflict(new ProblemDetails
{
Title = "导入失败",
Detail = "存在重复编码或无效关联,未写入任何数据。",
Status = StatusCodes.Status409Conflict
});
}
},
cancellationToken);
}
private async Task<IReadOnlyList<IReadOnlyList<object?>>> GetExportRowsAsync(
string kind,
CancellationToken cancellationToken) =>
kind.ToLowerInvariant() switch
{
"campuses" => (await db.Campuses.AsNoTracking().OrderBy(x => x.Code)
.ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.Address, x.SortOrder, Status(x.IsEnabled))).ToList(),
"colleges" => (await db.Colleges.AsNoTracking().Include(x => x.Campus)
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.ShortName, x.Campus?.Code, x.SortOrder,
Status(x.IsEnabled))).ToList(),
"majors" => (await db.Majors.AsNoTracking().Include(x => x.College)
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.College!.Code, x.DegreeType,
x.SchoolingYears, x.SortOrder, Status(x.IsEnabled))).ToList(),
"classes" => (await db.AdministrativeClasses.AsNoTracking().Include(x => x.Major)
.Include(x => x.CounselorUser).OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.Major!.Code, x.Grade,
x.CounselorUser?.StaffNumber, x.SortOrder, Status(x.IsEnabled))).ToList(),
"terms" => (await db.AcademicTerms.AsNoTracking().OrderByDescending(x => x.StartDate)
.ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.AcademicYear, SeasonName(x.Season),
x.StartDate, x.EndDate, YesNo(x.IsCurrent), Status(x.IsEnabled))).ToList(),
"buildings" => (await db.Buildings.AsNoTracking().Include(x => x.Campus)
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.Campus!.Code, x.SortOrder,
Status(x.IsEnabled))).ToList(),
"classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
.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(),
"course-categories" => (await db.CourseCategories.AsNoTracking()
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
.ToListAsync(cancellationToken))
.Select(x => Row(x.Code, x.Name, x.SortOrder, Status(x.IsEnabled))).ToList(),
_ => []
};
private async Task<ExcelImportResult> ImportCampusesAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.Campuses.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
if (code is null || name is null) continue;
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new Campus { Code = code, Name = name };
db.Campuses.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.Address = Optional(row, "地址");
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportCollegesAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.Colleges.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var campuses = await db.Campuses.AsNoTracking().ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
if (code is null || name is null) continue;
Guid? campusId = null;
var campusCode = Optional(row, "所属校区编码");
if (campusCode is not null)
{
if (!campuses.TryGetValue(campusCode, out var campus))
errors.Add($"第 {row.RowNumber} 行:所属校区编码“{campusCode}”不存在。");
else campusId = campus.Id;
}
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new College { Code = code, Name = name };
db.Colleges.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.ShortName = Optional(row, "简称");
entity.CampusId = campusId;
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportMajorsAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.Majors.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var colleges = await db.Colleges.AsNoTracking().ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
var collegeCode = Required(row, "所属学院编码", errors);
var degreeType = Required(row, "学位类型", errors);
var years = ParseInt(row, "学制", 1, 8, errors);
if (code is null || name is null || collegeCode is null ||
degreeType is null || years is null) continue;
if (!colleges.TryGetValue(collegeCode, out var college))
{
errors.Add($"第 {row.RowNumber} 行:所属学院编码“{collegeCode}”不存在。");
continue;
}
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new Major
{
Code = code,
Name = name,
CollegeId = college.Id,
DegreeType = degreeType
};
db.Majors.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.CollegeId = college.Id;
entity.DegreeType = degreeType;
entity.SchoolingYears = years.Value;
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportClassesAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.AdministrativeClasses.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var majors = await db.Majors.AsNoTracking().ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var counselors = await db.Users.AsNoTracking()
.Where(user =>
user.StaffNumber != null &&
user.IsEnabled &&
db.UserRoles.Any(userRole =>
userRole.UserId == user.Id &&
db.Roles.Any(role =>
role.Id == userRole.RoleId &&
role.Name == SystemRoles.Counselor)))
.ToDictionaryAsync(x => x.StaffNumber!, StringComparer.OrdinalIgnoreCase,
cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
var majorCode = Required(row, "所属专业编码", errors);
var grade = ParseInt(row, "年级", 2000, 2200, errors);
if (code is null || name is null || majorCode is null || grade is null) continue;
if (!majors.TryGetValue(majorCode, out var major))
{
errors.Add($"第 {row.RowNumber} 行:所属专业编码“{majorCode}”不存在。");
continue;
}
ApplicationUser? counselor = null;
var counselorNumber = Optional(row, "辅导员工号");
if (counselorNumber is not null &&
!counselors.TryGetValue(counselorNumber, out counselor))
errors.Add($"第 {row.RowNumber} 行:辅导员工号“{counselorNumber}”不存在或已停用。");
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new AdministrativeClass
{
Code = code,
Name = name,
MajorId = major.Id,
Grade = grade.Value
};
db.AdministrativeClasses.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.MajorId = major.Id;
entity.Grade = grade.Value;
entity.CounselorUserId = counselor?.Id;
entity.CounselorName = counselor?.DisplayName;
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportTermsAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.AcademicTerms.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
AcademicTerm? requestedCurrent = null;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
var academicYear = Required(row, "学年", errors);
var season = ParseSeason(row, errors);
var startDate = ParseDate(row, "开始日期", true, errors);
var endDate = ParseDate(row, "结束日期", true, errors);
if (code is null || name is null || academicYear is null || season is null ||
startDate is null || endDate is null) continue;
if (endDate <= startDate)
{
errors.Add($"第 {row.RowNumber} 行:结束日期必须晚于开始日期。");
continue;
}
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new AcademicTerm
{
Code = code,
Name = name,
AcademicYear = academicYear,
Season = season.Value,
StartDate = startDate.Value,
EndDate = endDate.Value
};
db.AcademicTerms.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
entity.Name = name;
entity.AcademicYear = academicYear;
entity.Season = season.Value;
entity.StartDate = startDate.Value;
entity.EndDate = endDate.Value;
entity.IsEnabled = ParseEnabled(row, errors);
entity.IsCurrent = ParseBoolean(row, "当前学期", false, errors);
if (entity.IsCurrent)
{
if (requestedCurrent is not null)
errors.Add($"第 {row.RowNumber} 行:同一次导入只能设置一个当前学期。");
requestedCurrent = entity;
}
}
if (requestedCurrent is not null)
{
foreach (var term in await db.AcademicTerms.Where(x => x.Id != requestedCurrent.Id)
.ToListAsync(cancellationToken))
term.IsCurrent = false;
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportBuildingsAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.Buildings.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var campuses = await db.Campuses.AsNoTracking().ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
var campusCode = Required(row, "所属校区编码", errors);
if (code is null || name is null || campusCode is null) continue;
if (!campuses.TryGetValue(campusCode, out var campus))
{
errors.Add($"第 {row.RowNumber} 行:所属校区编码“{campusCode}”不存在。");
continue;
}
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new Building { Code = code, Name = name, CampusId = campus.Id };
db.Buildings.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.CampusId = campus.Id;
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportClassroomsAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.Classrooms.ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var buildings = await db.Buildings.AsNoTracking().ToDictionaryAsync(x => x.Code,
StringComparer.OrdinalIgnoreCase, cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
var buildingCode = Required(row, "所属教学楼编码", errors);
var capacity = ParseInt(row, "容量", 1, 1000, errors);
var roomType = Required(row, "教室类型", errors);
var venueNature = ParseVenueNature(row, roomType, errors);
if (code is null || name is null || buildingCode is null ||
capacity is null || roomType is null || venueNature is null) continue;
if (!buildings.TryGetValue(buildingCode, out var building))
{
errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。");
continue;
}
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new Classroom
{
Code = code,
Name = name,
BuildingId = building.Id,
RoomType = roomType,
TeachingVenueNature = venueNature.Value
};
db.Classrooms.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
entity.BuildingId = building.Id;
entity.Capacity = capacity.Value;
entity.RoomType = roomType;
entity.TeachingVenueNature = venueNature.Value;
entity.Equipment = Optional(row, "设备");
}
return new(created, updated, rows.Count);
}
private async Task<ExcelImportResult> ImportCourseCategoriesAsync(
IReadOnlyList<ExcelRow> rows,
List<string> errors,
CancellationToken cancellationToken)
{
var existing = await db.CourseCategories.ToDictionaryAsync(
x => x.Code,
StringComparer.OrdinalIgnoreCase,
cancellationToken);
var created = 0;
var updated = 0;
foreach (var row in rows)
{
var code = Required(row, "编码", errors);
var name = Required(row, "名称", errors);
if (code is null || name is null) continue;
var entity = existing.GetValueOrDefault(code);
if (entity is null)
{
entity = new CourseCategory { Code = code, Name = name };
db.CourseCategories.Add(entity);
existing[code] = entity;
created++;
}
else updated++;
ApplyCatalog(entity, row, name, errors);
}
return new(created, updated, rows.Count);
}
private ActionResult ImportValidationProblem(IReadOnlyList<string> errors)
{
foreach (var error in errors.Take(50))
ModelState.AddModelError("file", error);
if (errors.Count > 50)
ModelState.AddModelError("file", $"另有 {errors.Count - 50} 条错误未显示。");
return ValidationProblem(ModelState);
}
private static void ApplyCatalog(
CatalogEntity entity,
ExcelRow row,
string name,
List<string> errors)
{
entity.Name = name;
entity.SortOrder = string.IsNullOrWhiteSpace(row["排序"])
? 0
: ParseInt(row, "排序", 0, int.MaxValue, errors) ?? 0;
entity.IsEnabled = ParseEnabled(row, errors);
}
private static string? Required(ExcelRow row, string header, List<string> errors)
{
var value = Optional(row, header);
if (value is not null) return value;
errors.Add($"第 {row.RowNumber} 行:“{header}”不能为空。");
return null;
}
private static string? Optional(ExcelRow row, string header) =>
string.IsNullOrWhiteSpace(row[header]) ? null : row[header].Trim();
private static int? ParseInt(
ExcelRow row, string header, int minimum, int maximum, List<string> errors)
{
if (int.TryParse(row[header], out var value) && value >= minimum && value <= maximum)
return value;
errors.Add($"第 {row.RowNumber} 行:“{header}”必须是 {minimum} 至 {maximum} 的整数。");
return null;
}
private static DateOnly? ParseDate(
ExcelRow row, string header, bool required, List<string> errors)
{
var text = row[header];
if (string.IsNullOrWhiteSpace(text) && !required) return null;
if (DateOnly.TryParse(text, out var value)) return value;
errors.Add($"第 {row.RowNumber} 行:“{header}”必须是有效日期(如 2026-09-01)。");
return null;
}
private static bool ParseEnabled(ExcelRow row, List<string> errors)
{
var value = row["状态"];
if (value is "启用" or "是" or "1" ||
value.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
if (value is "停用" or "否" or "0" ||
value.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
errors.Add($"第 {row.RowNumber} 行:“状态”请填写“启用”或“停用”。");
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(
ExcelRow row, string header, bool defaultValue, List<string> errors)
{
var value = row[header];
if (string.IsNullOrWhiteSpace(value)) return defaultValue;
if (value is "是" or "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase))
return true;
if (value is "否" or "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase))
return false;
errors.Add($"第 {row.RowNumber} 行:“{header}”请填写“是”或“否”。");
return defaultValue;
}
private static TermSeason? ParseSeason(ExcelRow row, List<string> errors)
{
var value = row["学期季"];
if (value is "秋季" or "秋季学期" ||
value.Equals("Autumn", StringComparison.OrdinalIgnoreCase)) return TermSeason.Autumn;
if (value is "春季" or "春季学期" ||
value.Equals("Spring", StringComparison.OrdinalIgnoreCase)) return TermSeason.Spring;
if (value is "夏季" or "夏季学期" ||
value.Equals("Summer", StringComparison.OrdinalIgnoreCase)) return TermSeason.Summer;
errors.Add($"第 {row.RowNumber} 行:“学期季”请填写秋季、春季或夏季。");
return null;
}
private static bool TryGetHeaders(string kind, out string[] headers) =>
Headers.TryGetValue(kind, out headers!);
private static IReadOnlyList<object?> Row(params object?[] values) => values;
private static string Status(bool enabled) => enabled ? "启用" : "停用";
private static string YesNo(bool value) => value ? "是" : "否";
private static string SeasonName(TermSeason season) => season switch
{
TermSeason.Autumn => "秋季",
TermSeason.Spring => "春季",
_ => "夏季"
};
private static string KindName(string kind) => kind.ToLowerInvariant() switch
{
"campuses" => "校区",
"colleges" => "学院",
"majors" => "专业",
"classes" => "行政班",
"terms" => "学期",
"buildings" => "教学楼",
"classrooms" => "教室",
"course-categories" => "课程分类",
_ => "基础数据"
};
}