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;
+ }
+
+
+