1
0

feat: 完成基本功能

This commit is contained in:
2025-08-14 16:33:11 +08:00
commit 33e11818c5
19 changed files with 1249 additions and 0 deletions

134
.gitignore vendored Normal file
View File

@@ -0,0 +1,134 @@
.idea
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
.idea/sonarlint/
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
.idea/caches/build_file_checksums.ser
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.project
.classpath
*.class
*.log
*.ctxt
.mtj.tmp/
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
replay_pid*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
.externalToolBuilders/
*.launch
*.pydevproject
.cproject
.autotools
.factorypath
.buildpath
.target
.tern-project
.texlipse
.springBeans
.recommenders/
.apt_generated/
.apt_generated_test/
.cache-main
.scala_dependencies
.worksheet
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
.gradle
**/build/
!src/**/build/
gradle-app.setting
!gradle-wrapper.jar
!gradle-wrapper.properties
.gradletasknamecache
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

70
pom.xml Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.4.3</spring-boot.version>
<spring-cloud.version>2024.0.1</spring-cloud.version>
<eclipse-collections.version>11.1.0</eclipse-collections.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.blinkfox</groupId>
<artifactId>fenix-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring boot 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<distributionManagement>
<repository>
<id>${releases.id}</id>
<name>${releases.name}</name>
<url>${releases.url}</url>
</repository>
<snapshotRepository>
<id>${snapshots.id}</id>
<name>${snapshots.name}</name>
<url>${snapshots.url}</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalResponse;
public interface DetailController<DETAIL_ITEM> {
String DETAIL = "/detail/{id}";
GlobalResponse<DETAIL_ITEM> detail(Long id) throws Exception;
}

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalCrudResponse;
public interface ListController<LIST_ITEM> {
String LIST = "/list";
GlobalCrudResponse<LIST_ITEM> list() throws Exception;
GlobalCrudResponse<LIST_ITEM> list(Query query) throws Exception;
}

View File

