1
0

feat: 初始化提交

This commit is contained in:
2025-09-28 18:55:24 +08:00
commit 0a93e1d7ad
53 changed files with 4688 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
package com.lanyuanxiaoyao.bookstore;
import com.blinkfox.fenix.EnableFenix;
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;
/**
* 启动类
*
* @author lanyuanxiaoyao
* @version 20250922
*/
@SpringBootApplication
@EnableJpaAuditing
@EnableFenix
@EnableConfigurationProperties
public class BookStoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookStoreApplication.class, args);
}
}

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.bookstore;
/**
* 静态变量
*
* @author lanyuanxiaoyao
* @version 20250922
*/
public interface Constants {
String DATABASE_PREFIX = "bookstore_";
}

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.bookstore.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 网络配置
*
* @author lanyuanxiaoyao
* @version 20250922
*/
@Configuration
public class WebConfiguration {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setMaxAge(7200L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,91 @@
package com.lanyuanxiaoyao.bookstore.controller;
import com.lanyuanxiaoyao.bookstore.entity.Book;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.service.BookService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDateTime;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("book")
public class BookController extends SimpleControllerSupport<Book, BookController.SaveItem, BookController.DetailItem, BookController.DetailItem> {
private final BookService bookService;
public BookController(BookService service) {
super(service);
this.bookService = service;
}
@GetMapping("tags")
public List<Option> tags() {
return bookService.tags();
}
@Override
protected Function<SaveItem, Book> saveItemMapper() {
return item -> {
Book book = new Book();
book.setId(item.id());
book.setName(item.name());
book.setAuthor(item.author());
book.setDescription(item.description());
book.setSource(item.source());
book.setTags(item.tags());
return book;
};
}
private DetailItem toDetailItem(Book book) {
return new DetailItem(
book.getId(),
book.getName(),
book.getAuthor(),
book.getDescription(),
book.getSource(),
book.getTags(),
book.getCreatedTime(),
book.getModifiedTime()
);
}
@Override
protected Function<Book, DetailItem> listItemMapper() {
return this::toDetailItem;
}
@Override
protected Function<Book, DetailItem> detailItemMapper() {
return this::toDetailItem;
}
public record SaveItem(
Long id,
String name,
String author,
String description,
String source,
Set<String> tags
) {
}
public record DetailItem(
Long id,
String name,
String author,
String description,
String source,
Set<String> tags,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -0,0 +1,140 @@
package com.lanyuanxiaoyao.bookstore.controller;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.service.BookService;
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
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.List;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("chapter")
public class ChapterController extends SimpleControllerSupport<Chapter, ChapterController.SaveItem, ChapterController.DetailItem, ChapterController.DetailItem> {
private final ChapterService chapterService;
private final BookService bookService;
private final LineService lineService;
public ChapterController(ChapterService service, BookService bookService, LineService lineService) {
super(service);
this.chapterService = service;
this.bookService = bookService;
this.lineService = lineService;
}
@GetMapping("chapters")
public List<Option> chapters() {
return chapterService.chapters();
}
@Transactional(rollbackFor = Throwable.class)
@PostMapping("save_with_content")
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.setName(item.name());
chapter.setDescription(item.description());
chapter.setBook(bookService.detailOrThrow(item.bookId()));
var chapterId = chapterService.save(chapter);
lineService.load(chapterId, item.content());
} else if (SaveWithContentItem.Mode.OVERRIDE.equals(item.mode())) {
var chapter = chapterService.detailOrThrow(item.id());
chapter.setName(item.name());
chapter.setDescription(item.description());
var chapterId = chapterService.save(chapter);
lineService.empty(chapterId);
lineService.load(chapterId, item.content());
} else {
return GlobalResponse.responseError("Invalid mode");
}
return GlobalResponse.responseSuccess();
}
@GetMapping("generate_sequence")
public GlobalResponse<Object> generateSequence() {
chapterService.generateSequence();
return GlobalResponse.responseSuccess();
}
@Override
protected Function<SaveItem, Chapter> saveItemMapper() {
return item -> {
var chapter = new Chapter();
chapter.setId(item.id());
chapter.setSequence(chapterService.latestSequence(item.bookId()));
chapter.setName(item.name());
chapter.setDescription(item.description());
chapter.setBook(bookService.detailOrThrow(item.bookId()));
return chapter;
};
}
private DetailItem toDetailItem(Chapter chapter) {
return new DetailItem(
chapter.getId(),
chapter.getSequence(),
chapter.getName(),
chapter.getDescription(),
chapter.getCreatedTime(),
chapter.getModifiedTime()
);
}
@Override
protected Function<Chapter, DetailItem> listItemMapper() {
return this::toDetailItem;
}
@Override
protected Function<Chapter, DetailItem> detailItemMapper() {
return this::toDetailItem;
}
public record SaveItem(
Long id,
Long bookId,
String name,
String description
) {
}
public record SaveWithContentItem(
Long id,
Long bookId,
String name,
String description,
Mode mode,
String content
) {
public SaveItem toSaveItem() {
return new SaveItem(null, bookId, name, description);
}
public enum Mode {
CREATE, OVERRIDE
}
}
public record DetailItem(
Long id,
Long sequence,
String name,
String description,
LocalDateTime createdTime,
LocalDateTime modifiedTime
) {
}
}

View File

@@ -0,0 +1,85 @@
package com.lanyuanxiaoyao.bookstore.controller;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.bookstore.service.LineService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("line")
public class LineController extends SimpleControllerSupport<Line, LineController.SaveItem, LineController.DetailItem, LineController.DetailItem> {
private final LineService lineService;
public LineController(LineService service) {
super(service);
this.lineService = service;
}
@PostMapping("load")
public GlobalResponse<Object> load(@RequestBody LineController.LoadItem item) {
lineService.load(item.chapterId(), item.content());
return GlobalResponse.responseSuccess();
}
@GetMapping("generate_sequence")
public GlobalResponse<Object> generateSequence() {
lineService.generateSequence();
return GlobalResponse.responseSuccess();
}
@Override
protected Function<SaveItem, Line> saveItemMapper() {
return item -> {
var line = lineService.detailOrThrow(item.id());
line.setText(item.text());
return line;
};
}
private DetailItem toDetailItem(Line line) {
return new DetailItem(
line.getId(),
line.getSequence(),
line.getText()
);
}
@Override
protected Function<Line, DetailItem> listItemMapper() {
return this::toDetailItem;
}
@Override
protected Function<Line, DetailItem> detailItemMapper() {
return this::toDetailItem;
}
public record SaveItem(
Long id,
Long chapterId,
String text
) {
}
public record LoadItem(
Long chapterId,
String content
) {
}
public record DetailItem(
Long id,
Long sequence,
String text
) {
}
}

View File

@@ -0,0 +1,53 @@
package com.lanyuanxiaoyao.bookstore.entity;
import com.lanyuanxiaoyao.bookstore.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 书籍
*
* @author lanyuanxiaoyao
* @version 20250922
*/
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "book")
public class Book extends SimpleEntity {
@Column(nullable = false)
private String name;
private String author;
private String description;
private String source;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = Constants.DATABASE_PREFIX + "book_tags", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Set<String> tags;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "book")
@ToString.Exclude
private Set<Chapter> chapters;
}

