修正实验室选择
This commit is contained in:
1 parent
57372cf615
commit
90877ae428
3 files changed
+110
-42
No files matched your search
@@ -503,12 +503,32 @@ public sealed class ExperimentsController(
|
||||
x.CourseId != first.CourseId))
|
||||
return ValidationProblem("批量设置仅支持同一学期、同一课程的教学任务。");
|
||||
|
||||
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
|
||||
request.ProjectCount is < 1 or > 100)
|
||||
return ValidationProblem("自行安排一次可生成 1 至 100 个实验项目。");
|
||||
|
||||
var code = request.Code.Trim();
|
||||
var names = NormalizeBatchProjectNames(request.Names);
|
||||
var definitionCount = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
|
||||
? request.ProjectCount
|
||||
: 1;
|
||||
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
|
||||
names is not null && names.Count != definitionCount)
|
||||
return ValidationProblem($"已填写 {names.Count} 个项目名称,请将生成项目数设为相同数量。");
|
||||
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
|
||||
taskIds.Count * definitionCount > 1000)
|
||||
return ValidationProblem("本次最多生成 1000 个自行安排实验项目,请缩小教学任务或项目数量。");
|
||||
|
||||
var projectCodes = Enumerable.Range(1, definitionCount)
|
||||
.Select(index => ProjectCode(code, index, definitionCount))
|
||||
.ToList();
|
||||
if (projectCodes.Any(item => item.Length > 40))
|
||||
return ValidationProblem("项目编码前缀过长,无法追加顺序号。");
|
||||
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled)
|
||||
{
|
||||
var conflictingTaskNumbers = await db.ExperimentProjects.AsNoTracking()
|
||||
.WhereIn(taskIds, x => x.TeachingTaskId)
|
||||
.Where(x => x.Code == code)
|
||||
.Where(x => projectCodes.Contains(x.Code))
|
||||
.Select(x => x.TeachingTask!.TaskNumber)
|
||||
.OrderBy(x => x)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -565,7 +585,7 @@ public sealed class ExperimentsController(
|
||||
return ConflictProblem("所选实验课中已存在相同实验项目编码和周次,不能重复生成。");
|
||||
}
|
||||
|
||||
var projects = new List<ExperimentProject>(tasks.Count);
|
||||
var projects = new List<ExperimentProject>(tasks.Count * definitionCount);
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
var item = request.ForTeachingTask(task.Id);
|
||||
@@ -594,43 +614,57 @@ public sealed class ExperimentsController(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
projects.Add(new ExperimentProject
|
||||
for (var definitionIndex = 1; definitionIndex <= definitionCount; definitionIndex++)
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
ScheduleEntryId = occurrence.Entry?.Id,
|
||||
ScheduleWeek = occurrence.Week,
|
||||
Code = code,
|
||||
Name = request.Name.Trim(),
|
||||
ArrangementMode = request.ArrangementMode,
|
||||
Description = Normalize(request.Description),
|
||||
Requirements = Normalize(request.Requirements),
|
||||
StartDate = request.StartDate,
|
||||
EndDate = request.EndDate,
|
||||
SelectionStartsAt = NormalizeSelectionTime(request.SelectionStartsAt),
|
||||
SelectionEndsAt = NormalizeSelectionTime(request.SelectionEndsAt)
|
||||
});
|
||||
projects.Add(new ExperimentProject
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
ScheduleEntryId = occurrence.Entry?.Id,
|
||||
ScheduleWeek = occurrence.Week,
|
||||
Code = ProjectCode(code, definitionIndex, definitionCount),
|
||||
Name = request.Name.Trim(),
|
||||
ArrangementMode = request.ArrangementMode,
|
||||
Description = Normalize(request.Description),
|
||||
Requirements = Normalize(request.Requirements),
|
||||
StartDate = request.StartDate,
|
||||
EndDate = request.EndDate,
|
||||
SelectionStartsAt = NormalizeSelectionTime(request.SelectionStartsAt),
|
||||
SelectionEndsAt = NormalizeSelectionTime(request.SelectionEndsAt)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var names = NormalizeBatchProjectNames(request.Names);
|
||||
if (request.GenerateSequentialNames)
|
||||
{
|
||||
if (names is not null)
|
||||
return ValidationProblem("自动生成顺序号时无需填写逐行项目名称。");
|
||||
var width = Math.Max(2, projects.Count.ToString().Length);
|
||||
var width = Math.Max(2, (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
|
||||
? definitionCount
|
||||
: projects.Count).ToString().Length);
|
||||
if (request.Name.Trim().Length + width + 1 > 120)
|
||||
return ValidationProblem("项目名称前缀过长,无法追加顺序号。");
|
||||
for (var index = 0; index < projects.Count; index++)
|
||||
projects[index].Name = $"{request.Name.Trim()} {(index + 1).ToString($"D{width}")}";
|
||||
{
|
||||
var number = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
|
||||
? index % definitionCount + 1
|
||||
: index + 1;
|
||||
projects[index].Name = $"{request.Name.Trim()} {number.ToString($"D{width}")}";
|
||||
}
|
||||
}
|
||||
else if (names is not null)
|
||||
{
|
||||
if (names.Any(name => string.IsNullOrWhiteSpace(name) || name.Length > 120))
|
||||
return ValidationProblem("每个实验项目名称均不能为空,且不能超过 120 个字符。");
|
||||
if (names.Count != projects.Count)
|
||||
var expectedNameCount = request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
|
||||
? definitionCount
|
||||
: projects.Count;
|
||||
if (names.Count != expectedNameCount)
|
||||
return ValidationProblem($"已填写 {names.Count} 个项目名称,但本次将生成 {projects.Count} 个实验项目;请确保每行对应一个项目。");
|
||||
for (var index = 0; index < projects.Count; index++)
|
||||
projects[index].Name = names[index];
|
||||
projects[index].Name = names[request.ArrangementMode == ExperimentArrangementMode.SelfScheduled
|
||||
? index % definitionCount
|
||||
: index];
|
||||
}
|
||||
|
||||
db.ExperimentProjects.AddRange(projects);
|
||||
@@ -1370,9 +1404,7 @@ public sealed class ExperimentsController(
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.Id == request.ClassroomId && x.IsEnabled,
|
||||
cancellationToken);
|
||||
if (classroom is null) return "实验教室不存在或已停用。";
|
||||
if (!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
|
||||
return "所选场地未标注实验教学性质。";
|
||||
if (classroom is null) return "实验场地不存在或已停用。";
|
||||
if (request.Capacity > classroom.Capacity)
|
||||
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
|
||||
if (project.ArrangementMode ==
|
||||
@@ -1737,6 +1769,11 @@ public sealed class ExperimentsController(
|
||||
return names.Select(name => name.Trim()).ToList();
|
||||
}
|
||||
|
||||
private static string ProjectCode(string prefix, int index, int count) =>
|
||||
count == 1
|
||||
? prefix
|
||||
: $"{prefix}-{index.ToString($"D{Math.Max(2, count.ToString().Length)}")}";
|
||||
|
||||
private ActionResult ConflictProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
@@ -1776,7 +1813,8 @@ public sealed record ExperimentProjectBatchRequest(
|
||||
IReadOnlyList<string>? Names = null,
|
||||
bool GenerateSequentialNames = false,
|
||||
DateTime? SelectionStartsAt = null,
|
||||
DateTime? SelectionEndsAt = null)
|
||||
DateTime? SelectionEndsAt = null,
|
||||
[Range(1, 100)] int ProjectCount = 1)
|
||||
{
|
||||
public ExperimentProjectRequest ForTeachingTask(Guid teachingTaskId) =>
|
||||
new(
|
||||
|
||||
@@ -45,24 +45,28 @@ public sealed class ExperimentsControllerTests
|
||||
|
||||
var pasted = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
|
||||
{
|
||||
Names = ["实验一", "实验二"]
|
||||
Names = ["实验一", "实验二"],
|
||||
ProjectCount = 2
|
||||
};
|
||||
Assert.IsType<CreatedResult>(await controller.CreateProjects(pasted, CancellationToken.None));
|
||||
Assert.Equal(["实验一", "实验二"], await fixture.Db.ExperimentProjects
|
||||
.OrderBy(x => x.TeachingTask!.TaskNumber)
|
||||
Assert.Equal(["实验一", "实验二", "实验一", "实验二"], await fixture.Db.ExperimentProjects
|
||||
.OrderBy(x => x.TeachingTask!.TaskNumber).ThenBy(x => x.Code)
|
||||
.Select(x => x.Name)
|
||||
.ToListAsync());
|
||||
Assert.Equal(["LAB-BATCH-01", "LAB-BATCH-02"], await fixture.Db.ExperimentProjects
|
||||
.Select(x => x.Code).Distinct().OrderBy(x => x).ToListAsync());
|
||||
|
||||
fixture.Db.ExperimentProjects.RemoveRange(fixture.Db.ExperimentProjects);
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
var sequential = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
|
||||
{
|
||||
Name = "数据库实验",
|
||||
GenerateSequentialNames = true
|
||||
GenerateSequentialNames = true,
|
||||
ProjectCount = 3
|
||||
};
|
||||
Assert.IsType<CreatedResult>(await controller.CreateProjects(sequential, CancellationToken.None));
|
||||
Assert.Equal(["数据库实验 01", "数据库实验 02"], await fixture.Db.ExperimentProjects
|
||||
.OrderBy(x => x.TeachingTask!.TaskNumber)
|
||||
Assert.Equal(["数据库实验 01", "数据库实验 02", "数据库实验 03", "数据库实验 01", "数据库实验 02", "数据库实验 03"], await fixture.Db.ExperimentProjects
|
||||
.OrderBy(x => x.TeachingTask!.TaskNumber).ThenBy(x => x.Code)
|
||||
.Select(x => x.Name)
|
||||
.ToListAsync());
|
||||
}
|
||||
@@ -254,6 +258,23 @@ public sealed class ExperimentsControllerTests
|
||||
await fixture.Controller(fixture.StudentScope).Book(session.Id, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfScheduledSession_AllowsAnyEnabledTeachingVenue()
|
||||
{
|
||||
await using var fixture = await ExperimentFixture.CreateAsync();
|
||||
fixture.Classroom.TeachingVenueNature = TeachingVenueNature.GeneralClassroom;
|
||||
await fixture.Db.SaveChangesAsync();
|
||||
var manager = fixture.Controller(fixture.ManagerScope);
|
||||
|
||||
await manager.CreateProject(
|
||||
fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
|
||||
CancellationToken.None);
|
||||
var project = await fixture.Db.ExperimentProjects.SingleAsync();
|
||||
|
||||
Assert.IsType<CreatedResult>(await manager.CreateSession(
|
||||
project.Id, fixture.SessionRequest(1, 2), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BoundInstructor_CanCancelOwnSession()
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ const projectForm = reactive({
|
||||
name: '',
|
||||
projectNames: '',
|
||||
nameMode: 'Sequence',
|
||||
projectCount: 1,
|
||||
arrangementMode: 'Centralized',
|
||||
description: '',
|
||||
requirements: '',
|
||||
@@ -142,6 +143,7 @@ const activePeriods = computed(() =>
|
||||
|| period.academicTermId === selectedProject.value.academicTermId,
|
||||
),
|
||||
)
|
||||
const experimentClassrooms = computed(() => options.classrooms)
|
||||
|
||||
const modeMeta: Record<string, { label: string; note: string }> = {
|
||||
Centralized: {
|
||||
@@ -170,6 +172,7 @@ function resetProjectForm() {
|
||||
name: '',
|
||||
projectNames: '',
|
||||
nameMode: 'Sequence',
|
||||
projectCount: 1,
|
||||
arrangementMode: 'Centralized',
|
||||
description: '',
|
||||
requirements: '',
|
||||
@@ -252,6 +255,9 @@ async function saveProject() {
|
||||
scheduleEntryId: projectForm.scheduleEntryId || null,
|
||||
code: projectForm.code,
|
||||
name: projectForm.nameMode === 'InputNames' && !editingProjectId.value ? names[0] : projectForm.name,
|
||||
projectCount: projectForm.arrangementMode === 'SelfScheduled'
|
||||
? (projectForm.nameMode === 'InputNames' ? names.length : projectForm.projectCount)
|
||||
: 1,
|
||||
arrangementMode: projectForm.arrangementMode,
|
||||
description: projectForm.description || null,
|
||||
requirements: projectForm.requirements || null,
|
||||
@@ -283,7 +289,7 @@ async function saveProject() {
|
||||
})
|
||||
ElMessage.success(projectForm.arrangementMode === 'Centralized'
|
||||
? `已按 ${projectForm.teachingTaskIds.length} 个教学班的全部实验课生成项目`
|
||||
: `已为 ${projectForm.teachingTaskIds.length} 个教学任务创建实验项目`)
|
||||
: `已为 ${projectForm.teachingTaskIds.length} 个教学任务创建 ${projectForm.teachingTaskIds.length * payload.projectCount} 个自选实验项目`)
|
||||
}
|
||||
projectDialog.value = false
|
||||
await load()
|
||||
@@ -1117,9 +1123,8 @@ onMounted(async () => {
|
||||
:key="task.id"
|
||||
:value="task.id"
|
||||
:disabled="projectForm.arrangementMode === 'SelfScheduled'
|
||||
&& (task.schedulingMode !== 'Flexible'
|
||||
|| (!!selectedTask && (task.academicTermId !== selectedTask.academicTermId
|
||||
|| task.courseId !== selectedTask.courseId)))"
|
||||
&& !!selectedTask && (task.academicTermId !== selectedTask.academicTermId
|
||||
|| task.courseId !== selectedTask.courseId)"
|
||||
>
|
||||
<b>{{ task.taskNumber }}</b>
|
||||
<span>{{ task.classNames.join('、') || task.name }}</span>
|
||||
@@ -1130,7 +1135,7 @@ onMounted(async () => {
|
||||
</el-checkbox-group>
|
||||
<small v-if="!editingProjectId" class="form-help">
|
||||
<template v-if="projectForm.arrangementMode === 'Centralized'">已选 {{ projectForm.teachingTaskIds.length }} 个教学班;系统会为每个教学班的全部已发布实验课生成项目。</template>
|
||||
<template v-else>自行安排仅可选择“非排时课程”的教学任务;只能勾选同一学期、同一课程,实验编码、名称、内容和开放日期将一次应用到这些教学任务。</template>
|
||||
<template v-else>自行安排不写入课表;只能勾选同一学期、同一课程。每个项目都可另行设置多个可预约场次。</template>
|
||||
</small>
|
||||
</el-form-item>
|
||||
<div class="form-grid two">
|
||||
@@ -1151,6 +1156,10 @@ onMounted(async () => {
|
||||
<el-form-item v-if="projectForm.nameMode === 'Sequence'" label="项目名称前缀" required>
|
||||
<el-input v-model="projectForm.name" maxlength="116" placeholder="如 数据库实验;将生成“数据库实验 01、数据库实验 02”" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="projectForm.arrangementMode === 'SelfScheduled' && projectForm.nameMode === 'Sequence'" label="生成项目数" required>
|
||||
<el-input-number v-model="projectForm.projectCount" :min="1" :max="100" />
|
||||
<small class="form-help">同一门课程会生成 {{ projectForm.projectCount }} 个自选项目;编码和名称均自动追加顺序号。</small>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="实验项目名称" required>
|
||||
<el-input
|
||||
v-model="projectForm.projectNames"
|
||||
@@ -1159,7 +1168,7 @@ onMounted(async () => {
|
||||
maxlength="120000"
|
||||
placeholder="每行一个名称,可直接从 Excel 粘贴;系统按实际生成顺序逐行对应"
|
||||
/>
|
||||
<small class="form-help">已输入 {{ projectNameLines(projectForm.projectNames).length }} 行有效名称。</small>
|
||||
<small class="form-help">已输入 {{ projectNameLines(projectForm.projectNames).length }} 行有效名称;同一门课程将按这些名称生成多个自选项目。</small>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1295,10 +1304,10 @@ onMounted(async () => {
|
||||
<el-input-number v-model="sessionForm.capacity" :min="1" :max="10000" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="实验室" required>
|
||||
<el-select v-model="sessionForm.classroomId" filterable placeholder="选择实验室或教学场所">
|
||||
<el-form-item label="实验场地" required>
|
||||
<el-select v-model="sessionForm.classroomId" filterable placeholder="选择教学场地">
|
||||
<el-option
|
||||
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
|
||||
v-for="room in experimentClassrooms"
|
||||
:key="room.id"
|
||||
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
||||
:value="room.id"
|
||||
@@ -1388,9 +1397,9 @@ onMounted(async () => {
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number v-model="row.periodCount" :min="1" :max="12" controls-position="right" />
|
||||
<el-select v-model="row.classroomId" filterable placeholder="实验室">
|
||||
<el-select v-model="row.classroomId" filterable placeholder="实验场地">
|
||||
<el-option
|
||||
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
|
||||
v-for="room in experimentClassrooms"
|
||||
:key="room.id"
|
||||
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
||||
:value="room.id"
|
||||
|
||||
Reference in new issue
Block a user