feat(perf): пагинация аудита и bulk-группы курса

This commit is contained in:
2026-10-28 13:26:29 +03:00
parent 3adfba9bec
commit 4ef6c7f7ed
16 changed files with 472 additions and 49 deletions

View File

@@ -17,5 +17,6 @@ class AuditController(
fun list(
@RequestParam(defaultValue = "100") limit: Int,
@RequestParam(required = false) action: String?,
): List<AuditEventDto> = auditService.list(limit, action)
@RequestParam(defaultValue = "0") page: Int,
): List<AuditEventDto> = auditService.list(limit, action, page)
}

View File

@@ -38,15 +38,17 @@ class AuditService(
fun list(
limit: Int = DEFAULT_LIMIT,
action: String? = null,
page: Int = 0,
): List<AuditEventDto> {
val size = limit.coerceIn(1, MAX_LIMIT)
val page = PageRequest.of(0, size)
val pageIndex = page.coerceAtLeast(0)
val pageable = PageRequest.of(pageIndex, size)
val filter = action?.trim()?.takeIf { it.isNotEmpty() }
val found =
if (filter == null) {
events.findAllByOrderByOccurredAtDescIdDesc(page)
events.findAllByOrderByOccurredAtDescIdDesc(pageable)
} else {
events.findAllByActionOrderByOccurredAtDescIdDesc(filter, page)
events.findAllByActionOrderByOccurredAtDescIdDesc(filter, pageable)
}
return found.map(::toDto)
}

View File

@@ -29,6 +29,7 @@ import ru.ysu.lms.identity.repo.AccountRepository
import ru.ysu.lms.identity.roster.CreateGroupRequest
import ru.ysu.lms.identity.roster.CreateStudentRequest
import ru.ysu.lms.identity.roster.GroupService
import java.nio.charset.StandardCharsets
import java.util.UUID
@SpringBootTest(properties = ["lms.promotion.scheduler-enabled=false"])
@@ -198,6 +199,33 @@ class AuditIntegrationTest {
}
}
@Test
fun `pagination returns disjoint pages newest first`() {
repeat(5) { index ->
write(AuditEventRequest(action = "AUDIT_PAGE", entityType = "STUDENT", details = "page-${index + 1}"))
}
val first = detailsOfPage(limit = 2, page = 0)
val second = detailsOfPage(limit = 2, page = 1)
val third = detailsOfPage(limit = 2, page = 2)
assertEquals(listOf("page-5", "page-4"), first)
assertEquals(listOf("page-3", "page-2"), second)
assertEquals(listOf("page-1"), third)
assertTrue(first.intersect(second.toSet()).isEmpty())
assertTrue((first + second).intersect(third.toSet()).isEmpty())
}
@Test
fun `negative page is clamped to the first page and page past the end is empty`() {
repeat(3) { index ->
write(AuditEventRequest(action = "AUDIT_PAGE_CLAMP", entityType = "STUDENT", details = "page-${index + 1}"))
}
assertEquals(listOf("page-3", "page-2"), detailsOfPage(limit = 2, page = -5))
assertEquals(emptyList<String>(), detailsOfPage(limit = 2, page = 1000))
}
@Test
fun `blank action or entity type is rejected`() {
writeExpectingBadRequest(AuditEventRequest(action = " ", entityType = "STUDENT"))
@@ -291,6 +319,24 @@ class AuditIntegrationTest {
assertTrue(auditEvents.findById(deleteEvent.id).isPresent)
}
private fun detailsOfPage(
limit: Int,
page: Int,
): List<String> {
val body =
mockMvc
.get("/api/v1/audit") {
with(admin())
param("limit", limit.toString())
param("page", page.toString())
}.andExpect {
status { isOk() }
}.andReturn()
.response
.getContentAsString(StandardCharsets.UTF_8)
return objectMapper.readTree(body).map { it.path("details").asText() }
}
private fun write(request: AuditEventRequest) {
mockMvc
.post("/api/v1/internal/audit") {