View File

@@ -0,0 +1,54 @@
package com.lanyuanxiaoyao.bookstore.entity;
import com.lanyuanxiaoyao.bookstore.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 章节
*
* @author lanyuanxiaoyao
* @version 20250922
*/
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "chapter")
public class Chapter extends SimpleEntity {
@Column(nullable = false)
private Long sequence;
@Column(nullable = false)
private String name;
private String description;
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
@JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ToString.Exclude
private Book book;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "chapter")
@ToString.Exclude
private Set<Line> content;
}

View File

@@ -0,0 +1,53 @@
package com.lanyuanxiaoyao.bookstore.entity;
import com.lanyuanxiaoyao.bookstore.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 书籍行
*
* @author lanyuanxiaoyao
* @version 20250922
*/
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "line")
public class Line extends SimpleEntity {
@Column(nullable = false)
private Long sequence;
@Lob
@Basic(fetch = FetchType.LAZY)
@ToString.Exclude
@Column(nullable = false)
private String text;
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
@JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ToString.Exclude
private Chapter chapter;
}

View File

@@ -0,0 +1,10 @@
package com.lanyuanxiaoyao.bookstore.entity.vo;
/**
* Amis Option
*
* @author 选项
* @version 20250923
*/
public record Option(String label, Object value) {
}

View File

@@ -0,0 +1,14 @@
package com.lanyuanxiaoyao.bookstore.repository;
import com.lanyuanxiaoyao.bookstore.entity.Book;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface BookRepository extends SimpleRepository<Book> {
@Query("select distinct book.tags from Book book")
List<String> findDistinctTags();
}

View File

@@ -0,0 +1,14 @@
package com.lanyuanxiaoyao.bookstore.repository;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import java.util.Optional;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ChapterRepository extends SimpleRepository<Chapter> {
@Query("select max(chapter.sequence) from Chapter chapter")
Optional<Long> findMaxSequence(Long bookId);
}

