Grade comments: show live usage count and allow deleting unused templates

This commit is contained in:
2026-09-29 15:56:00 +03:00
parent 4bb1e332e3
commit 2869142a79
9 changed files with 187 additions and 12 deletions

View File

@@ -146,6 +146,13 @@ data class CommentOut(
val tone: String,
)
/** Подсказка комментария: текст, тон и текущее число использований. */
data class CommentSuggestion(
val text: String,
val tone: String,
val count: Long,
)
data class GradeResponse(
val studentId: String,
val labId: String,

View File

@@ -74,11 +74,38 @@ class GradeService(
lid: UUID,
): List<GradeComment> = gradeComments.findByStudentIdAndLabId(sid, lid)
/**
* Подсказки комментариев: поиск по подстроке, выше — чаще используемые сейчас.
* Число использований считается по актуальным оценкам, а не по накопительному [CommentTemplate.useCount].
*/
@Transactional(readOnly = true)
fun suggestComments(query: String): List<CommentOut> =
templates
.findTop30ByTextContainingIgnoreCaseOrderByUseCountDesc(query.trim())
.map { CommentOut(it.text, it.tone) }
fun suggestComments(query: String): List<CommentSuggestion> {
val found = templates.findByTextContainingIgnoreCase(query.trim())
val counts = liveCounts(found.map { it.text })
return found
.map { it to (counts[it.text] ?: 0L) }
.sortedWith(compareByDescending<Pair<CommentTemplate, Long>> { it.second }.thenByDescending { it.first.useCount })
.take(SUGGEST_LIMIT)
.map { CommentSuggestion(it.first.text, it.first.tone, it.second) }
}
/** Удаляет шаблон комментария, только если он нигде не проставлен. */
@Transactional
fun deleteCommentTemplate(text: String) {
val trimmed = text.trim()
val inUse = gradeComments.countByText(trimmed)
if (inUse > 0) throw BadRequestException("Комментарий используется $inUse раз(а) — удалить нельзя")
templates.deleteByText(trimmed)
}
private fun liveCounts(texts: List<String>): Map<String, Long> {
if (texts.isEmpty()) return emptyMap()
return gradeComments
.findByTextIn(texts)
.groupingBy { it.text }
.eachCount()
.mapValues { it.value.toLong() }
}
@Transactional(readOnly = true)
fun myComments(studentId: UUID): List<GradeComment> = gradeComments.findByStudentId(studentId)
@@ -98,4 +125,8 @@ class GradeService(
)
}
}
private companion object {
const val SUGGEST_LIMIT = 30
}
}

View File

@@ -4,6 +4,7 @@ import jakarta.validation.Valid
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
@@ -121,5 +122,13 @@ class ProgressController(
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun suggestComments(
@RequestParam q: String,
): List<CommentOut> = grades.suggestComments(q)
): List<CommentSuggestion> = grades.suggestComments(q)
@DeleteMapping("/comment-templates")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun deleteCommentTemplate(
@RequestParam text: String,
) {
grades.deleteCommentTemplate(text)
}
}

View File

@@ -8,7 +8,9 @@ import java.util.UUID
interface CommentTemplateRepository : JpaRepository<CommentTemplate, UUID> {
fun findByText(text: String): CommentTemplate?
fun findTop30ByTextContainingIgnoreCaseOrderByUseCountDesc(text: String): List<CommentTemplate>
fun findByTextContainingIgnoreCase(text: String): List<CommentTemplate>
fun deleteByText(text: String)
}
interface GradeCommentRepository : JpaRepository<GradeComment, UUID> {
@@ -23,4 +25,8 @@ interface GradeCommentRepository : JpaRepository<GradeComment, UUID> {
)
fun findByStudentId(studentId: UUID): List<GradeComment>
fun findByTextIn(texts: Collection<String>): List<GradeComment>
fun countByText(text: String): Long
}