课程库 Excel 导入导出已完成:
支持下载标准导入模板。 导出严格沿用当前关键词、学院、分类、课程性质和状态筛选。 以课程编码为唯一键:存在则更新,不存在则新增。 校验学院编码、课程分类、课程性质、学分学时及考核方式。 整批原子导入,任一行错误则全部不写入,并提示具体行号。 学院管理员仍只能维护本学院的专业课和实践课,无法借 Excel 越权。 界面入口沿用现有课程库工具栏样式。
This commit is contained in:
1 parent
5a7a8d530b
commit
d7cf3f9e76
24 files changed
+3709
-31
No files matched your search
@@ -20,6 +20,9 @@ else {
|
|||||||
}
|
}
|
||||||
$stdoutPath = Join-Path $env:TEMP 'jiaowu-api-smoke.out.log'
|
$stdoutPath = Join-Path $env:TEMP 'jiaowu-api-smoke.out.log'
|
||||||
$stderrPath = Join-Path $env:TEMP 'jiaowu-api-smoke.err.log'
|
$stderrPath = Join-Path $env:TEMP 'jiaowu-api-smoke.err.log'
|
||||||
|
$courseExcelToken = [guid]::NewGuid().ToString('N')
|
||||||
|
$courseTemplatePath = Join-Path $env:TEMP "jiaowu-course-template-$courseExcelToken.xlsx"
|
||||||
|
$courseExportPath = Join-Path $env:TEMP "jiaowu-course-export-$courseExcelToken.xlsx"
|
||||||
$env:ASPNETCORE_ENVIRONMENT = 'Development'
|
$env:ASPNETCORE_ENVIRONMENT = 'Development'
|
||||||
$env:ASPNETCORE_URLS = 'http://localhost:5255'
|
$env:ASPNETCORE_URLS = 'http://localhost:5255'
|
||||||
$startParameters = @{
|
$startParameters = @{
|
||||||
@@ -78,6 +81,37 @@ try {
|
|||||||
$teachers = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' -Headers $headers
|
$teachers = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/teachers?page=1&pageSize=10' -Headers $headers
|
||||||
$students = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' -Headers $headers
|
$students = Invoke-RestMethod -Uri 'http://localhost:5255/api/personnel/students?page=1&pageSize=10' -Headers $headers
|
||||||
$courses = Invoke-RestMethod -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' -Headers $headers
|
$courses = Invoke-RestMethod -Uri 'http://localhost:5255/api/courses?page=1&pageSize=10' -Headers $headers
|
||||||
|
$courseCategories = Invoke-RestMethod `
|
||||||
|
-Uri 'http://localhost:5255/api/base-data/course-categories' `
|
||||||
|
-Headers $headers
|
||||||
|
if (@($courseCategories).Count -lt 10 -or
|
||||||
|
@($courses.items | Where-Object { $null -eq $_.courseCategoryId }).Count -gt 0) {
|
||||||
|
throw 'Course categories were not seeded or assigned to all courses.'
|
||||||
|
}
|
||||||
|
Invoke-WebRequest `
|
||||||
|
-Uri 'http://localhost:5255/api/courses/template' `
|
||||||
|
-Headers $headers `
|
||||||
|
-OutFile $courseTemplatePath |
|
||||||
|
Out-Null
|
||||||
|
Invoke-WebRequest `
|
||||||
|
-Uri 'http://localhost:5255/api/courses/export' `
|
||||||
|
-Headers $headers `
|
||||||
|
-OutFile $courseExportPath |
|
||||||
|
Out-Null
|
||||||
|
if ((Get-Item -LiteralPath $courseTemplatePath).Length -lt 1 -or
|
||||||
|
(Get-Item -LiteralPath $courseExportPath).Length -lt 1) {
|
||||||
|
throw 'Course Excel template or export is empty.'
|
||||||
|
}
|
||||||
|
$courseImport = Invoke-RestMethod `
|
||||||
|
-Method Post `
|
||||||
|
-Uri 'http://localhost:5255/api/courses/import' `
|
||||||
|
-Headers $headers `
|
||||||
|
-Form @{ file = Get-Item -LiteralPath $courseExportPath }
|
||||||
|
if ($courseImport.created -ne 0 -or
|
||||||
|
$courseImport.updated -ne $courses.total -or
|
||||||
|
$courseImport.total -ne $courses.total) {
|
||||||
|
throw 'Course Excel round-trip did not update the expected records.'
|
||||||
|
}
|
||||||
$curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers
|
$curriculumPlans = Invoke-RestMethod -Uri 'http://localhost:5255/api/curriculum-plans?page=1&pageSize=10' -Headers $headers
|
||||||
if ($curriculumPlans.total -gt 0) {
|
if ($curriculumPlans.total -gt 0) {
|
||||||
$curriculumDetail = Invoke-RestMethod `
|
$curriculumDetail = Invoke-RestMethod `
|
||||||
@@ -757,6 +791,8 @@ try {
|
|||||||
Teachers = $teachers.total
|
Teachers = $teachers.total
|
||||||
Students = $students.total
|
Students = $students.total
|
||||||
Courses = $courses.total
|
Courses = $courses.total
|
||||||
|
CourseCategories = @($courseCategories).Count
|
||||||
|
CourseExcelRows = $courseImport.total
|
||||||
Plans = $curriculumPlans.total
|
Plans = $curriculumPlans.total
|
||||||
PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 }
|
PlanModules = if ($null -ne $curriculumDetail) { @($curriculumDetail.modules).Count } else { 0 }
|
||||||
TeachingTasks = $teachingTasks.total
|
TeachingTasks = $teachingTasks.total
|
||||||
@@ -790,4 +826,6 @@ finally {
|
|||||||
if (-not $process.HasExited) {
|
if (-not $process.HasExited) {
|
||||||
Stop-Process -Id $process.Id -Force
|
Stop-Process -Id $process.Id -Force
|
||||||
}
|
}
|
||||||
|
Remove-Item -LiteralPath $courseTemplatePath -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -LiteralPath $courseExportPath -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
@@ -120,6 +120,44 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
|||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken));
|
.ToListAsync(cancellationToken));
|
||||||
|
|
||||||
|
[HttpGet("course-categories")]
|
||||||
|
public async Task<ActionResult<IReadOnlyCollection<CourseCategory>>> GetCourseCategories(
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
await db.CourseCategories.AsNoTracking()
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Code)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
[HttpPost("course-categories")]
|
||||||
|
[Authorize(Roles = Administrators)]
|
||||||
|
public async Task<ActionResult<CourseCategory>> CreateCourseCategory(
|
||||||
|
CatalogRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = new CourseCategory
|
||||||
|
{
|
||||||
|
Code = request.Code.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
SortOrder = request.SortOrder,
|
||||||
|
IsEnabled = request.IsEnabled
|
||||||
|
};
|
||||||
|
return await CreateAsync(entity, nameof(GetCourseCategories), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("course-categories/{id:guid}")]
|
||||||
|
[Authorize(Roles = Administrators)]
|
||||||
|
public async Task<ActionResult<CourseCategory>> UpdateCourseCategory(
|
||||||
|
Guid id,
|
||||||
|
CatalogRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await db.CourseCategories.FindAsync([id], cancellationToken);
|
||||||
|
if (entity is null) return NotFound();
|
||||||
|
ApplyCatalog(entity, request);
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("majors")]
|
[HttpPost("majors")]
|
||||||
[Authorize(Roles = Administrators)]
|
[Authorize(Roles = Administrators)]
|
||||||
public async Task<ActionResult<Major>> CreateMajor(
|
public async Task<ActionResult<Major>> CreateMajor(
|
||||||
@@ -403,6 +441,7 @@ public sealed class BaseDataController(AppDbContext db) : ControllerBase
|
|||||||
"terms" => await db.AcademicTerms.FindAsync([id], cancellationToken),
|
"terms" => await db.AcademicTerms.FindAsync([id], cancellationToken),
|
||||||
"buildings" => await db.Buildings.FindAsync([id], cancellationToken),
|
"buildings" => await db.Buildings.FindAsync([id], cancellationToken),
|
||||||
"classrooms" => await db.Classrooms.FindAsync([id], cancellationToken),
|
"classrooms" => await db.Classrooms.FindAsync([id], cancellationToken),
|
||||||
|
"course-categories" => await db.CourseCategories.FindAsync([id], cancellationToken),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
|||||||
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
|
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
|
||||||
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
|
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
|
||||||
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
|
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
|
||||||
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"]
|
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"],
|
||||||
|
["course-categories"] = ["编码", "名称", "排序", "状态"]
|
||||||
};
|
};
|
||||||
|
|
||||||
[HttpGet("{kind}/template")]
|
[HttpGet("{kind}/template")]
|
||||||
@@ -90,6 +91,8 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
|||||||
"terms" => await ImportTermsAsync(rows, errors, cancellationToken),
|
"terms" => await ImportTermsAsync(rows, errors, cancellationToken),
|
||||||
"buildings" => await ImportBuildingsAsync(rows, errors, cancellationToken),
|
"buildings" => await ImportBuildingsAsync(rows, errors, cancellationToken),
|
||||||
"classrooms" => await ImportClassroomsAsync(rows, errors, cancellationToken),
|
"classrooms" => await ImportClassroomsAsync(rows, errors, cancellationToken),
|
||||||
|
"course-categories" => await ImportCourseCategoriesAsync(
|
||||||
|
rows, errors, cancellationToken),
|
||||||
_ => throw new InvalidOperationException()
|
_ => throw new InvalidOperationException()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,6 +150,10 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
|||||||
.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,
|
||||||
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
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(),
|
||||||
_ => []
|
_ => []
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -471,6 +478,36 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
|||||||
return new(created, updated, rows.Count);
|
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)
|
private ActionResult ImportValidationProblem(IReadOnlyList<string> errors)
|
||||||
{
|
{
|
||||||
foreach (var error in errors.Take(50))
|
foreach (var error in errors.Take(50))
|
||||||
@@ -582,6 +619,7 @@ public sealed class BaseDataExcelController(AppDbContext db) : ControllerBase
|
|||||||
"terms" => "学期",
|
"terms" => "学期",
|
||||||
"buildings" => "教学楼",
|
"buildings" => "教学楼",
|
||||||
"classrooms" => "教室",
|
"classrooms" => "教室",
|
||||||
|
"course-categories" => "课程分类",
|
||||||
_ => "基础数据"
|
_ => "基础数据"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Loaded 3 of 24 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user