diff --git a/.gitignore b/.gitignore index d7e9863..5465356 100644 --- a/.gitignore +++ b/.gitignore @@ -290,29 +290,5 @@ Network Trash Folder Temporary Items .apdisk -### Kotlin template -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - +### Custom +*.db diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 25a6c14..3406ced 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -5,18 +5,10 @@ - - + - - - - - - - diff --git a/build.gradle.kts b/build.gradle.kts index 0a76c33..12a04c4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,6 @@ plugins { kotlin("plugin.jpa") version kotlinVersion kotlin("plugin.allopen") version kotlinVersion kotlin("plugin.spring") version kotlinVersion - kotlin("plugin.lombok") version kotlinVersion - id("io.freefair.lombok") version "8.11" id("org.springframework.boot") version "3.4.1" id("io.spring.dependency-management") version "1.1.7" } @@ -20,10 +18,8 @@ java { } repositories { - maven { - url = uri("http://localhost:3105/threepartrepo") - isAllowInsecureProtocol = true - } + maven("https://maven.aliyun.com/repository/central") + mavenCentral() } dependencies { diff --git a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Application.kt b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Application.kt index dbc0720..d4791ea 100644 --- a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Application.kt +++ b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Application.kt @@ -3,15 +3,34 @@ package com.lanyuanxiaoyao.bookstore import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource +import org.springframework.web.filter.CorsFilter @SpringBootApplication +@EnableJpaAuditing class BookstoreApplication fun main(args: Array) { runApplication(*args) +} +@Configuration +class WebConfiguration { @Bean - fun bookstore(): List { - return emptyList() + fun corsFilter(): CorsFilter { + val config = CorsConfiguration().apply { + allowCredentials = true + addAllowedOriginPattern("*") + addAllowedHeader("*") + addAllowedMethod("*") + maxAge = 3600L + } + val source = UrlBasedCorsConfigurationSource().apply { + registerCorsConfiguration("/**", config) + } + return CorsFilter(source) } } diff --git a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Controller.kt b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Controller.kt index 4337387..6d71efe 100644 --- a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Controller.kt +++ b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Controller.kt @@ -1,12 +1,253 @@ package com.lanyuanxiaoyao.bookstore -import lombok.extern.slf4j.Slf4j -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import cn.hutool.core.util.IdUtil +import jakarta.annotation.Resource +import jakarta.transaction.Transactional +import org.slf4j.LoggerFactory +import org.springframework.data.domain.PageRequest +import org.springframework.web.bind.annotation.* + +data class PageResponse( + val items: List, + val total: Long, +) -@Slf4j @RestController @RequestMapping("book") class BookController { + private val log = LoggerFactory.getLogger(javaClass) -} \ No newline at end of file + @Resource + private lateinit var bookRepository: BookRepository + + @GetMapping("list") + fun list( + @RequestParam("page", defaultValue = "1") page: Int, + @RequestParam("size", defaultValue = "10") size: Int + ): PageResponse { + val pageable = bookRepository.findAll(PageRequest.of(0.coerceAtLeast(page - 1), size)) + return PageResponse( + pageable.content.map { ViewItem(it) }, + pageable.totalElements, + ) + } + + @Transactional + @PostMapping("save") + fun save(@RequestBody item: ViewItem) { + bookRepository.save( + Book( + bookId = item.bookId ?: IdUtil.fastSimpleUUID(), + name = item.name, + author = item.author, + description = item.description, + source = item.source, + tags = item.tags?.toMutableSet() ?: mutableSetOf() + ) + ) + } + + @GetMapping("detail/{bookId}") + fun detail(@PathVariable("bookId") bookId: String): ViewItem { + return bookRepository.findById(bookId).orElseThrow().let { ViewItem(it) } + } + + @Transactional + @GetMapping("remove/{bookId}") + fun remove(@PathVariable("bookId") bookId: String) { + bookRepository.deleteById(bookId) + } + + @GetMapping("tags") + fun tags(): List { + return bookRepository.findAllTag() + } + + data class ViewItem( + val bookId: String?, + val name: String, + val author: String?, + val description: String, + val source: String?, + val tags: Set?, + ) { + constructor(book: Book) : this( + book.bookId, + book.name, + book.author, + book.description, + book.source, + book.tags, + ) + } +} + +@RestController +@RequestMapping("chapter") +class ChapterController { + private val log = LoggerFactory.getLogger(javaClass) + + @Resource + private lateinit var bookRepository: BookRepository + + @Resource + private lateinit var chapterRepository: ChapterRepository + + @Resource + private lateinit var lineRepository: LineRepository + + @GetMapping("list/{bookId}") + fun list( + @PathVariable("bookId") bookId: String, + @RequestParam("page", defaultValue = "1") page: Int, + @RequestParam("size", defaultValue = "10") size: Int + ): PageResponse { + val pageable = chapterRepository.findAll({ root, _, builder -> + builder.equal(root.get("book").get("bookId"), bookId) + }, PageRequest.of(0.coerceAtLeast(page - 1), size)) + return PageResponse( + pageable.content.map { ViewItem(it) }, + pageable.totalElements, + ) + } + + @Transactional + @PostMapping("save/{bookId}") + fun save(@PathVariable("bookId") bookId: String, @RequestBody item: ViewItem) { + val book = bookRepository.findById(bookId).orElseThrow() + chapterRepository.save( + Chapter( + chapterId = item.chapterId ?: IdUtil.fastSimpleUUID(), + name = item.name, + sequence = item.sequence, + description = item.description, + book = book, + ) + ) + } + + @Transactional + @PostMapping("import/{chapterId}") + fun import(@PathVariable("chapterId") chapterId: String, @RequestBody item: ImportItem) { + val chapter = chapterRepository.findById(chapterId).orElseThrow() + var startIndex = chapter.content.size.toLong() + if (item.override) { + lineRepository.deleteAllInBatch(chapter.content) + startIndex = 0 + } + chapter.content = item.text + .split("\n") + .mapIndexed { index, line -> + Line( + lineId = IdUtil.fastSimpleUUID(), + sequence = startIndex + index, + text = line.trim(), + chapter = chapter, + ) + } + .toMutableSet() + chapterRepository.save(chapter) + } + + @GetMapping("detail/{chapterId}") + fun detail(@PathVariable("chapterId") chapterId: String): ViewItem { + return chapterRepository.findById(chapterId).orElseThrow().let { ViewItem(it) } + } + + @Transactional + @GetMapping("remove/{chapterId}") + fun remove(@PathVariable("chapterId") chapterId: String) { + chapterRepository.deleteById(chapterId) + } + + data class ViewItem( + val chapterId: String?, + val name: String?, + val sequence: Int, + val description: String?, + ) { + constructor(chapter: Chapter) : this( + chapter.chapterId, + chapter.name, + chapter.sequence, + chapter.description, + ) + } + + data class ImportItem( + val override: Boolean = false, + val text: String + ) +} + +@RestController +@RequestMapping("line") +class LineController { + private val log = LoggerFactory.getLogger(javaClass) + + @Resource + private lateinit var chapterRepository: ChapterRepository + + @Resource + private lateinit var lineRepository: LineRepository + + @GetMapping("list/{chapterId}") + fun list( + @PathVariable("chapterId") chapterId: String, + @RequestParam("page", defaultValue = "1") page: Int, + @RequestParam("size", defaultValue = "10") size: Int + ): PageResponse { + val pageable = lineRepository.findAll({ root, _, builder -> + builder.equal(root.get("chapter").get("chapterId"), chapterId) + }, PageRequest.of(0.coerceAtLeast(page - 1), size)) + return PageResponse( + pageable.content.map { ViewItem(it) }, + pageable.totalElements, + ) + } + + @Transactional + @PostMapping("save/{lineId}") + fun save(@PathVariable("lineId") lineId: String, @RequestBody item: ViewItem) { + val chapter = chapterRepository.findById(lineId).orElseThrow() + lineRepository.save( + Line( + lineId = item.lineId ?: IdUtil.fastSimpleUUID(), + sequence = item.sequence, + text = item.text, + chapter = chapter, + ) + ) + } + + @Transactional + @PostMapping("update/{lineId}") + fun update(@PathVariable("lineId") lineId: String, @RequestBody item: ViewItem) { + val line = lineRepository.findById(lineId).orElseThrow() + line.text = item.text + lineRepository.save(line) + } + + @GetMapping("detail/{lineId}") + fun detail(@PathVariable("lineId") lineId: String): ViewItem { + return lineRepository.findById(lineId).orElseThrow().let { ViewItem(it) } + } + + @Transactional + @GetMapping("remove/{lineId}") + fun remove(@PathVariable("lineId") lineId: String) { + lineRepository.deleteById(lineId) + } + + data class ViewItem( + val lineId: String?, + val sequence: Long, + val text: String, + ) { + constructor(line: Line) : this( + line.lineId, + line.sequence, + line.text, + ) + } +} diff --git a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Entity.kt b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Entity.kt index ff28a33..d5321f5 100644 --- a/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Entity.kt +++ b/src/main/kotlin/com/lanyuanxiaoyao/bookstore/Entity.kt @@ -1,44 +1,85 @@ +@file:Suppress("FunctionName") + package com.lanyuanxiaoyao.bookstore -import jakarta.persistence.CascadeType -import jakarta.persistence.ConstraintMode -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.ForeignKey -import jakarta.persistence.Id -import jakarta.persistence.JoinColumn -import jakarta.persistence.Lob -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToMany +import jakarta.persistence.* +import org.hibernate.annotations.DynamicUpdate +import org.springframework.data.jpa.repository.EntityGraph +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.JpaSpecificationExecutor +import org.springframework.data.jpa.repository.Query +import org.springframework.stereotype.Repository @Entity +@DynamicUpdate +@NamedEntityGraph( + name = "book.list", attributeNodes = [ + NamedAttributeNode("tags"), + NamedAttributeNode("chapters"), + ] +) class Book( @Id - var id: String, + var bookId: String, + @Column(nullable = false) var name: String, - var author: String, + var author: String?, + @Column(nullable = false) var description: String, - var tags: List, - @OneToMany(cascade = [CascadeType.REMOVE], fetch = FetchType.LAZY, mappedBy = "book") - var chapters: List, + var source: String?, + @ElementCollection(fetch = FetchType.EAGER) + var tags: MutableSet = mutableSetOf(), + @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, mappedBy = "book") + var chapters: MutableSet = mutableSetOf(), ) +@Repository +interface BookRepository : JpaRepository, JpaSpecificationExecutor { + @EntityGraph("book.list") + override fun findAll(): List + + @Query("select distinct book.tags from Book book") + fun findAllTag(): List +} + @Entity +@DynamicUpdate +@NamedEntityGraph( + name = "chapter.list", attributeNodes = [ + NamedAttributeNode("content"), + ] +) class Chapter( @Id - var id: String, + var chapterId: String, + @Column(nullable = false) var sequence: Int, - var name: String, - var content: List, + var name: String?, + var description: String?, @ManyToOne(cascade = [CascadeType.DETACH], fetch = FetchType.LAZY) @JoinColumn(nullable = false, foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) var book: Book, + @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, mappedBy = "chapter") + var content: MutableSet = mutableSetOf(), ) +@Repository +interface ChapterRepository : JpaRepository, JpaSpecificationExecutor + +@Entity +@DynamicUpdate class Line( @Id - var id: String, - var sequence: Int, + var lineId: String, + @Column(nullable = false) + var sequence: Long, @Lob + @Column(nullable = false) var text: String, + @ManyToOne(cascade = [CascadeType.DETACH], fetch = FetchType.LAZY) + @JoinColumn(nullable = false, foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) + var chapter: Chapter ) + +@Repository +interface LineRepository : JpaRepository, JpaSpecificationExecutor diff --git a/src/main/resources/static/components/book.js b/src/main/resources/static/components/book.js new file mode 100644 index 0000000..b393db0 --- /dev/null +++ b/src/main/resources/static/components/book.js @@ -0,0 +1,90 @@ +function bookForm() { + return { + debug: '${debug}', + type: 'form', + ...horizontalFormOptions(), + body: [ + { + type: 'hidden', + name: 'id', + }, + { + type: 'input-text', + name: 'name', + label: '名称', + required: true, + ...formInputClearable(), + }, + { + type: 'input-text', + name: 'author', + label: '作者', + ...formInputClearable(), + }, + { + type: 'input-text', + name: 'source', + label: '来源', + ...formInputClearable(), + validations: { + isUrl: true, + }, + }, + { + type: 'input-tag', + name: 'tags', + label: '标签', + clearable: true, + joinValues: false, + extractValue: true, + max: 10, + maxTagLength: 10, + maxTagCount: 5, + source: '${base}/book/tags' + }, + { + type: 'textarea', + name: 'description', + label: '简介', + required: true, + ...formInputClearable(), + showCounter: true, + trimContents: true, + minRows: 2, + maxRows: 2, + maxLength: 100, + }, + ], + } +} + +function bookAddDialog() { + return { + type: 'action', + actionType: 'dialog', + dialog: { + title: '新增书籍', + size: 'md', + body: { + ...bookForm(), + api: '${base}/book/save', + }, + }, + } +} + +function bookDetailDialog() { + return { + type: 'action', + actionType: 'dialog', + dialog: { + title: '编辑书籍', + size: 'md', + body: { + ...bookForm(), + initApi: '${base}/book/detail/${bookId}', + api: '${base}/book/save', + }, + }, + } +} diff --git a/src/main/resources/static/components/chapter.js b/src/main/resources/static/components/chapter.js new file mode 100644 index 0000000..193f0df --- /dev/null +++ b/src/main/resources/static/components/chapter.js @@ -0,0 +1,69 @@ +function chapterForm() { + return { + debug: '${debug}', + type: 'form', + ...horizontalFormOptions(), + body: [ + { + type: 'hidden', + name: 'id', + }, + { + type: 'input-number', + name: 'sequence', + label: '章节数', + required: true, + step: 1, + precision: 0, + }, + { + type: 'input-text', + name: 'name', + label: '名称', + ...formInputClearable(), + }, + { + type: 'textarea', + name: 'description', + label: '简介', + ...formInputClearable(), + showCounter: true, + trimContents: true, + minRows: 2, + maxRows: 2, + maxLength: 100, + }, + ], + } +} + +function chapterAddDialog() { + return { + type: 'action', + actionType: 'dialog', + dialog: { + title: '新增章节', + size: 'md', + body: { + ...chapterForm(), + api: '${base}/chapter/save/${bookId}', + }, + }, + } +} + +function chapterDetailDialog() { + return { + type: 'action', + actionType: 'dialog', + dialog: { + title: '编辑章节', + size: 'md', + body: { + ...chapterForm(), + initApi: '${base}/chapter/detail/${chapterId}', + api: '${base}/chapter/save/${bookId}', + }, + }, + } +} \ No newline at end of file diff --git a/src/main/resources/static/components/helper.js b/src/main/resources/static/components/helper.js new file mode 100644 index 0000000..2164b9f --- /dev/null +++ b/src/main/resources/static/components/helper.js @@ -0,0 +1,36 @@ +function crudCommonOptions() { + return { + affixHeader: false, + stopAutoRefreshWhenModalIsOpen: true, + resizable: false, + syncLocation: false, + silentPolling: true, + } +} + +function horizontalFormOptions() { + return { + mode: 'horizontal', + canAccessSuperData: false, + horizontal: { + left: 1, + }, + } +} + +function formInputClearable() { + return { + clearable: true, + clearValueOnEmpty: true, + } +} + +function paginationOption() { + return { + type: 'pagination', + mode: 'normal', + layout: 'total,perPage,pager', + maxButtons: 5, + showPageInput: false, + } +} diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 622a4f4..0de89c7 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -17,40 +17,287 @@ margin: 0; padding: 0; } + + textarea { + resize: none !important; + }
+ + + \ No newline at end of file