1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
46b1aa8853 feat: 实现整书导出 2025-09-29 14:09:24 +08:00
cf5f7470c6 feat: 章节增加修改功能 2025-09-29 13:50:26 +08:00
9e9f65da76 feat: 数据库转移到mysql 2025-09-29 13:49:54 +08:00
12 changed files with 200 additions and 80 deletions

8
.idea/GitCommitMessageStorage.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitCommitMessageStorage">
<option name="messageStorage">
<MessageStorage />
</option>
</component>
</project>

16
.idea/csv-editor.xml generated Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CsvFileAttributes">
<option name="attributeMap">
<map>
<entry key="/dumpDataPreview">
<value>
<Attribute>
<option name="separator" value="," />
</Attribute>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -9,6 +9,48 @@ import {
time,
} from '../../util/amis.tsx'
const detailDialog = (bookId: string | undefined) => {
return {
title: '添加书架',
size: 'md',
body: {
debug: commonInfo.debug,
type: 'form',
api: `${commonInfo.baseUrl}/chapter/save`,
initApi: `${commonInfo.baseUrl}/chapter/detail/\${id}`,
initFetchOn: '${id}',
...horizontalFormOptions(),
canAccessSuperData: false,
body: [
{
type: 'hidden',
name: 'bookId',
value: bookId,
},
{
type: 'input-number',
name: 'sequence',
label: '序号',
required: true,
},
{
type: 'input-text',
name: 'name',
label: '名称',
required: true,
clearable: true,
},
{
type: 'textarea',
name: 'description',
label: '描述',
clearable: true,
},
],
},
}
}
function Book() {
const navigate = useNavigate()
const {id} = useParams()
@@ -49,11 +91,11 @@ function Book() {
sort: [
{
column: 'sequence',
direction: 'ASC',
direction: 'DESC',
},
{
column: 'modifiedTime',
direction: 'DESC',
direction: 'ASC',
},
],
},
@@ -70,7 +112,9 @@ function Book() {
actionType: 'ajax',
tooltip: '序号重排',
tooltipPlacement: 'top',
api: `get:${commonInfo.baseUrl}/chapter/generate_sequence`
api: `get:${commonInfo.baseUrl}/chapter/generate_sequence`,
confirmText: '确认重排序号?',
confirmTitle: '序号重拍',
},
{
type: 'action',
@@ -142,7 +186,7 @@ function Book() {
actionType: 'dialog',
dialog: detailDialog(),
},*/
]
],
),
columns: [
{
@@ -193,14 +237,14 @@ function Book() {
},
},
},
/*{
{
type: 'action',
label: '修改',
level: 'link',
size: 'sm',
actionType: 'dialog',
dialog: detailDialog(),
},*/
dialog: detailDialog(id),
},
{
className: 'text-danger btn-deleted',
type: 'action',

View File

@@ -96,7 +96,7 @@ function Bookshelf() {
{
name: 'name',
label: '书名',
width: 150,
width: 120,
fixed: 'left',
},
{
@@ -112,7 +112,7 @@ function Bookshelf() {
{
name: 'source',
label: '来源',
width: 150,
width: 200,
},
{
name: 'tags',
@@ -159,6 +159,14 @@ function Bookshelf() {
actionType: 'dialog',
dialog: detailDialog(),
},
{
type: 'action',
label: '导出',
level: 'link',
size: 'sm',
actionType: 'download',
api: `${commonInfo.baseUrl}/book/export/\${id}`,
},
{
className: 'text-danger btn-deleted',
type: 'action',

View File

@@ -1,6 +1,13 @@
import React from 'react'
import {useParams} from 'react-router'
import {amisRender, commonInfo, crudCommonOptions, horizontalFormOptions, paginationTemplate} from '../../util/amis.tsx'
import {
amisRender,
commonInfo,
crudCommonOptions,
horizontalFormOptions,
paginationTemplate,
readOnlyDialogOptions,
} from '../../util/amis.tsx'
function Chapter() {
// const navigate = useNavigate()
@@ -52,19 +59,46 @@ function Chapter() {
},
...crudCommonOptions(),
...paginationTemplate(
undefined,
50,
undefined,
[
{
type: 'action',
label: '',
icon: 'fa fa-rotate-right',
icon: 'fa fa-book-open-reader',
actionType: 'ajax',
tooltip: '序号重排',
tooltipPlacement: 'top',
api: `get:${commonInfo.baseUrl}/line/generate_sequence/${id}`
}
]
api: `get:${commonInfo.baseUrl}/line/generate_sequence/${id}`,
confirmText: '确认重排序号?',
confirmTitle: '序号重拍',
},
{
type: 'action',
label: '',
icon: 'fa fa-glasses',
actionType: 'dialog',
tooltip: '全文阅读',
tooltipPlacement: 'top',
dialog: {
title: '全文查看',
size: 'md',
...readOnlyDialogOptions(),
body: {
type: 'service',
size: 'none',
api: `${commonInfo.baseUrl}/chapter/content/${id}`,
body: {
type: 'markdown',
value: '${detail}',
options: {
breaks: true,
},
},
},
},
},
],
),
columns: [
{

View File

@@ -58,6 +58,11 @@
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>

View File

@@ -1,23 +1,10 @@
package com.lanyuanxiaoyao.bookstore;
import cn.hutool.core.lang.Tuple;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import com.blinkfox.fenix.EnableFenix;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.bookstore.service.BookService;
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
import jakarta.annotation.Resource;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.stream.Collectors;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.transaction.annotation.Transactional;
/**
* 启动类
@@ -33,44 +20,4 @@ public class BookStoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookStoreApplication.class, args);
}
@Resource
private BookService bookService;
@Resource
private ChapterService chapterService;
@Transactional(rollbackFor = Throwable.class)
// @EventListener(ApplicationReadyEvent.class)
public void loadOldData() throws FileNotFoundException {
var reader = CsvUtil.getReader(new FileReader("C:\\Users\\lanyuanxiaoyao\\Result_6.csv"));
var rows = reader.stream()
.map(row -> new Row(Long.parseLong(row.get(0)), row.get(1), Long.parseLong(row.get(2)), row.get(3)))
.toList();
var book = bookService.detailOrThrow(3602572744994816L);
rows.stream()
.map(row -> new Tuple(row.chapterSequence(), row.chapterTitle()))
.distinct()
.forEach(tuple -> {
var chapter = new Chapter();
chapter.setSequence(NumberUtil.toDouble(tuple.get(0)));
chapter.setName(tuple.get(1));
chapter.setBook(book);
var lines = rows.stream()
.filter(row -> ObjectUtil.equals(row.chapterSequence(), tuple.get(0)) && ObjectUtil.equals(row.chapterTitle(), tuple.get(1)))
.map(row -> {
var line = new Line();
line.setSequence(NumberUtil.toDouble(row.lineSequence()));
line.setText(row.lineText());
line.setChapter(chapter);
return line;
})
.collect(Collectors.toSet());
chapter.setContent(lines);
chapterService.save(chapter);
});
}
public record Row(long chapterSequence, String chapterTitle, long lineSequence, String lineText) {
}
}

View File

@@ -1,15 +1,25 @@
package com.lanyuanxiaoyao.bookstore.controller;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.lanyuanxiaoyao.bookstore.entity.Book;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.service.BookService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -29,6 +39,39 @@ public class BookController extends SimpleControllerSupport<Book, BookController
return bookService.tags();
}
@GetMapping("export/{book_id}")
public void export(@PathVariable("book_id") Long bookId, HttpServletResponse response) throws IOException {
var book = bookService.detailOrThrow(bookId);
var result = new StringBuilder();
result.append(StrUtil.format("""
书名:{}
作者:{}
简介:{}
来源:{}
""", book.getName(), book.getAuthor(), book.getDescription(), book.getSource()));
book.getChapters()
.stream()
.sorted(Comparator.comparing(Chapter::getSequence))
.forEach(chapter -> {
result.append(StrUtil.format("第{}章 {}", chapter.getSequence(), chapter.getName()));
result.append("\n\n");
chapter.getContent()
.stream()
.sorted(Comparator.comparing(Line::getSequence))
.forEach(line -> {
result.append(line.getText());
result.append("\n");
});
result.append("\n\n");
});
response.setHeader("Access-Control-Expose-Headers", "Content-Type");
response.setHeader("Content-Type", "text/plain");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", StrUtil.format("attachment; filename={}", URLUtil.encodeAll(StrUtil.format("{}.txt", book.getName()))));
IoUtil.copy(new ByteArrayInputStream(result.toString().getBytes()), response.getOutputStream());
}
@Override
protected Function<SaveItem, Book> saveItemMapper() {
return item -> {

View File

@@ -1,6 +1,8 @@
package com.lanyuanxiaoyao.bookstore.controller;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.service.BookService;
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
@@ -8,11 +10,15 @@ import com.lanyuanxiaoyao.bookstore.service.LineService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -43,7 +49,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
public GlobalResponse<Object> saveWithContent(@RequestBody SaveWithContentItem item) {
if (SaveWithContentItem.Mode.CREATE.equals(item.mode())) {
var chapter = new Chapter();
chapter.setSequence(chapterService.latestSequence(item.bookId()));
chapter.setSequence(chapterService.latestSequence(item.bookId()) + 1);
chapter.setName(item.name());
chapter.setDescription(item.description());
chapter.setBook(bookService.detailOrThrow(item.bookId()));
@@ -62,6 +68,18 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
return GlobalResponse.responseSuccess();
}
@GetMapping("content/{chapter_id}")
public GlobalResponse<Map<String, Object>> content(@PathVariable("chapter_id") Long chapterId) {
var chapter = chapterService.detailOrThrow(chapterId);
return GlobalResponse.responseDetailData(
chapter.getContent()
.stream()
.sorted(Comparator.comparing(Line::getSequence))
.map(Line::getText)
.collect(Collectors.joining("\n"))
);
}
@GetMapping("generate_sequence")
public GlobalResponse<Object> generateSequence() {
chapterService.generateSequence();
@@ -73,7 +91,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
return item -> {
var chapter = new Chapter();
chapter.setId(item.id());
chapter.setSequence(chapterService.latestSequence(item.bookId()));
chapter.setSequence(ObjectUtil.defaultIfNull(item.sequence(), chapterService.latestSequence(item.bookId()) + 1));
chapter.setName(item.name());
chapter.setDescription(item.description());
chapter.setBook(bookService.detailOrThrow(item.bookId()));
@@ -105,6 +123,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
public record SaveItem(
Long id,
Long bookId,
Double sequence,
String name,
String description
) {
@@ -118,10 +137,6 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
Mode mode,
String content
) {
public SaveItem toSaveItem() {
return new SaveItem(null, bookId, name, description);
}
public enum Mode {
CREATE, OVERRIDE
}

View File

@@ -43,7 +43,7 @@ public class Line extends SimpleEntity {
@Lob
@Basic(fetch = FetchType.LAZY)
@ToString.Exclude
@Column(nullable = false)
@Column(nullable = false, columnDefinition = "longtext")
private String text;
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)

View File

@@ -2,12 +2,12 @@ package com.lanyuanxiaoyao.bookstore.service;
import cn.hutool.core.util.NumberUtil;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.bookstore.entity.QChapter;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.repository.ChapterRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Slf4j
@@ -32,7 +32,7 @@ public class ChapterService extends SimpleServiceSupport<Chapter> {
}
public void generateSequence() {
var chapters = chapterRepository.findAll(Sort.by(Sort.Direction.ASC, Chapter.Fields.sequence));
var chapters = chapterRepository.findAll(QChapter.chapter.sequence.asc());
for (int index = 0; index < chapters.size(); index++) {
chapters.get(index).setSequence(NumberUtil.toDouble(index));
}

View File

@@ -9,10 +9,10 @@ spring:
async:
request-timeout: 3600000
datasource:
url: jdbc:h2:file:./bookstore;DB_CLOSE_ON_EXIT=TRUE
url: jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false
username: bookstore
password: bookstore
driver-class-name: org.h2.Driver
password: EzSn+RZ*x2&fHFh9kC+H
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
generate-ddl: false
hibernate: