1
0

feat: 完成基本功能

This commit is contained in:
2024-12-22 01:33:46 +08:00
parent 779cd23b8b
commit 0c979985ed
10 changed files with 803 additions and 96 deletions

28
.gitignore vendored
View File

@@ -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

10
.idea/compiler.xml generated
View File

@@ -5,18 +5,10 @@
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-configuration-processor/3.4.1/8dfcdae21f559be9c8a4d6d515e77cfd1d9c06a8/spring-boot-configuration-processor-3.4.1.jar" />
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" />
<entry name="$USER_HOME$/scoop/apps/gradle/current/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-configuration-processor/3.4.1/8dfcdae21f559be9c8a4d6d515e77cfd1d9c06a8/spring-boot-configuration-processor-3.4.1.jar" />
</processorPath>
<module name="bookstore.main" />
</profile>
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" />
</processorPath>
<module name="bookstore.test" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="17" />
</component>

View File

@@ -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 {

View File

@@ -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<String>) {
runApplication<BookstoreApplication>(*args)
}
@Configuration
class WebConfiguration {
@Bean
fun bookstore(): List<Book> {
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)
}
}

View File

@@ -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<E>(
val items: List<E>,
val total: Long,
)
@Slf4j
@RestController
@RequestMapping("book")
class BookController {
private val log = LoggerFactory.getLogger(javaClass)
@Resource
private lateinit var bookRepository: BookRepository
@GetMapping("list")
fun list(
@RequestParam("page", defaultValue = "1") page: Int,
@RequestParam("size", defaultValue = "10") size: Int
): PageResponse<ViewItem> {
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<String> {
return bookRepository.findAllTag()
}
data class ViewItem(
val bookId: String?,
val name: String,
val author: String?,
val description: String,
val source: String?,
val tags: Set<String>?,
) {
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<ViewItem> {
val pageable = chapterRepository.findAll({ root, _, builder ->
builder.equal(root.get<Book>("book").get<String>("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<ViewItem> {
val pageable = lineRepository.findAll({ root, _, builder ->
builder.equal(root.get<Chapter>("chapter").get<String>("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,
)
}
}

View File

@@ -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<String>,
@OneToMany(cascade = [CascadeType.REMOVE], fetch = FetchType.LAZY, mappedBy = "book")
var chapters: List<Chapter>,
var source: String?,
@ElementCollection(fetch = FetchType.EAGER)
var tags: MutableSet<String> = mutableSetOf(),
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY, mappedBy = "book")
var chapters: MutableSet<Chapter> = mutableSetOf(),
)
@Repository
interface BookRepository : JpaRepository<Book, String>, JpaSpecificationExecutor<Book> {
@EntityGraph("book.list")
override fun findAll(): List<Book>
@Query("select distinct book.tags from Book book")
fun findAllTag(): List<String>
}
@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<Line>,
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<Line> = mutableSetOf(),
)
@Repository
interface ChapterRepository : JpaRepository<Chapter, String>, JpaSpecificationExecutor<Chapter>
@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<Line, String>, JpaSpecificationExecutor<Line>

View File

@@ -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',
},
},
}
}

View File

@@ -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}',
},
},
}
}

View File

@@ -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,
}
}

View File

