Content import: sync sections and articles (archive is the source of truth)

- importZip now updates titles/positions of existing chapters and sections and
  removes sections/articles of archived chapters that are missing in the archive
  (so restructured material no longer leaves stale sections behind); removed
  count is reported in ContentImportResult and in the admin flash message
- docs: note about import semantics in the class diagram and sequences
This commit is contained in:
2026-09-16 22:00:22 +03:00
parent da464f882a
commit a1392a469e
5 changed files with 54 additions and 4 deletions

View File

@@ -59,6 +59,8 @@ data class ContentImportResult(
val sections: Int,
val articles: Int,
val assets: Int,
/** Сколько устаревших разделов и статей удалено при синхронизации. */
val removed: Int = 0,
)
/**
@@ -453,24 +455,38 @@ class ContentService(
var chCount = 0
var secCount = 0
var artCount = 0
var removedCount = 0
var currentChapter: ContentChapter? = null
var currentSection: ContentSection? = null
val touchedChapters = mutableSetOf<UUID>()
val seenSections = mutableSetOf<UUID>()
val seenArticles = mutableSetOf<UUID>()
for (p in parsed) {
if (currentChapter?.title != p.chapterDir) {
val slug = chapterSlug(p) ?: "chapter-${chCount + 1}"
currentChapter = chapters.findBySlug(slug) ?: chapters.save(
currentChapter = chapters.findBySlug(slug)?.apply {
title = p.chapterDir
position = p.chapterPos
} ?: chapters.save(
ContentChapter(title = p.chapterDir, slug = slug, position = p.chapterPos),
)
currentChapter = chapters.save(currentChapter!!)
touchedChapters.add(currentChapter!!.id)
chCount += 1
currentSection = null
}
val chapter = currentChapter ?: throw BadRequestException("Не удалось определить главу")
if (currentSection?.title != p.sectionDir) {
val slug = sectionSlug(p) ?: "section-${secCount + 1}"
currentSection = sections.findByChapterIdAndSlug(chapter.id, slug) ?: sections.save(
currentSection = sections.findByChapterIdAndSlug(chapter.id, slug)?.apply {
title = p.sectionDir
position = p.sectionPos
} ?: sections.save(
ContentSection(chapterId = chapter.id, title = p.sectionDir, slug = slug, position = p.sectionPos),
)
currentSection = sections.save(currentSection!!)
seenSections.add(currentSection!!.id)
secCount += 1
}
val section = currentSection ?: throw BadRequestException("Не удалось определить раздел")
@@ -489,10 +505,34 @@ class ContentService(
position = p.articlePos
}
articles.save(article)
seenArticles.add(article.id)
artCount += 1
}
return ContentImportResult(chapters = chCount, sections = secCount, articles = artCount, assets = linkRewrite.size)
// Синхронизация: в главах из архива удаляем разделы и статьи, которых в архиве нет.
for (chapterId in touchedChapters) {
for (section in sections.findByChapterIdOrderByPositionAscIdAsc(chapterId)) {
if (section.id in seenSections) {
for (article in articles.findBySectionIdOrderByPositionAscIdAsc(section.id)) {
if (article.id !in seenArticles) {
articles.delete(article)
removedCount++
}
}
} else {
removedCount += articles.findBySectionIdOrderByPositionAscIdAsc(section.id).size + 1
sections.delete(section)
}
}
}
return ContentImportResult(
chapters = chCount,
sections = secCount,
articles = artCount,
assets = linkRewrite.size,
removed = removedCount,
)
}
private fun chapterSlug(p: ParsedArticle): String? =