View File

@@ -0,0 +1,10 @@
package com.lanyuanxiaoyao.bookstore.repository;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface LineRepository extends SimpleRepository<Line> {
}

View File

@@ -0,0 +1,28 @@
package com.lanyuanxiaoyao.bookstore.service;
import com.lanyuanxiaoyao.bookstore.entity.Book;
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
import com.lanyuanxiaoyao.bookstore.repository.BookRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class BookService extends SimpleServiceSupport<Book> {
private final BookRepository bookRepository;
public BookService(BookRepository repository) {
super(repository);
this.bookRepository = repository;
}
public List<Option> tags() {
return bookRepository.findDistinctTags()
.stream()
.map(tag -> new Option(tag, tag))
.toList();
}
}

View File

@@ -0,0 +1,41 @@
package com.lanyuanxiaoyao.bookstore.service;
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
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
@Service
public class ChapterService extends SimpleServiceSupport<Chapter> {
private final ChapterRepository chapterRepository;
public ChapterService(ChapterRepository repository) {
super(repository);
this.chapterRepository = repository;
}
public List<Option> chapters() {
return list()
.stream()
.map(chapter -> new Option(chapter.getName(), chapter.getId()))
.toList();
}
public Long latestSequence(Long bookId) {
return chapterRepository.findMaxSequence(bookId).orElse(0L);
}
public void generateSequence() {
var chapters = chapterRepository.findAll(Sort.by(Sort.Direction.ASC, Chapter.Fields.sequence));
for (int index = 0; index < chapters.size(); index++) {
chapters.get(index).setSequence((long) index);
}
chapterRepository.saveAll(chapters);
}
}

View File

@@ -0,0 +1,58 @@
package com.lanyuanxiaoyao.bookstore.service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.bookstore.entity.Line;
import com.lanyuanxiaoyao.bookstore.repository.LineRepository;
import com.lanyuanxiaoyao.service.template.entity.IdOnlyEntity;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class LineService extends SimpleServiceSupport<Line> {
private final LineRepository lineRepository;
private final ChapterService chapterService;
public LineService(LineRepository repository, ChapterService chapterService) {
super(repository);
this.lineRepository = repository;
this.chapterService = chapterService;
}
@Transactional(rollbackFor = Throwable.class)
public void load(Long chapterId, String text) {
var chapter = chapterService.detailOrThrow(chapterId);
var lines = Stream.of(text.split("\n"))
.map(StrUtil::trimToNull)
.filter(ObjectUtil::isNotNull)
.toList();
for (int index = 0; index < lines.size(); index++) {
var line = new Line();
line.setSequence((long) index);
line.setText(lines.get(index));
line.setChapter(chapter);
lineRepository.save(line);
}
}
@Transactional(rollbackFor = Throwable.class)
public void empty(Long chapterId) {
repository.delete((root, query, builder) -> builder.equal(root.get(Line.Fields.chapter).get(IdOnlyEntity.Fields.id), chapterId));
}
@Transactional(rollbackFor = Throwable.class)
public void generateSequence() {
var lines = lineRepository.findAll(Sort.by(Sort.Direction.ASC, Line.Fields.sequence));
for (int index = 0; index < lines.size(); index++) {
lines.get(index).setSequence((long) index);
}
lineRepository.saveAll(lines);
}
}

View File

@@ -0,0 +1,28 @@
server:
port: 27891
compression:
enabled: true
spring:
application:
name: bookstore
mvc:
async:
request-timeout: 3600000
datasource:
url: jdbc:h2:file:./bookstore;DB_CLOSE_ON_EXIT=TRUE
username: bookstore
password: bookstore
driver-class-name: org.h2.Driver
jpa:
generate-ddl: false
hibernate:
ddl-auto: update
main:
banner-mode: off
fenix:
print-banner: false
liteflow:
enable: false
print-banner: false
check-node-exists: false
rule-source: flow.xml

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
<flow>
<chain name="empty">
</chain>
</flow>

View File

@@ -0,0 +1,24 @@
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex" class="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx" class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<springProperty scope="context" name="LOGGING_PARENT" source="logging.parent"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%5p) %clr([${HOSTNAME}]){yellow} %clr([%t]){magenta} %clr(%logger{40}){cyan}: %m%n%wEx
</pattern>
</encoder>
</appender>
<logger name="com.lanyuanxiaoyao.leopard" level="INFO"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="Console"/>
</root>
</configuration>