@@ -17,40 +17,287 @@
margin: 0;
padding: 0;
}
textarea {
resize: none !important;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
<script src="sdk/sdk.js"></script>
<script src="components/helper.js"></script>
<script src="components/book.js"></script>
<script src="components/chapter.js"></script>
<script>
(function () {
let amis = amisRequire('amis/embed')
let amisJSON = {
type: 'page',
title: '书籍中心',
subTitle: '网络书籍精排版工具',
body: [
'Hello World'
]
}
let debug = false
amis.embed(
'#root',
amisJSON,
{
data: {
base: 'http://127.0.0.1:23890'
},
},
{
theme: 'antd',
enableAMISDebug: debug,
},
);
if (debug) {
console.log('Source', amisJSON)
}
})()
(function () {
let debug = true
let amis = amisRequire('amis/embed')
let amisJSON = {
type: 'page',
title: '书籍中心',
subTitle: '网络书籍精排版工具',
body: [
{
type: 'crud',
api: {
method: 'get',
url: '${base}/book/list',
data: {
page: '${page|default:1}',
size: '${size|default:10}'
}
},
...crudCommonOptions(),
headerToolbar: [
'reload',
{
label: '',
icon: 'fa fa-add',
...bookAddDialog(),
},
],
footerToolbar: [
paginationOption(),
],
columns: [
{
name: 'name',
label: '名称',
width: 150,
},
{
name: 'description',
label: '描述',
},
{
name: 'source',
label: '来源',
width: 200,
},
{
type: 'operation',
label: '操作',
fixed: 'right',
className: 'nowrap',
width: 300,
buttons: [
{
type: 'action',
label: '跳转',
},
{
type: 'action',
label: '编辑',
...bookDetailDialog(),
},
{
type: 'action',
label: '编辑章节',
actionType: 'dialog',
dialog: {
title: '章节',
size: 'lg',
actions: [],
body: [
{
type: 'crud',
...crudCommonOptions(),
api: {
method: 'get',
url: '${base}/chapter/list/${bookId}',
data: {
page: '${page|default:1}',
size: '${size|default:10}'
}
},
headerToolbar: [
'reload',
{
label: '',
icon: 'fa fa-add',
...chapterAddDialog(),
},
],
footerToolbar: [
paginationOption(),
],
columns: [
{
name: 'sequence',
label: '章节数',
width: 50,
},
{
name: 'name',
label: '名称',
width: 150,
},
{
name: 'description',
label: '简介',
},
{
type: 'operation',
label: '操作',
fixed: 'right',
className: 'nowrap',
width: 200,
buttons: [
{
type: 'action',
label: '编辑',
...chapterDetailDialog(),
},
{
type: 'action',
label: '导入',
actionType: 'dialog',
dialog: {
title: '导入正文',
size: 'lg',
body: {
debug: '${debug}',
type: 'form',
api: '${base}/chapter/import/${chapterId}',
mode: 'normal',
body: [
{
type: 'switch',
name: 'override',
label: '覆盖导入',
},
{
type: 'textarea',
name: 'text',
label: '正文',
required: true,
...formInputClearable(),
showCounter: true,
trimContents: true,
minRows: 10,
maxRows: 10,
},
],
},
},
},
{
type: 'action',
label: '编辑正文',
actionType: 'dialog',
dialog: {
title: '编辑正文',
size: 'lg',
actions: [],
body: {
type: 'crud',
...crudCommonOptions(),
api: {
method: 'get',
url: '${base}/line/list/${chapterId}',
data: {
page: '${page|default:1}',
size: '${size|default:10}'
}
},
quickSaveItemApi: '${base}/line/update/${lineId}',
headerToolbar: [
'reload',
],
footerToolbar: [
paginationOption(),
],
columns: [
{
name: 'sequence',
label: '行号',
width: 50,
},
{
name: 'text',
label: '内容',
quickEdit: {
saveImmediately: true,
resetOnFailed: true,
type: 'textarea',
showCounter: true,
trimContents: true,
minRows: 10,
maxRows: 10,
},
},
{
type: 'operation',
label: '操作',
fixed: 'right',
className: 'nowrap',
width: 100,
buttons: [
{
type: 'action',
label: '删除',
level: 'danger',
confirmTitle: '确认删除',
confirmText: '确认删除当前行吗?',
actionType: 'ajax',
api: 'get:${base}/line/remove/${lineId}',
},
]
}
],
},
},
},
{
type: 'action',
label: '删除',
level: 'danger',
confirmTitle: '确认删除',
confirmText: '确认删除名称为「${name}」的章节吗?',
actionType: 'ajax',
api: 'get:${base}/chapter/remove/${chapterId}',
},
],
},
],
},
],
},
},
{
type: 'action',
label: '删除',
level: 'danger',
confirmTitle: '确认删除',
confirmText: '确认删除名称为「${name}」的书籍吗?',
actionType: 'ajax',
api: 'get:${base}/book/remove/${bookId}',
},
],
},
],
},
],
}
amis.embed(
'#root',
amisJSON,
{
data: {
base: 'http://127.0.0.1:23890',
debug: debug,
},
},
{
theme: 'antd',
enableAMISDebug: debug,
},
);
if (debug) {
console.log('Source', amisJSON)
}
})()
</script>
</html>