Lint: strict mode on; tune detekt thresholds; more decompositions

This commit is contained in:
2026-09-27 22:15:00 +03:00
parent 5fbd9cb6cb
commit a021bdb904
8 changed files with 621 additions and 505 deletions

View File

@@ -94,6 +94,34 @@ class ProgressService(
request: DistributeRequest,
token: String,
): DistributeResult {
val labIds = resolveLabIds(request)
val force = request.force == true
val allStudents = collectStudentIds(request, token)
var assignedTotal = 0
var totalStudents = 0
var lastLabId: String? = null
var lastLabTitle: String? = null
for (labIdStr in labIds) {
val labId = UUID.fromString(labIdStr)
val lab = academic.getLab(labIdStr, token)
if (force) clearAutoAssignments(labId, allStudents)
val kept = keepExisting(labId, allStudents)
assignedTotal += kept.size
assignedTotal += assignNew(lab, labId, allStudents.filterNot { it in kept })
totalStudents += allStudents.size
lastLabId = labIdStr
lastLabTitle = lab.title
}
return DistributeResult(
labs = labIds.size,
assigned = assignedTotal,
total = totalStudents,
labId = if (labIds.size == 1) lastLabId else null,
labTitle = if (labIds.size == 1) lastLabTitle else null,
)
}
private fun resolveLabIds(request: DistributeRequest): List<String> {
val labIds =
request.labIds
.orEmpty()
@@ -102,8 +130,13 @@ class ProgressService(
.filter { it.isNotBlank() }
.distinct()
if (labIds.isEmpty()) throw BadRequestException("Не указана ни одна лабораторная")
return labIds
}
val force = request.force == true
private fun collectStudentIds(
request: DistributeRequest,
token: String,
): List<UUID> {
val groupFilter =
request.groupIds
.orEmpty()
@@ -113,94 +146,75 @@ class ProgressService(
for (g in identity.listGroups(token)) {
val gid = g["id"] as String
if (groupFilter.isNotEmpty() && gid !in groupFilter) continue
for (s in identity.getGroup(gid, token).students) {
studentIds.add(UUID.fromString(s.id))
}
for (s in identity.getGroup(gid, token).students) studentIds.add(UUID.fromString(s.id))
}
val allStudents = studentIds.distinct()
return studentIds.distinct()
}
var assignedTotal = 0
var totalStudents = 0
var lastLabId: String? = null
var lastLabTitle: String? = null
private fun clearAutoAssignments(
labId: UUID,
allStudents: List<UUID>,
) {
assignments
.findByLabId(labId)
.filter { it.studentId in allStudents && !it.manual }
.forEach { assignments.delete(it) }
}
for (labIdStr in labIds) {
val labId = UUID.fromString(labIdStr)
val lab = academic.getLab(labIdStr, token)
private fun keepExisting(
labId: UUID,
allStudents: List<UUID>,
): Map<UUID, VariantAssignment> {
val kept = mutableMapOf<UUID, VariantAssignment>()
assignments
.findByLabId(labId)
.filter { it.studentId in allStudents }
.forEach { kept[it.studentId] = it }
return kept
}
// force сбрасывает авто-назначения только у выбранных студентов;
// закреплённые вручную варианты остаются как есть.
if (force) {
assignments
.findByLabId(labId)
.filter { it.studentId in allStudents && !it.manual }
.forEach { assignments.delete(it) }
}
val kept = mutableMapOf<UUID, VariantAssignment>()
assignedTotal += assignments.findByLabId(labId).count { it.studentId in allStudents }
/** Выдаёт варианты студентам без назначения; очередь общая, повторы — только когда пул исчерпан. */
private fun assignNew(
lab: LabInfo,
labId: UUID,
toAssign: List<UUID>,
): Int {
val poolsByLevel = lab.levels.associate { level -> level.id to level.variants.map { it.id } }
val defaultLevel = lab.levels.firstOrNull()?.id
if (toAssign.isEmpty() || poolsByLevel.isEmpty()) return 0
val usage =
assignments
.findByLabId(labId)
.filter { it.studentId in allStudents }
.forEach { kept[it.studentId] = it }
val toAssign = allStudents.filterNot { kept.containsKey(it) }
// Уровень ограничивает пул: студент получает вариант только СВОЕГО уровня.
// При этом очередь одна общая для всех выбранных групп: круг не
// «перезапускается» на каждой группе, а продолжается по наименее
// занятым вариантам уровня (пока хоть один не выдан — повторов нет).
val poolsByLevel = lab.levels.associate { lv -> lv.id to lv.variants.map { it.id } }
val defaultLevel = lab.levels.firstOrNull()?.id
if (toAssign.isNotEmpty() && poolsByLevel.isNotEmpty()) {
val usage =
assignments
.findByLabId(labId)
.groupingBy { it.variantId.toString() }
.eachCount()
.toMutableMap()
for (sid in toAssign.shuffled()) {
val levelId =
levels.findByStudentIdAndLabId(sid, labId)?.labLevelId?.toString()
?: defaultLevel
val pool =
levelId
?.let { poolsByLevel[it] }
.orEmpty()
.ifEmpty { defaultLevel?.let { poolsByLevel[it] }.orEmpty() }
.ifEmpty { poolsByLevel.values.firstOrNull { it.isNotEmpty() }.orEmpty() }
if (pool.isEmpty()) continue
val minCount = pool.minOf { usage[it] ?: 0 }
val chosen = pool.filter { (usage[it] ?: 0) == minCount }.random()
assignments.save(
VariantAssignment(
studentId = sid,
labId = labId,
variantId = UUID.fromString(chosen),
manual = false,
),
)
usage[chosen] = (usage[chosen] ?: 0) + 1
assignedTotal++
}
}
totalStudents += allStudents.size
lastLabId = labIdStr
lastLabTitle = lab.title
.groupingBy { it.variantId.toString() }
.eachCount()
.toMutableMap()
var assigned = 0
for (sid in toAssign.shuffled()) {
val levelId = levels.findByStudentIdAndLabId(sid, labId)?.labLevelId?.toString() ?: defaultLevel
val pool = pickPool(levelId, poolsByLevel, defaultLevel)
if (pool.isEmpty()) continue
val minCount = pool.minOf { id -> usage[id] ?: 0 }
val chosen = pool.filter { id -> (usage[id] ?: 0) == minCount }.random()
assignments.save(
VariantAssignment(studentId = sid, labId = labId, variantId = UUID.fromString(chosen), manual = false),
)
usage[chosen] = (usage[chosen] ?: 0) + 1
assigned++
}
return DistributeResult(
labs = labIds.size,
assigned = assignedTotal,
total = totalStudents,
labId = if (labIds.size == 1) lastLabId else null,
labTitle = if (labIds.size == 1) lastLabTitle else null,
)
return assigned
}
private fun pickPool(
levelId: String?,
poolsByLevel: Map<String, List<String>>,
defaultLevel: String?,
): List<String> =
levelId
?.let { poolsByLevel[it] }
.orEmpty()
.ifEmpty { defaultLevel?.let { poolsByLevel[it] }.orEmpty() }
.ifEmpty { poolsByLevel.values.firstOrNull { it.isNotEmpty() }.orEmpty() }
@Transactional(readOnly = true)
fun getAssignmentsForLab(labId: UUID): List<VariantAssignment> = assignments.findByLabId(labId)