@@ -0,0 +1,304 @@
package com.lanyuanxiaoyao.service.template.controller;
import java.util.List;
import java.util.Map;
public class Query {
private Queryable query;
private List<Sortable> sort;
private Pageable page;
public Queryable getQuery() {
return query;
}
public void setQuery(Queryable query) {
this.query = query;
}
public List<Sortable> getSort() {
return sort;
}
public void setSort(List<Sortable> sort) {
this.sort = sort;
}
public Pageable getPage() {
return page;
}
public void setPage(Pageable page) {
this.page = page;
}
@Override
public String toString() {
return "Query{" +
"query=" + query +
", sort=" + sort +
", page=" + page +
'}';
}
public static class Queryable {
private List<String> nullEqual;
private List<String> notNullEqual;
private List<String> empty;
private List<String> notEmpty;
private Map<String, Object> equal;
private Map<String, Object> notEqual;
private Map<String, String> like;
private Map<String, String> notLike;
private Map<String, Comparable<Object>> great;
private Map<String, Comparable<Object>> less;
private Map<String, Comparable<Object>> greatEqual;
private Map<String, Comparable<Object>> lessEqual;
private Map<String, List<Object>> in;
private Map<String, List<Object>> notIn;
private Map<String, Between> between;
private Map<String, Between> notBetween;
public java.util.List<String> getNullEqual() {
return nullEqual;
}
public void setNullEqual(java.util.List<String> nullEqual) {
this.nullEqual = nullEqual;
}
public java.util.List<String> getNotNullEqual() {
return notNullEqual;
}
public void setNotNullEqual(java.util.List<String> notNullEqual) {
this.notNullEqual = notNullEqual;
}
public java.util.List<String> getEmpty() {
return empty;
}
public void setEmpty(java.util.List<String> empty) {
this.empty = empty;
}
public java.util.List<String> getNotEmpty() {
return notEmpty;
}
public void setNotEmpty(java.util.List<String> notEmpty) {
this.notEmpty = notEmpty;
}
public java.util.Map<String, Object> getEqual() {
return equal;
}
public void setEqual(java.util.Map<String, Object> equal) {
this.equal = equal;
}
public java.util.Map<String, Object> getNotEqual() {
return notEqual;
}
public void setNotEqual(java.util.Map<String, Object> notEqual) {
this.notEqual = notEqual;
}
public java.util.Map<String, String> getLike() {
return like;
}
public void setLike(java.util.Map<String, String> like) {
this.like = like;
}
public java.util.Map<String, String> getNotLike() {
return notLike;
}
public void setNotLike(java.util.Map<String, String> notLike) {
this.notLike = notLike;
}
public java.util.Map<String, Comparable<Object>> getGreat() {
return great;
}
public void setGreat(java.util.Map<String, Comparable<Object>> great) {
this.great = great;
}
public java.util.Map<String, Comparable<Object>> getLess() {
return less;
}
public void setLess(java.util.Map<String, Comparable<Object>> less) {
this.less = less;
}
public java.util.Map<String, Comparable<Object>> getGreatEqual() {
return greatEqual;
}
public void setGreatEqual(java.util.Map<String, Comparable<Object>> greatEqual) {
this.greatEqual = greatEqual;
}
public java.util.Map<String, Comparable<Object>> getLessEqual() {
return lessEqual;
}
public void setLessEqual(java.util.Map<String, Comparable<Object>> lessEqual) {
this.lessEqual = lessEqual;
}
public java.util.Map<String, java.util.List<Object>> getIn() {
return in;
}
public void setIn(java.util.Map<String, java.util.List<Object>> in) {
this.in = in;
}
public java.util.Map<String, java.util.List<Object>> getNotIn() {
return notIn;
}
public void setNotIn(java.util.Map<String, java.util.List<Object>> notIn) {
this.notIn = notIn;
}
public java.util.Map<String, com.lanyuanxiaoyao.service.template.controller.Query.Queryable.Between> getBetween() {
return between;
}
public void setBetween(java.util.Map<String, com.lanyuanxiaoyao.service.template.controller.Query.Queryable.Between> between) {
this.between = between;
}
public java.util.Map<String, com.lanyuanxiaoyao.service.template.controller.Query.Queryable.Between> getNotBetween() {
return notBetween;
}
public void setNotBetween(java.util.Map<String, com.lanyuanxiaoyao.service.template.controller.Query.Queryable.Between> notBetween) {
this.notBetween = notBetween;
}
@Override
public String toString() {
return "Queryable{" +
"nullEqual=" + nullEqual +
", notNullEqual=" + notNullEqual +
", empty=" + empty +
", notEmpty=" + notEmpty +
", equal=" + equal +
", notEqual=" + notEqual +
", like=" + like +
", notLike=" + notLike +
", great=" + great +
", less=" + less +
", greatEqual=" + greatEqual +
", lessEqual=" + lessEqual +
", in=" + in +
", notIn=" + notIn +
", between=" + between +
", notBetween=" + notBetween +
'}';
}
public static class Between {
private String start;
private String end;
public String getStart() {
return start;
}
public void setStart(String start) {
this.start = start;
}
public String getEnd() {
return end;
}
public void setEnd(String end) {
this.end = end;
}
@Override
public String toString() {
return "Between{" +
"start='" + start + '\'' +
", end='" + end + '\'' +
'}';
}
}
}
public static class Sortable {
private String column;
private Direction direction;
public String getColumn() {
return column;
}
public void setColumn(String column) {
this.column = column;
}
public Direction getDirection() {
return direction;
}
public void setDirection(Direction direction) {
this.direction = direction;
}
@Override
public String toString() {
return "Sortable{" +
"column='" + column + '\'' +
", direction=" + direction +
'}';
}
public enum Direction {
ASC,
DESC,
}
}
public static class Pageable {
private Integer index;
private Integer size;
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
@Override
public String toString() {
return "Pageable{" +
"index=" + index +
", size=" + size +
'}';
}
}
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalResponse;
public interface RemoveController {
String REMOVE = "/remove/{id}";
GlobalResponse<Object> remove(Long id) throws Exception;
}

View File

@@ -0,0 +1,9 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalResponse;
public interface SaveController<SAVE_ITEM> {
String SAVE = "/save";
GlobalResponse<Long> save(SAVE_ITEM item) throws Exception;
}

View File

@@ -0,0 +1,4 @@
package com.lanyuanxiaoyao.service.template.controller;
public interface SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> extends SaveController<SAVE_ITEM>, ListController<LIST_ITEM>, DetailController<DETAIL_ITEM>, RemoveController {
}

View File

@@ -0,0 +1,100 @@
package com.lanyuanxiaoyao.service.template.controller;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalCrudResponse;
import com.lanyuanxiaoyao.service.template.controller.response.GlobalResponse;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List;
import org.springframework.data.domain.Page;
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;
public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> implements SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> {
protected final SimpleServiceSupport<ENTITY> service;
public SimpleControllerSupport(SimpleServiceSupport<ENTITY> service) {
this.service = service;
}
@PostMapping(SAVE)
@Override
public GlobalResponse<Long> save(@RequestBody SAVE_ITEM item) throws Exception {
SaveItemMapper<ENTITY, SAVE_ITEM> mapper = saveItemMapper();
return GlobalResponse.responseSuccess(service.save(mapper.from(item)));
}
@GetMapping(LIST)
@Override
public GlobalCrudResponse<LIST_ITEM> list() throws Exception {
ListItemMapper<ENTITY, LIST_ITEM> mapper = listItemMapper();
return GlobalCrudResponse.responseCrudData(
service.list()
.stream()
.map(entity -> {
try {
return mapper.from(entity);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList()
);
}
@PostMapping(LIST)
@Override
public GlobalCrudResponse<LIST_ITEM> list(@RequestBody Query query) throws Exception {
if (ObjectHelper.isNull(query)) {
return GlobalCrudResponse.responseCrudData(List.of(), 0);
}
ListItemMapper<ENTITY, LIST_ITEM> mapper = listItemMapper();
Page<ENTITY> result = service.list(query);
return GlobalCrudResponse.responseCrudData(
result.get()
.map(entity -> {
try {
return mapper.from(entity);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList(),
result.getTotalElements()
);
}
@GetMapping(DETAIL)
@Override
public GlobalResponse<DETAIL_ITEM> detail(@PathVariable("id") Long id) throws Exception {
DetailItemMapper<ENTITY, DETAIL_ITEM> mapper = detailItemMapper();
return GlobalResponse.responseSuccess(mapper.from(service.detailOrThrow(id)));
}
@GetMapping(REMOVE)
@Override
public GlobalResponse<Object> remove(@PathVariable("id") Long id) throws Exception {
service.remove(id);
return GlobalResponse.responseSuccess();
}
protected abstract SaveItemMapper<ENTITY, SAVE_ITEM> saveItemMapper();
protected abstract ListItemMapper<ENTITY, LIST_ITEM> listItemMapper();
protected abstract DetailItemMapper<ENTITY, DETAIL_ITEM> detailItemMapper();
public interface SaveItemMapper<ENTITY, SAVE_ITEM> {
ENTITY from(SAVE_ITEM item) throws Exception;
}
public interface ListItemMapper<ENTITY, LIST_ITEM> {
LIST_ITEM from(ENTITY entity) throws Exception;
}
public interface DetailItemMapper<ENTITY, DETAIL_ITEM> {
DETAIL_ITEM from(ENTITY entity) throws Exception;
}
}

View File

@@ -0,0 +1,30 @@
package com.lanyuanxiaoyao.service.template.controller.response;
/**
* Crud 响应
*
* @author lanyuanxiaoyao
* @date 2023-07-06
*/
public class GlobalCrudResponse<T> extends GlobalMapResponse {
public void setData(Iterable<T> list) {
setData("items", list);
}
public void setTotal(Long total) {
setData("total", total);
}
public void setTotal(Integer total) {
setTotal(total.longValue());
}
public void setData(Iterable<T> list, Long total) {
setData(list);
setTotal(total);
}
public void setData(Iterable<T> list, Integer total) {
setData(list, total.longValue());
}
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.service.template.controller.response;
/**
* Crud 响应
*
* @author lanyuanxiaoyao
* @date 2023-07-06
*/
public class GlobalDetailResponse extends GlobalMapResponse {
public void setDetail(Object detail) {
setData("detail", detail);
}
}

View File

@@ -0,0 +1,21 @@
package com.lanyuanxiaoyao.service.template.controller.response;
import java.util.HashMap;
import java.util.Map;
/**
* Map 响应
*
* @author lanyuanxiaoyao
* @date 2023-07-06
*/
public class GlobalMapResponse extends GlobalResponse<Map<String, Object>> {
public GlobalMapResponse() {
setData(new HashMap<>());
}
public GlobalMapResponse setData(String key, Object value) {
getData().put(key, value);
return this;
}
}

View File

@@ -0,0 +1,138 @@
package com.lanyuanxiaoyao.service.template.controller.response;
import java.util.Map;
/**
* Amis 组件结构化返回值
*
* @author lanyuanxiaoyao
* @date 2022-09-21
*/
public class GlobalResponse<T> {
private static final int SUCCESS_STATUS = 0;
private static final int ERROR_STATUS = 500;
private static final String SUCCESS_MESSAGE = "OK";
private static final String ERROR_MESSAGE = "ERROR";
private Integer status;
private String message;
private T data;
public static GlobalResponse<Object> responseError() {
return responseError(ERROR_MESSAGE);
}
public static GlobalResponse<Object> responseError(String message) {
GlobalResponse<Object> response = new GlobalResponse<>();
response.setStatus(ERROR_STATUS);
response.setMessage(message);
return response;
}
public static GlobalResponse<Object> responseSuccess() {
GlobalResponse<Object> response = new GlobalResponse<>();
response.setStatus(SUCCESS_STATUS);
response.setMessage(SUCCESS_MESSAGE);
return response;
}
public static GlobalResponse<Object> responseSuccess(String message) {
GlobalResponse<Object> response = new GlobalResponse<>();
response.setStatus(SUCCESS_STATUS);
response.setMessage(message);
return response;
}
public static <E> GlobalResponse<E> responseSuccess(String message, E data) {
GlobalResponse<E> response = new GlobalResponse<>();
response.setStatus(SUCCESS_STATUS);
response.setMessage(message);
response.setData(data);
return response;
}
public static <E> GlobalResponse<E> responseSuccess(E data) {
GlobalResponse<E> response = new GlobalResponse<>();
response.setStatus(SUCCESS_STATUS);
response.setMessage(SUCCESS_MESSAGE);
response.setData(data);
return response;
}
public static GlobalMapResponse responseMapData() {
GlobalMapResponse response = new GlobalMapResponse();
response.setStatus(SUCCESS_STATUS);
response.setMessage(SUCCESS_MESSAGE);
return response;
}
public static GlobalMapResponse responseMapData(Map<String, Object> data) {
GlobalMapResponse response = responseMapData();
response.setData(data);
return response;
}
public static GlobalMapResponse responseMapData(String key, Object value) {
GlobalMapResponse response = responseMapData();
response.setData(key, value);
return response;
}
public static <T> GlobalCrudResponse<T> responseCrudData(Iterable<T> data) {
GlobalCrudResponse<T> response = new GlobalCrudResponse<>();
response.setStatus(SUCCESS_STATUS);
response.setMessage(SUCCESS_MESSAGE);
response.setData(data);
return response;
}
public static <T> GlobalCrudResponse<T> responseCrudData(Iterable<T> data, Integer total) {
GlobalCrudResponse<T> response = responseCrudData(data);
response.setTotal(total);
return response;
}
public static <T> GlobalCrudResponse<T> responseCrudData(Iterable<T> data, Long total) {
GlobalCrudResponse<T> response = responseCrudData(data);
response.setTotal(total);
return response;
}
public static GlobalDetailResponse responseDetailData(Object detail) {
GlobalDetailResponse response = new GlobalDetailResponse();
response.setDetail(detail);
return response;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "AmisResponse{" +
"status=" + status +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}

View File

@@ -0,0 +1,34 @@
package com.lanyuanxiaoyao.service.template.entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class IdOnlyEntity {
@Comment("记录唯一标记")
@Id
@GeneratedValue(generator = "snowflake")
@GenericGenerator(name = "snowflake", strategy = "com.lanyuanxiaoyao.service.ai.web.configuration.SnowflakeIdGenerator")
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String toString() {
return "IdOnlyEntity{" +
"id=" + id +
'}';
}
}

View File

@@ -0,0 +1,44 @@
package com.lanyuanxiaoyao.service.template.entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import org.hibernate.annotations.Comment;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class SimpleEntity extends IdOnlyEntity {
@Comment("记录创建时间")
@CreatedDate
private LocalDateTime createdTime;
@Comment("记录更新时间")
@LastModifiedDate
private LocalDateTime modifiedTime;
public LocalDateTime getCreatedTime() {
return createdTime;
}
public void setCreatedTime(LocalDateTime createdTime) {
this.createdTime = createdTime;
}
public LocalDateTime getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(LocalDateTime modifiedTime) {
this.modifiedTime = modifiedTime;
}
@Override
public String toString() {
return "SimpleEntity{" +
"createdTime=" + createdTime +
", modifiedTime=" + modifiedTime +
'}';
}
}

View File

@@ -0,0 +1,41 @@
package com.lanyuanxiaoyao.service.template.helper;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
public class ObjectHelper {
public static boolean isNull(Object obj) {
return obj == null;
}
public static boolean isNotNull(Object obj) {
return !isNull(obj);
}
public static boolean isEmpty(Object obj) {
if (isNull(obj)) return true;
else if (obj instanceof Collection<?> collection) return collection.isEmpty();
else if (obj instanceof Map<?, ?> map) return map.isEmpty();
else if (obj instanceof CharSequence sequence) return sequence.isEmpty();
else if (obj instanceof Object[] array) return array.length == 0;
else if (obj instanceof byte[] array) return array.length == 0;
else if (obj instanceof short[] array) return array.length == 0;
else if (obj instanceof int[] array) return array.length == 0;
else if (obj instanceof long[] array) return array.length == 0;
else if (obj instanceof float[] array) return array.length == 0;
else if (obj instanceof double[] array) return array.length == 0;
else if (obj instanceof char[] array) return array.length == 0;
else if (obj instanceof boolean[] array) return array.length == 0;
else if (obj instanceof Optional<?> optional) return optional.isEmpty();
else return false;
}
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
public static <T> T defaultIfNull(final T object, final T defaultValue) {
return isNull(object) ? defaultValue : object;
}
}

View File

@@ -0,0 +1,10 @@
package com.lanyuanxiaoyao.service.template.repository;
import com.blinkfox.fenix.jpa.FenixJpaRepository;
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.QueryByExampleExecutor;
@NoRepositoryBean
public interface SimpleRepository<E, ID> extends FenixJpaRepository<E, ID>, FenixJpaSpecificationExecutor<E>, QueryByExampleExecutor<E> {
}

View File

@@ -0,0 +1,34 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.controller.Query;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.domain.Page;
/**
* @author lanyuanxiaoyao
* @date 2024-11-28
*/
public interface SimpleService<ENTITY extends SimpleEntity> {
Long save(ENTITY entity) throws Exception;
Long count() throws Exception;
List<ENTITY> list() throws Exception;
List<ENTITY> list(Set<Long> ids) throws Exception;
Page<ENTITY> list(Query query) throws Exception;
Optional<ENTITY> detailOptional(Long id) throws Exception;
ENTITY detail(Long id) throws Exception;
ENTITY detailOrThrow(Long id) throws Exception;
ENTITY detailOrNull(Long id) throws Exception;
void remove(Long id) throws Exception;
}

View File

@@ -0,0 +1,234 @@
package com.lanyuanxiaoyao.service.template.service;
import com.lanyuanxiaoyao.service.template.controller.Query;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
private static final Integer DEFAULT_PAGE_INDEX = 1;
private static final Integer DEFAULT_PAGE_SIZE = 10;
protected final SimpleRepository<ENTITY, Long> repository;
public SimpleServiceSupport(SimpleRepository<ENTITY, Long> repository) {
this.repository = repository;
}
@Transactional(rollbackOn = Throwable.class)
@Override
public Long save(ENTITY entity) {
if (!ObjectHelper.isNull(entity.getId())) {
var id = entity.getId();
var targetEntity = repository.findById(entity.getId()).orElseThrow(() -> new IdNotFoundException(id));
BeanUtils.copyProperties(entity, targetEntity, "id", "created_time", "modified_time");
entity = targetEntity;
}
entity = repository.save(entity);
return entity.getId();
}
@Override
public Long count() {
return repository.count(
(root, query, builder) ->
builder.and(
listPredicate(root, query, builder)
.stream()
.filter(ObjectHelper::isNotNull)
.toArray(Predicate[]::new)
)
);
}
@Override
public List<ENTITY> list() {
return repository.findAll(
(root, query, builder) ->
builder.and(
listPredicate(root, query, builder)
.stream()
.filter(ObjectHelper::isNotNull)
.toArray(Predicate[]::new)
)
);
}
@Override
public List<ENTITY> list(Set<Long> ids) {
return repository.findAll(
(root, query, builder) -> {
var predicates = listPredicate(root, query, builder);
predicates.add(builder.in(root.get("id")).value(ids));
return builder.and(predicates.stream().filter(ObjectHelper::isNotNull).toArray(Predicate[]::new));
}
);
}
private <Y> Path<Y> column(Root<ENTITY> root, String column) {
if (ObjectHelper.isEmpty(column)) {
throw new IllegalArgumentException("Column cannot be blank");
}
String[] columns = column.split("/");
Path<Y> path = root.get(columns[0]);
for (int i = 1; i < columns.length; i++) {
path = path.get(columns[i]);
}
return path;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private <Y> Object value(Path<Y> column, Object value) {
Class<?> javaType = column.getJavaType();
if (javaType.isEnum()) {
return Enum.valueOf((Class<Enum>) javaType, (String) value);
}
return value;
}
protected List<Predicate> queryPredicates(Query.Queryable queryable, Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
if (ObjectHelper.isNull(queryable)) {
return predicates;
}
if (ObjectHelper.isNotEmpty(queryable.getNullEqual())) {
queryable.getNullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getNotNullEqual())) {
queryable.getNotNullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getEmpty())) {
queryable.getEmpty().forEach(column -> predicates.add(builder.isEmpty(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getNotEmpty())) {
queryable.getNotEmpty().forEach(column -> predicates.add(builder.isNotEmpty(column(root, column))));
}
if (ObjectHelper.isNotEmpty(queryable.getEqual())) {
queryable.getEqual().forEach((column, value) -> {
Path<Object> path = column(root, column);
predicates.add(builder.equal(path, value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getNotEqual())) {
queryable.getEqual().forEach((column, value) -> {
Path<Object> path = column(root, column);
predicates.add(builder.notEqual(path, value(path, value)));
});
}
if (ObjectHelper.isNotEmpty(queryable.getLike())) {
queryable.getLike().forEach((column, value) -> predicates.add(builder.like(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getNotLike())) {
queryable.getNotLike().forEach((column, value) -> predicates.add(builder.notLike(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getGreat())) {
queryable.getGreat().forEach((column, value) -> predicates.add(builder.greaterThan(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getLess())) {
queryable.getLess().forEach((column, value) -> predicates.add(builder.lessThan(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getGreatEqual())) {
queryable.getGreatEqual().forEach((column, value) -> predicates.add(builder.greaterThanOrEqualTo(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getLessEqual())) {
queryable.getLessEqual().forEach((column, value) -> predicates.add(builder.lessThanOrEqualTo(column(root, column), value)));
}
if (ObjectHelper.isNotEmpty(queryable.getIn())) {
queryable.getIn().forEach((column, value) -> predicates.add(builder.in(column(root, column)).value(value)));
}
if (ObjectHelper.isNotEmpty(queryable.getNotIn())) {
queryable.getNotIn().forEach((column, value) -> predicates.add(builder.in(column(root, column)).value(value).not()));
}
if (ObjectHelper.isNotEmpty(queryable.getBetween())) {
queryable.getBetween().forEach((column, value) -> predicates.add(builder.between(column(root, column), value.getStart(), value.getEnd())));
}
if (ObjectHelper.isNotEmpty(queryable.getNotBetween())) {
queryable.getNotBetween().forEach((column, value) -> predicates.add(builder.between(column(root, column), value.getStart(), value.getEnd())));
}
return predicates;
}
protected List<Predicate> listPredicate(Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return new ArrayList<>(0);
}
@Override
public Page<ENTITY> list(Query listQuery) {
PageRequest pageRequest = PageRequest.of(DEFAULT_PAGE_INDEX - 1, DEFAULT_PAGE_SIZE, Sort.by("created_time").descending());
if (ObjectHelper.isNotNull(listQuery.getPage())) {
pageRequest = PageRequest.of(
ObjectHelper.defaultIfNull(listQuery.getPage().getIndex(), DEFAULT_PAGE_INDEX) - 1,
ObjectHelper.defaultIfNull(listQuery.getPage().getSize(), DEFAULT_PAGE_SIZE),
Sort.by("create_time").descending()
);
}
return repository.findAll(
(root, query, builder) -> {
var predicates = listPredicate(root, query, builder);
predicates.addAll(queryPredicates(listQuery.getQuery(), root, query, builder));
return builder.and(predicates.stream().filter(ObjectHelper::isNotNull).toArray(Predicate[]::new));
},
pageRequest
);
}
@Override
public Optional<ENTITY> detailOptional(Long id) {
if (ObjectHelper.isNull(id)) {
return Optional.empty();
}
return repository.findOne(
(root, query, builder) -> {
var predicates = listPredicate(root, query, builder);
predicates.add(builder.equal(root.get("id"), id));
return builder.and(predicates.stream().filter(ObjectHelper::isNotNull).toArray(Predicate[]::new));
}
);
}
@Override
public ENTITY detail(Long id) {
return detailOrNull(id);
}
@Override
public ENTITY detailOrThrow(Long id) {
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
}
@Override
public ENTITY detailOrNull(Long id) {
return detailOptional(id).orElse(null);
}
@Transactional(rollbackOn = Throwable.class)
@Override
public void remove(Long id) {
if (ObjectHelper.isNotNull(id)) {
repository.deleteById(id);
}
}
public static final class IdNotFoundException extends RuntimeException {
public IdNotFoundException() {
super("资源不存在");
}
public IdNotFoundException(Long id) {
super("ID为%d的资源不存在".formatted(id));
}
}
}