Prevent duplicate equivalency rules and support safe deletion
This commit is contained in:
1 parent
73f870221b
commit
370a82d4d4
2 files changed
+56
-2
No files matched your search
@@ -40,6 +40,8 @@ public sealed class CourseEquivalencyRulesController(
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
||||||
return ValidationProblem("适用专业不在当前维护范围内。");
|
return ValidationProblem("适用专业不在当前维护范围内。");
|
||||||
|
if (await HasSameScopeRuleAsync(request, null, token))
|
||||||
|
return ConflictProblem();
|
||||||
var rule = new CourseEquivalencyRule { OriginalCourseId = request.OriginalCourseId, SubstituteCourseId = request.SubstituteCourseId,
|
var rule = new CourseEquivalencyRule { OriginalCourseId = request.OriginalCourseId, SubstituteCourseId = request.SubstituteCourseId,
|
||||||
MajorId = request.MajorId, EffectiveGrade = request.EffectiveGrade, IsEnabled = request.IsEnabled, Notes = request.Notes?.Trim() };
|
MajorId = request.MajorId, EffectiveGrade = request.EffectiveGrade, IsEnabled = request.IsEnabled, Notes = request.Notes?.Trim() };
|
||||||
db.CourseEquivalencyRules.Add(rule);
|
db.CourseEquivalencyRules.Add(rule);
|
||||||
@@ -58,11 +60,48 @@ public sealed class CourseEquivalencyRulesController(
|
|||||||
var collegeId = scope.Current.RestrictedCollegeId;
|
var collegeId = scope.Current.RestrictedCollegeId;
|
||||||
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
if (request.MajorId.HasValue && !await db.Majors.AnyAsync(x => x.Id == request.MajorId && (!collegeId.HasValue || x.CollegeId == collegeId), token))
|
||||||
return ValidationProblem("适用专业不在当前维护范围内。");
|
return ValidationProblem("适用专业不在当前维护范围内。");
|
||||||
|
if (await HasSameScopeRuleAsync(request, id, token))
|
||||||
|
return ConflictProblem();
|
||||||
rule.MajorId = request.MajorId; rule.EffectiveGrade = request.EffectiveGrade; rule.IsEnabled = request.IsEnabled; rule.Notes = request.Notes?.Trim();
|
rule.MajorId = request.MajorId; rule.EffectiveGrade = request.EffectiveGrade; rule.IsEnabled = request.IsEnabled; rule.Notes = request.Notes?.Trim();
|
||||||
await db.SaveChangesAsync(token);
|
await db.SaveChangesAsync(token);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
public async Task<ActionResult> Delete(Guid id, CancellationToken token)
|
||||||
|
{
|
||||||
|
var rule = await Scoped(scope.Current.RestrictedCollegeId)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
|
if (rule is null) return NotFound();
|
||||||
|
if (await db.CourseSubstitutions.AnyAsync(
|
||||||
|
x => x.CourseEquivalencyRuleId == id, token))
|
||||||
|
return Conflict(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "该规则已有课程替代申请引用,不能删除;请改为停用以保留审批和毕业审核留痕。",
|
||||||
|
Status = StatusCodes.Status409Conflict
|
||||||
|
});
|
||||||
|
db.CourseEquivalencyRules.Remove(rule);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> HasSameScopeRuleAsync(
|
||||||
|
CourseEquivalencyRuleRequest request,
|
||||||
|
Guid? exceptId,
|
||||||
|
CancellationToken token) => db.CourseEquivalencyRules.AnyAsync(x =>
|
||||||
|
x.OriginalCourseId == request.OriginalCourseId &&
|
||||||
|
x.SubstituteCourseId == request.SubstituteCourseId &&
|
||||||
|
x.MajorId == request.MajorId &&
|
||||||
|
x.EffectiveGrade == request.EffectiveGrade &&
|
||||||
|
(!exceptId.HasValue || x.Id != exceptId.Value), token);
|
||||||
|
|
||||||
|
private static ConflictObjectResult ConflictProblem() =>
|
||||||
|
new(new ProblemDetails
|
||||||
|
{
|
||||||
|
Title = "相同适用范围的等价课程规则已存在。",
|
||||||
|
Status = StatusCodes.Status409Conflict
|
||||||
|
});
|
||||||
|
|
||||||
private IQueryable<CourseEquivalencyRule> Scoped(Guid? collegeId) => collegeId.HasValue
|
private IQueryable<CourseEquivalencyRule> Scoped(Guid? collegeId) => collegeId.HasValue
|
||||||
? db.CourseEquivalencyRules.Where(x => x.OriginalCourse!.CollegeId == collegeId.Value)
|
? db.CourseEquivalencyRules.Where(x => x.OriginalCourse!.CollegeId == collegeId.Value)
|
||||||
: db.CourseEquivalencyRules;
|
: db.CourseEquivalencyRules;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
|
|
||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
@@ -37,6 +37,21 @@ async function toggle(row: any) {
|
|||||||
await load()
|
await load()
|
||||||
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
|
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function remove(row: any) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认删除“${row.substituteCourseName} → ${row.originalCourseName}”规则吗?未被申请引用的规则才可删除。`,
|
||||||
|
'删除等价课程规则',
|
||||||
|
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||||
|
)
|
||||||
|
await http.delete(`/course-equivalency-rules/${row.id}`)
|
||||||
|
ElMessage.success('规则已删除')
|
||||||
|
await load()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -44,6 +59,6 @@ onMounted(load)
|
|||||||
<div class="page-stack" v-loading="loading">
|
<div class="page-stack" v-loading="loading">
|
||||||
<section class="page-intro"><div><span class="section-kicker">EQUIVALENCY RULES</span><h2>等价课程规则</h2><p>先维护可追溯的等价关系;学生只能按适用规则申请课程替代。</p></div></section>
|
<section class="page-intro"><div><span class="section-kicker">EQUIVALENCY RULES</span><h2>等价课程规则</h2><p>先维护可追溯的等价关系;学生只能按适用规则申请课程替代。</p></div></section>
|
||||||
<el-card><el-form :inline="true" @submit.prevent="create"><el-form-item label="被替代课程"><el-select v-model="form.originalCourseId" filterable placeholder="选择课程" style="width:240px"><el-option v-for="course in courses" :key="course.id" :label="`${course.code} · ${course.name}`" :value="course.id" /></el-select></el-form-item><el-form-item label="等价课程"><el-select v-model="form.substituteCourseId" filterable placeholder="选择课程" style="width:240px"><el-option v-for="course in courses" :key="course.id" :label="`${course.code} · ${course.name}`" :value="course.id" /></el-select></el-form-item><el-form-item label="适用年级"><el-input-number v-model="form.effectiveGrade" :min="2000" :max="2100" controls-position="right" placeholder="全部" /></el-form-item><el-form-item label="说明"><el-input v-model="form.notes" maxlength="500" /></el-form-item><el-button type="primary" native-type="submit">建立规则</el-button></el-form></el-card>
|
<el-card><el-form :inline="true" @submit.prevent="create"><el-form-item label="被替代课程"><el-select v-model="form.originalCourseId" filterable placeholder="选择课程" style="width:240px"><el-option v-for="course in courses" :key="course.id" :label="`${course.code} · ${course.name}`" :value="course.id" /></el-select></el-form-item><el-form-item label="等价课程"><el-select v-model="form.substituteCourseId" filterable placeholder="选择课程" style="width:240px"><el-option v-for="course in courses" :key="course.id" :label="`${course.code} · ${course.name}`" :value="course.id" /></el-select></el-form-item><el-form-item label="适用年级"><el-input-number v-model="form.effectiveGrade" :min="2000" :max="2100" controls-position="right" placeholder="全部" /></el-form-item><el-form-item label="说明"><el-input v-model="form.notes" maxlength="500" /></el-form-item><el-button type="primary" native-type="submit">建立规则</el-button></el-form></el-card>
|
||||||
<el-table :data="rows" stripe><el-table-column label="等价关系" min-width="300"><template #default="{ row }">{{ row.substituteCourseCode }} {{ row.substituteCourseName }} → {{ row.originalCourseCode }} {{ row.originalCourseName }}</template></el-table-column><el-table-column label="适用范围" min-width="160"><template #default="{ row }">{{ row.majorName || '全部专业' }} · {{ row.effectiveGrade || '全部年级' }}</template></el-table-column><el-table-column prop="notes" label="说明" min-width="180" /><el-table-column label="状态" width="130"><template #default="{ row }"><el-switch :model-value="row.isEnabled" active-text="启用" inactive-text="停用" @change="toggle(row)" /></template></el-table-column></el-table>
|
<el-table :data="rows" stripe><el-table-column label="等价关系" min-width="300"><template #default="{ row }">{{ row.substituteCourseCode }} {{ row.substituteCourseName }} → {{ row.originalCourseCode }} {{ row.originalCourseName }}</template></el-table-column><el-table-column label="适用范围" min-width="160"><template #default="{ row }">{{ row.majorName || '全部专业' }} · {{ row.effectiveGrade || '全部年级' }}</template></el-table-column><el-table-column prop="notes" label="说明" min-width="180" /><el-table-column label="状态" width="130"><template #default="{ row }"><el-switch :model-value="row.isEnabled" active-text="启用" inactive-text="停用" @change="toggle(row)" /></template></el-table-column><el-table-column label="操作" width="85" fixed="right"><template #default="{ row }"><el-button link type="danger" @click="remove(row)">删除</el-button></template></el-table-column></el-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
Reference in new issue
Block a user