1
0

Compare commits

...

8 Commits

Author SHA1 Message Date
1d6a08a16f build(deps): 移除 spring-boot-service-template-jpa-task 模块依赖 2026-01-06 15:58:27 +08:00
51b2cdb21d refactor(jpa): 使用 @Table 和 @Column 的 comment 属性替换 @Comment 注解 2026-01-06 15:53:40 +08:00
327e983c46 doc(common): 优化统一响应和工具类注释 2026-01-06 15:50:36 +08:00
d38196cb6e refactor(jpa): 使用 @Column 替换 @Comment 注解 2026-01-06 15:49:55 +08:00
8a944923ea refactor(common): 重构响应结构,使用泛型记录替代Map
- 将 responseCrudData() 重命名为 responseListData()
- 新增 ListItem 和 DetailItem 泛型记录替代 Map 包装
- 更新 QueryController 接口支持双泛型参数
- 优化类型安全性和代码可读性
2026-01-06 15:32:39 +08:00
6840d4a366 doc(common): 优化注释说明 2026-01-06 15:06:23 +08:00
8622891dbb refactor: 移除所有文件中的 @author 注释
移除所有 Java 文件中的 @author lanyuanxiaoyao 注释,统一代码风格。
2026-01-06 14:56:02 +08:00
d88078ce42 refactor(common): 优化继承结构,合并list和detail查询 2026-01-06 14:49:07 +08:00
28 changed files with 871 additions and 500 deletions

View File

@@ -12,7 +12,6 @@
<modules>
<module>spring-boot-service-template-common</module>
<module>spring-boot-service-template-jpa</module>
<module>spring-boot-service-template-jpa-task</module>
</modules>
<properties>

View File

@@ -1,27 +0,0 @@
package com.lanyuanxiaoyao.service.template.common.controller;
/**
* 详情控制器接口,用于定义统一的获取实体详情的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* GET /detail/1
* </pre>
* </p>
*
* @param <DETAIL_ITEM> 详情实体类型
* @author lanyuanxiaoyao
*/
public interface DetailController<DETAIL_ITEM> {
String DETAIL = "/detail/{id}";
/**
* 根据ID获取实体详情
*
* @param id 实体ID
* @return GlobalResponse<DETAIL_ITEM> 返回实体详情
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<DETAIL_ITEM> detail(Long id) throws Exception;
}

View File

@@ -1,58 +1,359 @@
package com.lanyuanxiaoyao.service.template.common.controller;
import java.util.List;
import java.util.Map;
public record GlobalResponse<T>(
Integer status,
String message,
T data
) {
/**
* 全局统一API响应封装类
* <p>
* 该类用于统一封装RESTful API接口的响应结果提供标准化的响应格式。
* 通过状态码、消息和数据三个字段,清晰地表达请求的处理结果,便于前端统一处理。
* </p>
*
* <h3>设计特点</h3>
* <ul>
* <li>使用Java Record实现不可变线程安全</li>
* <li>泛型设计,支持任意类型的数据封装</li>
* <li>提供丰富的静态工厂方法,简化响应对象创建</li>
* <li>支持分页查询、详情查询等常见场景</li>
* </ul>
*
* <h3>响应格式示例</h3>
* <p><b>成功响应(无数据):</b></p>
* <pre>
* {
* "status": 0,
* "message": "OK",
* "data": null
* }
* </pre>
*
* <p><b>成功响应(带数据):</b></p>
* <pre>
* {
* "status": 0,
* "message": "操作成功",
* "data": {
* "id": 1,
* "name": "示例数据"
* }
* }
* </pre>
*
* <p><b>分页列表响应:</b></p>
* <pre>
* {
* "status": 0,
* "message": "OK",
* "data": {
* "items": [
* {"id": 1, "name": "数据1"},
* {"id": 2, "name": "数据2"}
* ],
* "total": 100
* }
* }
* </pre>
*
* <p><b>错误响应:</b></p>
* <pre>
* {
* "status": 500,
* "message": "系统异常,请稍后重试",
* "data": null
* }
* </pre>
*
* <h3>使用场景</h3>
* <ul>
* <li><b>RESTful API:</b> 作为所有API接口的标准响应格式</li>
* <li><b>前后端分离:</b> 前端可统一处理成功/失败逻辑</li>
* <li><b>微服务架构:</b> 服务间调用的标准化响应</li>
* <li><b>移动端接口:</b> 移动端APP的统一数据格式</li>
* </ul>
*
* @param <T> 响应数据的类型可以是任意对象、集合、Map或包装类
* @param status 响应状态码
* <ul>
* <li>0 - 成功</li>
* <li>500 - 服务器错误</li>
* <li>其他 - 业务自定义状态码</li>
* </ul>
* @param message 响应消息,对状态码的简短描述,如"OK"、"ERROR"或具体的错误信息
* @param data 响应数据具体的业务数据可以是任意类型。对于列表查询通常封装为包含items和total的Map对于详情查询直接返回对象或Map
*
* @see #responseSuccess()
* @see #responseError()
* @see #responseListData(Iterable, Long)
* @see #responseDetailData(Object)
*/
public record GlobalResponse<T>(Integer status, String message, T data) {
/**
* 成功状态码 - 表示请求处理成功
*/
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";
/**
* 返回默认错误响应
* <p>
* 使用默认错误消息"ERROR"状态码500数据为null。
* 适用于无法确定具体错误原因的通用异常场景。
* </p>
*
* @return 错误响应对象,格式:{status: 500, message: "ERROR", data: null}
*
* @see #responseError(String)
* @see #responseSuccess()
*/
public static GlobalResponse<Object> responseError() {
return responseError(ERROR_MESSAGE);
}
/**
* 返回指定错误消息的响应
* <p>
* 使用指定的错误消息状态码500数据为null。
* 适用于需要向客户端传递具体错误信息的场景。
* </p>
*
* @param message 错误消息内容,建议描述具体错误原因,便于前端展示和问题定位
* @return 错误响应对象,格式:{status: 500, message: "自定义错误信息", data: null}
*
* @see #responseError()
* @see #responseSuccess(String)
*/
public static GlobalResponse<Object> responseError(String message) {
return new GlobalResponse<>(ERROR_STATUS, message, null);
}
/**
* 返回默认成功响应
* <p>
* 使用默认成功消息"OK"状态码0数据为null。
* 适用于操作成功但不需要返回数据的场景。
* </p>
*
* @return 成功响应对象,格式:{status: 0, message: "OK", data: null}
*
* @see #responseSuccess(String)
* @see #responseSuccess(Object)
* @see #responseError()
*/
public static GlobalResponse<Object> responseSuccess() {
return responseSuccess(SUCCESS_MESSAGE);
}
/**
* 返回指定成功消息的响应
* <p>
* 使用指定的成功消息状态码0数据为null。
* 适用于需要向客户端返回自定义成功提示的场景。
* </p>
*
* @param message 成功消息内容,建议描述具体操作结果,便于用户理解
* @return 成功响应对象,格式:{status: 0, message: "自定义成功信息", data: null}
*
* @see #responseSuccess()
* @see #responseSuccess(Object)
* @see #responseSuccess(String, Object)
*/
public static GlobalResponse<Object> responseSuccess(String message) {
return responseSuccess(message, null);
}
/**
* 返回包含数据的成功响应
* <p>
* 使用默认成功消息"OK"状态码0包含指定数据。
* 适用于需要返回数据但不需要自定义消息的场景。
* </p>
*
* @param <E> 数据类型可以是任意Java对象
* @param data 业务数据可以是实体对象、Map、集合等
* @return 成功响应对象,格式:{status: 0, message: "OK", data: 业务数据}
*
* @see #responseSuccess(String, Object)
* @see #responseListData(Iterable, Long)
* @see #responseDetailData(Object)
*/
public static <E> GlobalResponse<E> responseSuccess(E data) {
return responseSuccess(SUCCESS_MESSAGE, data);
}
/**
* 返回包含指定消息和数据的成功响应
* <p>
* 使用指定的成功消息状态码0包含指定数据。
* 这是最完整的方法,适用于需要同时自定义消息和返回数据的场景。
* </p>
*
* @param <E> 数据类型
* @param message 成功消息内容,描述具体操作结果
* @param data 业务数据,可以是任意类型
* @return 成功响应对象,格式:{status: 0, message: "自定义消息", data: 业务数据}
*
* @see #responseSuccess()
* @see #responseSuccess(Object)
* @see #responseSuccess(String)
*/
public static <E> GlobalResponse<E> responseSuccess(String message, E data) {
return new GlobalResponse<>(SUCCESS_STATUS, message, data);
}
/**
* 返回Map类型数据的成功响应
* <p>
* 适用于需要返回结构化数据的场景,如分页查询结果、统计信息等。
* 内部使用responseSuccess方法封装。
* </p>
*
* @param data Map格式的业务数据键为String值为任意对象
* @return 成功响应对象,格式:{status: 0, message: "OK", data: Map数据}
*
* @see #responseMapData(String, Object)
* @see #responseSuccess(Object)
*/
public static GlobalResponse<Map<String, Object>> responseMapData(Map<String, Object> data) {
return responseSuccess(data);
}
/**
* 返回单个键值对的成功响应
* <p>
* 将单个键值对封装为Map后返回适用于返回单个配置项或简单结果的场景。
* </p>
*
* @param key 数据键名不能为null
* @param value 数据值,可以是任意对象
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {key: value}}
*
* @see #responseMapData(Map)
* @see #responseSuccess(Object)
*/
public static GlobalResponse<Map<String, Object>> responseMapData(String key, Object value) {
return responseMapData(Map.of(key, value));
}
public static <T> GlobalResponse<Map<String, Object>> responseCrudData(Iterable<T> data, Integer total) {
return responseCrudData(data, total.longValue());
/**
* 返回空列表的成功响应
* <p>
* 适用于查询结果为空的场景返回空列表和总数为0。
* </p>
*
* @param <T> 数据项类型
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [], total: 0}}
*
* @see #responseListData(Iterable, Long)
* @see #responseListData(Iterable, Integer)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData() {
return responseListData(List.of(), 0);
}
public static <T> GlobalResponse<Map<String, Object>> responseCrudData(Iterable<T> data, Long total) {
return responseMapData(Map.of("items", data, "total", total));
/**
* 返回CRUD列表数据的成功响应Integer类型总数
* <p>
* 适用于分页查询,将数据列表和总数封装为标准格式。
* 自动将Integer类型的总数转换为Long类型。
* </p>
*
* @param <T> 数据项类型
* @param data 数据列表可以是List、Set等Iterable实现
* @param total 总记录数Integer类型
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
*
* @see #responseListData(Iterable, Long)
* @see #responseListData()
* @see #responseSuccess(Object)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData(Iterable<T> data, Integer total) {
return responseListData(data, total.longValue());
}
public static GlobalResponse<Map<String, Object>> responseDetailData(Object detail) {
return responseMapData("detail", detail);
/**
* 返回CRUD列表数据的成功响应Long类型总数
* <p>
* 适用于分页查询,将数据列表和总数封装为标准格式。
* 支持大数据量场景使用Long类型避免整数溢出。
* </p>
*
* @param <T> 数据项类型
* @param data 数据列表可以是List、Set等Iterable实现
* @param total 总记录数Long类型支持大数据量
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
*
* @see #responseListData(Iterable, Integer)
* @see #responseListData()
* @see ListItem
* @see #responseSuccess(Object)
*/
public static <T> GlobalResponse<ListItem<T>> responseListData(Iterable<T> data, Long total) {
return responseSuccess(new ListItem<>(data, total));
}
/**
* 返回详情数据的成功响应
* <p>
* 适用于详情查询,将单条记录封装为标准格式。
* 便于前端统一处理详情数据的展示。
* </p>
*
* @param <T> 数据类型
* @param data 详情数据可以是实体对象、Map等
* @return 成功响应对象,格式:{status: 0, message: "OK", data: {item: 详情数据}}
*
* @see #responseSuccess(Object)
* @see DetailItem
*/
public static <T> GlobalResponse<DetailItem<T>> responseDetailData(T data) {
return responseSuccess(new DetailItem<>(data));
}
/**
* 列表数据封装类
* <p>
* 用于封装分页查询的结果,包含数据列表和总记录数。
* 便于前端进行分页控件的渲染和数据展示。
* </p>
*
* @param <T> 数据项类型
* @param items 数据列表,包含当前页的所有记录
* @param total 总记录数,用于计算总页数和显示分页信息
*
* @see #responseListData(Iterable, Long)
*/
public record ListItem<T>(Iterable<T> items, Long total) {
}
/**
* 详情数据封装类
* <p>
* 用于封装单条记录的查询结果,提供统一的详情数据结构。
* 便于前端统一处理详情页面的数据展示。
* </p>
*
* <p><b>注意:</b> item字段使用Object类型可以存储任意类型的详情数据。</p>
*
* @param <T> 数据类型(主要用于类型提示)
* @param item 单条记录数据可以是实体对象、Map、VO对象等
*
* @see #responseDetailData(Object)
*/
public record DetailItem<T>(T item) {
}
}

View File

@@ -1,78 +0,0 @@
package com.lanyuanxiaoyao.service.template.common.controller;
import java.util.Map;
/**
* 列表控制器接口,用于定义统一的获取实体列表的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
* </pre>
* </p>
*
* <p>
* 支持的查询条件说明:
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表(如空字符串、空集合等)</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射(字段名 -> 值)</li>
* <li>notEqual: 指定字段值不相等的条件映射(字段名 -> 值)</li>
* <li>like: 指定字段模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>notLike: 指定字段不模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>great: 指定字段大于条件的映射(字段名 -> 值)</li>
* <li>less: 指定字段小于条件的映射(字段名 -> 值)</li>
* <li>greatEqual: 指定字段大于等于条件的映射(字段名 -> 值)</li>
* <li>lessEqual: 指定字段小于等于条件的映射(字段名 -> 值)</li>
* <li>in: 指定字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>between: 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* </ul>
* </p>
*
* @param <LIST_ITEM> 列表项的实体类型
* @author lanyuanxiaoyao
*/
public interface ListController<LIST_ITEM> {
String LIST = "/list";
/**
* 获取所有实体列表
*
* @return GlobalCrudResponse<LIST_ITEM> 返回实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<Map<String, Object>> list() throws Exception;
/**
* 根据查询条件获取实体列表
*
* @param query 查询条件对象
* @return GlobalCrudResponse<LIST_ITEM> 返回符合条件的实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<Map<String, Object>> list(Query query) throws Exception;
}

View File

@@ -5,7 +5,10 @@ import java.util.Map;
/**
* 查询条件封装类,用于构建复杂的查询条件
* 包含查询条件、排序条件和分页条件
* <p>
* 该类统一封装了查询条件、排序条件和分页条件,支持多种复杂的查询场景。
* 通过JSON格式传递查询参数后端自动解析并转换为数据库查询条件。
* </p>
*
* <p>前端传入的JSON格式示例:</p>
* <pre>
@@ -45,23 +48,33 @@ import java.util.Map;
*
* <p>查询条件说明:</p>
* <ul>
* <li>nullEqual: 字段值为null的条件</li>
* <li>notNullEqual: 字段值不为null的条件</li>
* <li>empty: 字段值为空的条件</li>
* <li>notEmpty: 字段值不为空的条件</li>
* <li>equal: 字段值相等的条件</li>
* <li>notEqual: 字段值不相等的条件</li>
* <li>like: 字段值模糊匹配的条件</li>
* <li>notLike: 字段值不模糊匹配的条件</li>
* <li>great: 字段值大于的条件</li>
* <li>less: 字段值小于的条件</li>
* <li>greatEqual: 字段值大于等于的条件</li>
* <li>lessEqual: 字段值小于等于的条件</li>
* <li>in: 字段值在指定范围内的条件</li>
* <li>notIn: 字段值不在指定范围内的条件</li>
* <li>between: 字段值在指定区间内的条件</li>
* <li>notBetween: 字段值不在指定区间内的条件</li>
* <li><b>nullEqual</b>: 字段值为null的条件列表</li>
* <li><b>notNullEqual</b>: 字段值不为null的条件列表</li>
* <li><b>empty</b>: 字段值为空的条件列表(如空字符串、空集合等)</li>
* <li><b>notEmpty</b>: 字段值不为空的条件列表</li>
* <li><b>equal</b>: 字段值相等的条件映射(字段名 -> 值)</li>
* <li><b>notEqual</b>: 字段值不相等的条件映射(字段名 -> 值)</li>
* <li><b>like</b>: 字段值模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li><b>notLike</b>: 字段值不模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li><b>contain</b>: 字段包含指定字符串的条件映射(字段名 -> 包含值)</li>
* <li><b>notContain</b>: 字段不包含指定字符串的条件映射(字段名 -> 不包含值)</li>
* <li><b>startWith</b>: 字段以指定字符串开头的条件映射(字段名 -> 开头值)</li>
* <li><b>notStartWith</b>: 字段不以指定字符串开头的条件映射(字段名 -> 不开头值)</li>
* <li><b>endWith</b>: 字段以指定字符串结尾的条件映射(字段名 -> 结尾值)</li>
* <li><b>notEndWith</b>: 字段不以指定字符串结尾的条件映射(字段名 -> 不结尾值)</li>
* <li><b>great</b>: 字段大于条件的映射(字段名 -> 值)</li>
* <li><b>less</b>: 字段小于条件的映射(字段名 -> 值)</li>
* <li><b>greatEqual</b>: 字段大于等于条件的映射(字段名 -> 值)</li>
* <li><b>lessEqual</b>: 字段小于等于条件的映射(字段名 -> 值)</li>
* <li><b>inside</b>: 字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li><b>notInside</b>: 字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li><b>between</b>: 字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li><b>notBetween</b>: 字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* </ul>
*
* @param query 查询条件对象,包含所有查询条件的封装
* @param sort 排序条件列表,支持多字段排序
* @param page 分页条件对象,指定页码和每页大小
*/
public record Query(
Queryable query,
@@ -70,6 +83,33 @@ public record Query(
) {
/**
* 可查询条件类,封装各种查询条件
* <p>
* 该类包含了所有支持的查询条件类型,每个字段对应一种查询条件。
* 字段名即为JSON中的键名字段类型决定了查询条件的值类型。
* </p>
*
* @param nullEqual 字段值为null的条件列表列表中的每个元素都是一个字段名
* @param notNullEqual 字段值不为null的条件列表列表中的每个元素都是一个字段名
* @param empty 字段值为空的条件列表(如空字符串、空集合等),列表中的每个元素都是一个字段名
* @param notEmpty 字段值不为空的条件列表,列表中的每个元素都是一个字段名
* @param equal 字段值相等的条件映射,键为字段名,值为要相等的值
* @param notEqual 字段值不相等的条件映射,键为字段名,值为要不相等的值
* @param like 字段值模糊匹配的条件映射,键为字段名,值为模糊匹配的模式(支持%通配符)
* @param notLike 字段值不模糊匹配的条件映射,键为字段名,值为不匹配的模式
* @param contain 字段包含指定字符串的条件映射,键为字段名,值为要包含的字符串
* @param notContain 字段不包含指定字符串的条件映射,键为字段名,值为不包含的字符串
* @param startWith 字段以指定字符串开头的条件映射,键为字段名,值为开头字符串
* @param notStartWith 字段不以指定字符串开头的条件映射,键为字段名,值为不开头的字符串
* @param endWith 字段以指定字符串结尾的条件映射,键为字段名,值为结尾字符串
* @param notEndWith 字段不以指定字符串结尾的条件映射,键为字段名,值为不结尾的字符串
* @param great 字段大于条件的映射,键为字段名,值为比较的阈值
* @param less 字段小于条件的映射,键为字段名,值为比较的阈值
* @param greatEqual 字段大于等于条件的映射,键为字段名,值为比较的阈值
* @param lessEqual 字段小于等于条件的映射,键为字段名,值为比较的阈值
* @param inside 字段值在指定范围内的条件映射,键为字段名,值为允许的值列表
* @param notInside 字段值不在指定范围内的条件映射,键为字段名,值为不允许的值列表
* @param between 字段值在指定区间内的条件映射,键为字段名,值为区间范围对象
* @param notBetween 字段值不在指定区间内的条件映射,键为字段名,值为区间范围对象
*/
public record Queryable(
List<String> nullEqual,
@@ -97,6 +137,12 @@ public record Query(
) {
/**
* 区间范围类,用于表示起始值和结束值
* <p>
* 主要用于 between 和 notBetween 查询条件,表示一个数值或时间的区间范围。
* </p>
*
* @param start 区间起始值(包含)
* @param end 区间结束值(包含)
*/
public record Between(
Object start,
@@ -106,17 +152,26 @@ public record Query(
}
/**
* 排序条件类,用于指定排序字段和排序方向
*
* @param column 排序字段名,对应数据库表的列名或实体类的属性名
* @param direction 排序方向ASC表示升序DESC表示降序
*/
public record Sortable(
String column,
Direction direction
) {
/**
* 排序方向枚举
*/
public enum Direction {
/**
* 升序排列
* 升序排列(从小到大)
*/
ASC,
/**
* 降序排列
* 降序排列(从大到小)
*/
DESC,
}
@@ -124,6 +179,12 @@ public record Query(
/**
* 可分页条件类,用于指定分页参数
* <p>
* 页码从0开始计数即第一页的索引为0。
* </p>
*
* @param index 页码索引从0开始0表示第一页
* @param size 每页大小,即每页显示的记录数
*/
public record Pageable(
Integer index,

View File

@@ -0,0 +1,65 @@
package com.lanyuanxiaoyao.service.template.common.controller;
/**
* 查询控制器接口,用于定义统一的查询实体详情和列表的接口规范
* <p>
* 该接口提供了标准的查询功能,支持条件查询、分页查询和详情查询。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*
* <h3>查询条件说明</h3>
* <ul>
* <li><b>空值条件:</b> nullEqual、notNullEqual、empty、notEmpty</li>
* <li><b>相等条件:</b> equal、notEqual</li>
* <li><b>模糊匹配:</b> like、notLike、contain、notContain</li>
* <li><b>前后缀匹配:</b> startWith、notStartWith、endWith、notEndWith</li>
* <li><b>范围条件:</b> great、less、greatEqual、lessEqual</li>
* <li><b>集合条件:</b> inside、notInside</li>
* <li><b>区间条件:</b> between、notBetween</li>
* </ul>
*
* @param <LIST_ITEM> 列表查询结果的实体类型
* @param <DETAIL_ITEM> 详情查询结果的实体类型
*/
public interface QueryController<LIST_ITEM, DETAIL_ITEM> {
String LIST = "/list";
String DETAIL = "/detail/{id}";
/**
* 获取所有实体列表
* <p>
* 查询所有记录,不带任何过滤条件,返回分页格式的数据。
* 适用于获取全量数据的场景。
* </p>
*
* @return 返回包含实体列表的响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception;
/**
* 根据查询条件获取实体列表
* <p>
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
* 查询条件包括相等、模糊匹配、范围查询、集合查询等。
* </p>
*
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
* @return 返回符合条件的实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(Query query) throws Exception;
/**
* 根据ID获取实体详情
* <p>
* 根据主键ID查询单条记录的详细信息。
* 适用于详情页面展示或数据编辑的场景。
* </p>
*
* @param id 实体主键ID
* @return 返回实体详情响应对象,格式:{status: 0, message: "OK", data: {item: 详情数据}}
* @throws Exception 查询过程中可能抛出的异常
*/
GlobalResponse<DETAIL_ITEM> detail(Long id) throws Exception;
}

View File

@@ -2,24 +2,23 @@ package com.lanyuanxiaoyao.service.template.common.controller;
/**
* 删除控制器接口,用于定义统一的删除实体对象的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* DELETE /remove/1
* </pre>
* 该接口提供了标准的删除功能通过主键ID删除单条记录。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*
* @author lanyuanxiaoyao
*/
public interface RemoveController {
String REMOVE = "/remove/{id}";
/**
* 根据ID删除实体对象
* <p>
* 根据主键ID删除指定的记录执行成功后返回操作结果。
* 适用于单条记录删除的场景。
* </p>
*
* @param id 需要删除的实体ID
* @return GlobalResponse<Object> 返回删除结果
* @param id 需要删除的实体主键ID
* @return 返回删除结果响应对象,格式:{status: 0, message: "OK", data: null}
* @throws Exception 删除过程中可能抛出的异常
*/
GlobalResponse<Object> remove(Long id) throws Exception;

View File

@@ -2,28 +2,25 @@ package com.lanyuanxiaoyao.service.template.common.controller;
/**
* 保存控制器接口,用于定义统一的保存实体对象的接口规范
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* // 实体对象的具体字段
* "name": "示例名称",
* "description": "示例描述"
* }
* </pre>
* 该接口提供了标准的保存功能,支持新增和更新操作。
* 所有实现类应当遵循统一的请求响应格式。
* </p>
*
* @author lanyuanxiaoyao
* @param <SAVE_ITEM> 保存操作的实体类型
*/
public interface SaveController<SAVE_ITEM> {
String SAVE = "/save";
/**
* 保存实体对象
* <p>
* 保存或更新实体对象,根据业务逻辑判断是新增还是更新操作。
* 返回保存后的实体ID便于前端获取操作结果。
* </p>
*
* @param item 需要保存的实体对象
* @return GlobalResponse<Long> 返回保存后的实体ID
* @param item 需要保存的实体对象,包含完整的字段信息
* @return 返回保存后的实体ID响应对象格式{status: 0, message: "OK", data: 实体ID}
* @throws Exception 保存过程中可能抛出的异常
*/
GlobalResponse<Long> save(SAVE_ITEM item) throws Exception;

View File

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

View File

@@ -4,9 +4,20 @@ import java.util.Collection;
import java.util.Map;
import java.util.Optional;
/**
* 对象工具类,提供常用的对象判断和处理方法
* <p>
* 该类封装了对象空值判断、空值检查、类型判断等常用功能,
* 用于简化代码中的对象处理逻辑。
* </p>
*/
public class ObjectHelper {
/**
* 判断对象是否为null
* <p>
* 简单的null值检查用于避免空指针异常。
* </p>
*
* @param obj 待检查的对象
* @return 如果对象为null返回true否则返回false
@@ -15,9 +26,11 @@ public class ObjectHelper {
return obj == null;
}
/**
* 判断对象是否不为null
* <p>
* isNull方法的反向操作语义更清晰。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象不为null则返回true否则返回false
@@ -26,82 +39,138 @@ public class ObjectHelper {
return !isNull(obj);
}
/**
* 判断对象是否为空
* <p>
* 支持多种类型的空值判断,包括:
* <ul>
* <li>null值</li>
* <li>集合Collection</li>
* <li>映射Map</li>
* <li>字符序列String、StringBuilder等</li>
* <li>各种基本类型数组</li>
* <li>Optional对象</li>
* </ul>
* </p>
*
* @param obj 待判断的对象
* @return 如果对象为null或为空则返回true否则返回false
*/
public static boolean isEmpty(Object obj) {
// 首先判断对象是否为null
if (isNull(obj)) return true;
// 判断是否为集合类型
else if (obj instanceof Collection<?> collection) return collection.isEmpty();
// 判断是否为Map类型
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;
// 判断是否为Optional类型
else if (obj instanceof Optional<?> optional) return optional.isEmpty();
// 其他情况认为对象不为空
else return false;
if (obj instanceof Collection<?> collection) return collection.isEmpty();
if (obj instanceof Map<?, ?> map) return map.isEmpty();
if (obj instanceof CharSequence sequence) return sequence.isEmpty();
if (obj instanceof Object[] array) return array.length == 0;
if (obj instanceof byte[] array) return array.length == 0;
if (obj instanceof short[] array) return array.length == 0;
if (obj instanceof int[] array) return array.length == 0;
if (obj instanceof long[] array) return array.length == 0;
if (obj instanceof float[] array) return array.length == 0;
if (obj instanceof double[] array) return array.length == 0;
if (obj instanceof char[] array) return array.length == 0;
if (obj instanceof boolean[] array) return array.length == 0;
if (obj instanceof Optional<?> optional) return optional.isEmpty();
return false;
}
/**
* 判断对象是否不为空
* <p>
* isEmpty方法的反向操作语义更清晰。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象不为空则返回true否则返回false
*/
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
/**
* 如果对象为null则返回默认值
* <p>
* 提供对象的null值保护避免空指针异常。
* </p>
*
* @param object 待检查的对象
* @param defaultValue 默认值当object为null时返回
* @return 如果object不为null则返回object否则返回defaultValue
*/
public static <T> T defaultIfNull(final T object, final T defaultValue) {
return isNull(object) ? defaultValue : object;
}
/**
* 判断给定的类是否可比较
* <p>
* 可比较的类型包括枚举、字符序列、实现了Comparable接口的类、基本数据类型。
* </p>
*
* @param clazz 待判断的类对象
* @return 如果类是枚举、字符序列、可比较接口的实现类或基本数据类型则返回true否则返回false
*/
public static boolean isComparable(Class<?> clazz) {
if (isNull(clazz)) return false;
// 判断类是否为可比较类型:枚举、字符序列、可比较接口实现类或基本数据类型
return clazz.isEnum() ||
CharSequence.class.isAssignableFrom(clazz) ||
Comparable.class.isAssignableFrom(clazz) ||
clazz.isPrimitive();
}
/**
* 判断对象是否可比较
* <p>
* 通过对象的类来判断其是否可比较。
* </p>
*
* @param obj 待判断的对象
* @return 如果对象所属的类可比较则返回true否则返回false
*/
public static boolean isComparable(Object obj) {
if (isNull(obj)) return false;
return isComparable(obj.getClass());
}
/**
* 判断给定的类是否为集合类型
*
* @param clazz 待判断的类对象
* @return 如果类是Collection的子类则返回true否则返回false
*/
public static boolean isCollection(Class<?> clazz) {
if (isNull(clazz)) return false;
return Collection.class.isAssignableFrom(clazz);
}
/**
* 判断对象是否为集合类型
*
* @param obj 待判断的对象
* @return 如果对象是集合类型则返回true否则返回false
*/
public static boolean isCollection(Object obj) {
if (isNull(obj)) return false;
return isCollection(obj.getClass());
}
/**
* 判断给定的类是否为字符串类型
*
* @param clazz 待判断的类对象
* @return 如果类是String类型则返回true否则返回false
*/
public static boolean isString(Class<?> clazz) {
if (isNull(clazz)) return false;
return String.class.isAssignableFrom(clazz);
}
/**
* 判断对象是否为字符串类型
*
* @param obj 待判断的对象
* @return 如果对象是字符串类型则返回true否则返回false
*/
public static boolean isString(Object obj) {
if (isNull(obj)) return false;
return isString(obj.getClass());

View File

@@ -3,10 +3,29 @@ package com.lanyuanxiaoyao.service.template.common.service;
import java.util.stream.Stream;
/**
* 分页
* 分页数据封装类
* <p>
* 用于封装分页查询的结果,包含数据流和总记录数。
* 适用于需要流式处理大量数据的场景,同时提供总数用于分页计算。
* </p>
*
* @author lanyuanxiaoyao
* @version 20260106
* <h3>使用场景</h3>
* <ul>
* <li>数据库分页查询结果封装</li>
* <li>大数据量流式处理</li>
* <li>分页控件的数据源</li>
* </ul>
*
* <h3>特点</h3>
* <ul>
* <li>使用Java Record实现不可变线程安全</li>
* <li>支持流式数据处理,内存效率高</li>
* <li>包含总记录数,便于分页计算</li>
* </ul>
*
* @param <ENTITY> 实体类型
* @param items 数据流,包含当前页的所有记录
* @param total 总记录数,用于计算总页数和显示分页信息
*/
public record Page<ENTITY>(Stream<ENTITY> items, long total) {
}

View File

@@ -0,0 +1,91 @@
package com.lanyuanxiaoyao.service.template.common.service;
import com.lanyuanxiaoyao.service.template.common.controller.Query;
import java.util.List;
import java.util.Set;
/**
* 查询服务接口,用于定义统一的查询实体详情和列表的服务规范
* <p>
* 该接口提供了标准的查询功能,支持详情查询、列表查询、分页查询和统计查询。
* 所有实现类应当遵循统一的查询逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface QueryService<ENTITY> {
/**
* 根据ID获取实体详情
* <p>
* 查询单条记录的详细信息如果记录不存在返回null。
* </p>
*
* @param id 实体主键ID
* @return 实体详情如果不存在则返回null
* @throws Exception 查询过程中可能抛出的异常
*/
ENTITY detail(Long id) throws Exception;
/**
* 根据ID获取实体详情如果不存在则抛出异常
* <p>
* 查询单条记录的详细信息,如果记录不存在则抛出异常。
* 适用于需要确保记录存在的场景。
* </p>
*
* @param id 实体主键ID
* @return 实体详情
* @throws Exception 当记录不存在或查询失败时抛出异常
*/
ENTITY detailOrThrow(Long id) throws Exception;
/**
* 获取实体总数
* <p>
* 统计所有记录的数量,不带任何过滤条件。
* </p>
*
* @return 实体总数
* @throws Exception 查询过程中可能抛出的异常
*/
Long count() throws Exception;
/**
* 获取所有实体列表
* <p>
* 查询所有记录,不带任何过滤条件,返回完整列表。
* 适用于数据量较小或需要全量数据的场景。
* </p>
*
* @return 实体列表
* @throws Exception 查询过程中可能抛出的异常
*/
List<ENTITY> list() throws Exception;
/**
* 根据ID集合获取实体列表
* <p>
* 批量查询指定ID的记录返回对应的实体列表。
* 适用于需要批量获取特定记录的场景。
* </p>
*
* @param ids 实体ID集合
* @return 实体列表包含集合中ID对应的记录
* @throws Exception 查询过程中可能抛出的异常
*/
List<ENTITY> list(Set<Long> ids) throws Exception;
/**
* 根据查询条件获取分页实体列表
* <p>
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
* 这是最完整的查询方法,适用于大多数业务场景。
* </p>
*
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
* @return 分页实体列表,包含数据流和总记录数
* @throws Exception 查询过程中可能抛出的异常
*/
Page<ENTITY> list(Query query) throws Exception;
}

View File

@@ -0,0 +1,37 @@
package com.lanyuanxiaoyao.service.template.common.service;
/**
* 删除服务接口,用于定义统一的删除实体对象的服务规范
* <p>
* 该接口提供了标准的删除功能,支持单条记录删除和批量删除。
* 所有实现类应当遵循统一的删除逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface RemoveService<ENTITY> {
/**
* 根据ID删除实体对象
* <p>
* 删除指定ID的单条记录执行成功后无返回值。
* 适用于单条记录删除的场景。
* </p>
*
* @param id 需要删除的实体主键ID
* @throws Exception 删除过程中可能抛出的异常
*/
void remove(Long id) throws Exception;
/**
* 批量删除实体对象
* <p>
* 删除指定ID集合的多条记录执行成功后无返回值。
* 适用于批量删除的场景,提高删除效率。
* </p>
*
* @param ids 需要删除的实体ID集合
* @throws Exception 删除过程中可能抛出的异常
*/
void remove(Iterable<Long> ids) throws Exception;
}

View File

@@ -0,0 +1,38 @@
package com.lanyuanxiaoyao.service.template.common.service;
/**
* 保存服务接口,用于定义统一的保存实体对象的服务规范
* <p>
* 该接口提供了标准的保存功能,支持单条记录保存和批量保存。
* 所有实现类应当遵循统一的保存逻辑和异常处理规范。
* </p>
*
* @param <ENTITY> 实体类型
*/
public interface SaveService<ENTITY> {
/**
* 保存实体对象
* <p>
* 保存或更新单条实体记录,根据业务逻辑判断是新增还是更新操作。
* 返回保存后的实体ID便于后续操作。
* </p>
*
* @param entity 需要保存的实体对象,包含完整的字段信息
* @return 保存后的实体主键ID
* @throws Exception 保存过程中可能抛出的异常
*/
Long save(ENTITY entity) throws Exception;
/**
* 批量保存实体对象
* <p>
* 批量保存或更新多条实体记录,提高数据处理效率。
* 适用于批量数据导入或同步的场景。
* </p>
*
* @param entities 需要保存的实体对象集合
* @throws Exception 保存过程中可能抛出的异常
*/
void save(Iterable<ENTITY> entities) throws Exception;
}

View File

@@ -1,27 +1,4 @@
package com.lanyuanxiaoyao.service.template.common.service;
import com.lanyuanxiaoyao.service.template.common.controller.Query;
import java.util.List;
import java.util.Set;
public interface SimpleService<ENTITY> {
Long save(ENTITY entity) throws Exception;
void save(Iterable<ENTITY> entities) 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;
ENTITY detail(Long id) throws Exception;
ENTITY detailOrThrow(Long id) throws Exception;
void remove(Long id) throws Exception;
void remove(Iterable<Long> ids) throws Exception;
public interface SimpleService<ENTITY> extends SaveService<ENTITY>, QueryService<ENTITY>, RemoveService<ENTITY> {
}

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-service-template-jpa-task</artifactId>
<dependencies>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-jpa</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
<path>
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
</path>
<path>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aquerydsl.entityAccessors=true</arg>
<arg>-Aquerydsl.createDefaultVariable=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,12 +1,11 @@
package com.lanyuanxiaoyao.service.template.jpa.controller;
import com.lanyuanxiaoyao.service.template.common.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.common.controller.Query;
import com.lanyuanxiaoyao.service.template.common.controller.SimpleController;
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
import com.lanyuanxiaoyao.service.template.jpa.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.jpa.service.SimpleServiceSupport;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
@@ -22,78 +21,26 @@ import org.springframework.web.bind.annotation.RequestBody;
* 子类需要实现对应的Mapper函数来完成实体类与传输对象之间的转换。
* </p>
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* // 保存实体
* POST /save
* {
* // 保存项的具体字段
* "name": "示例名称",
* "description": "示例描述"
* }
*
* // 查询列表(无条件)
* GET /list
*
* // 查询列表(带条件)
* POST /list
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 0,
* "size": 10
* }
* }
*
* // 获取详情
* GET /detail/1
*
* // 删除实体
* GET /remove/1
* </pre>
* </p>
*
* <p>
* 支持的查询条件说明:
* <h3>设计特点</h3>
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表(如空字符串、空集合等)</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射(字段名 -> 值)</li>
* <li>notEqual: 指定字段值不相等的条件映射(字段名 -> 值)</li>
* <li>like: 指定字段模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>notLike: 指定字段不模糊匹配的条件映射(字段名 -> 匹配值)</li>
* <li>great: 指定字段大于条件的映射(字段名 -> 值)</li>
* <li>less: 指定字段小于条件的映射(字段名 -> 值)</li>
* <li>greatEqual: 指定字段大于等于条件的映射(字段名 -> 值)</li>
* <li>lessEqual: 指定字段小于等于条件的映射(字段名 -> 值)</li>
* <li>in: 指定字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>between: 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>泛型设计,支持任意实体类型和数据转换</li>
* <li>统一的异常处理和事务管理</li>
* <li>支持条件查询、分页查询和详情查询</li>
* <li>提供抽象的Mapper方法便于子类实现数据转换逻辑</li>
* </ul>
*
* <h3>使用说明</h3>
* <p>子类需要实现以下抽象方法:</p>
* <ul>
* <li>saveItemMapper(): 保存项到实体的转换函数</li>
* <li>listItemMapper(): 实体到列表项的转换函数</li>
* <li>detailItemMapper(): 实体到详情项的转换函数</li>
* </ul>
* </p>
*
* @param <ENTITY> 实体类型必须继承SimpleEntity
* @param <SAVE_ITEM> 保存项类型
* @param <LIST_ITEM> 列表项类型
* @param <DETAIL_ITEM> 详情项类型
* @author lanyuanxiaoyao
*/
@Slf4j
public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> implements SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> {
@@ -110,9 +57,13 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 保存实体对象
* <p>
* 将保存项转换为实体对象后保存返回保存后的实体ID。
* 支持新增和更新操作,通过事务保证数据一致性。
* </p>
*
* @param item 需要保存的项
* @return GlobalResponse<Long> 返回保存后的实体ID
* @return 返回保存后的实体ID响应对象格式{status: 0, message: "OK", data: 实体ID}
* @throws Exception 保存过程中可能抛出的异常
*/
@Transactional(rollbackFor = Throwable.class)
@@ -125,17 +76,21 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 获取所有实体列表
* <p>
* 查询所有记录,不带任何过滤条件,返回分页格式的数据。
* 将实体对象转换为列表项对象后返回。
* </p>
*
* @return GlobalCrudResponse<LIST_ITEM> 返回实体列表
* @return 返回实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@GetMapping(LIST)
@Override
public GlobalResponse<Map<String, Object>> list() throws Exception {
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception {
var mapper = listItemMapper();
var result = service.list();
return GlobalResponse.responseCrudData(
return GlobalResponse.responseListData(
result
.stream()
.map(entity -> {
@@ -152,21 +107,25 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 根据查询条件获取实体列表
* <p>
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
* 将实体对象转换为列表项对象后返回。
* </p>
*
* @param query 查询条件对象
* @return GlobalCrudResponse<LIST_ITEM> 返回符合条件的实体列表
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
* @return 返回符合条件的实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@PostMapping(LIST)
@Override
public GlobalResponse<Map<String, Object>> list(@RequestBody com.lanyuanxiaoyao.service.template.common.controller.Query query) throws Exception {
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(@RequestBody Query query) throws Exception {
if (ObjectHelper.isNull(query)) {
return GlobalResponse.responseCrudData(List.of(), 0);
return GlobalResponse.responseListData();
}
var mapper = listItemMapper();
var result = service.list(query);
return GlobalResponse.responseCrudData(
return GlobalResponse.responseListData(
result.items()
.map(entity -> {
try {
@@ -182,9 +141,13 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 根据ID获取实体详情
* <p>
* 根据主键ID查询单条记录的详细信息转换为详情项对象后返回。
* 如果记录不存在则抛出异常。
* </p>
*
* @param id 实体ID
* @return GlobalResponse<DETAIL_ITEM> 返回实体详情
* @param id 实体主键ID
* @return 返回实体详情响应对象,格式:{status: 0, message: "OK", data: 详情数据}
* @throws Exception 查询过程中可能抛出的异常
*/
@Transactional(readOnly = true)
@@ -197,9 +160,13 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 根据ID删除实体对象
* <p>
* 根据主键ID删除指定的记录执行成功后返回成功响应。
* 通过事务保证删除操作的一致性。
* </p>
*
* @param id 需要删除的实体ID
* @return GlobalResponse<Object> 返回删除结果
* @param id 需要删除的实体主键ID
* @return 返回删除结果响应对象,格式:{status: 0, message: "OK", data: null}
* @throws Exception 删除过程中可能抛出的异常
*/
@Transactional(rollbackFor = Throwable.class)
@@ -212,6 +179,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 保存项映射器,将保存项转换为实体对象
* <p>
* 子类需要实现此方法,定义保存项到实体的转换逻辑。
* </p>
*
* @return Function<SAVE_ITEM, ENTITY> 保存项到实体的转换函数
*/
@@ -219,6 +189,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 列表项映射器,将实体对象转换为列表项
* <p>
* 子类需要实现此方法,定义实体到列表项的转换逻辑。
* </p>
*
* @return Function<ENTITY, LIST_ITEM> 实体到列表项的转换函数
*/
@@ -226,6 +199,9 @@ public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_
/**
* 详情项映射器,将实体对象转换为详情项
* <p>
* 子类需要实现此方法,定义实体到详情项的转换逻辑。
* </p>
*
* @return Function<ENTITY, DETAIL_ITEM> 实体到详情项的转换函数
*/

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.service.template.jpa.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@@ -7,7 +8,6 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
@@ -27,8 +27,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
* <li>@Comment: 为数据库字段添加注释</li>
* </ul>
* </p>
*
* @author lanyuanxiaoyao
*/
@Getter
@Setter
@@ -45,6 +43,6 @@ public class IdOnlyEntity {
*/
@Id
@SnowflakeId
@Comment("记录唯一标记")
@Column(comment = "记录唯一标记")
private Long id;
}

View File

@@ -1,5 +1,6 @@
package com.lanyuanxiaoyao.service.template.jpa.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@@ -7,7 +8,6 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
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;
@@ -30,7 +30,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
* </ul>
* </p>
*
* @author lanyuanxiaoyao
*/
@Getter
@Setter
@@ -46,7 +45,7 @@ public class SimpleEntity extends IdOnlyEntity {
* </p>
*/
@CreatedDate
@Comment("记录创建时间")
@Column(comment = "记录创建时间")
private LocalDateTime createdTime;
/**
@@ -56,6 +55,6 @@ public class SimpleEntity extends IdOnlyEntity {
* </p>
*/
@LastModifiedDate
@Comment("记录更新时间")
@Column(comment = "记录更新时间")
private LocalDateTime modifiedTime;
}

View File

@@ -38,8 +38,6 @@ import org.springframework.util.ClassUtils;
* );
* </pre>
* </p>
*
* @author lanyuanxiaoyao
*/
public class DatabaseHelper {
public static void generateDDL(

View File

@@ -97,8 +97,6 @@ import org.springframework.data.repository.query.ListQueryByExampleExecutor;
* </p>
*
* @param <E> 实体类型
* @param <ID> 实体ID类型
* @author lanyuanxiaoyao
*/
@NoRepositoryBean
public interface SimpleRepository<E> extends FenixJpaRepository<E, Long>, FenixJpaSpecificationExecutor<E>, ListQueryByExampleExecutor<E>, ListQuerydslPredicateExecutor<E> {

View File

@@ -31,67 +31,30 @@ import org.springframework.data.domain.Sort;
* 该类实现了SimpleService接口提供实体的增删改查等基本操作。
* 通过继承此类,可以快速实现常见的业务逻辑功能,包括:
* <ul>
* <li>实体的保存和更新</li>
* <li>实体的保存和更新(支持部分字段更新)</li>
* <li>实体的条件查询和分页查询</li>
* <li>实体的详情查询(多种方式)</li>
* <li>实体的删除操作</li>
* <li>实体的删除操作(支持批量删除)</li>
* <li>动态查询条件构建</li>
* </ul>
* </p>
*
* <p>
* 查询条件说明:
* <h3>设计特点</h3>
* <ul>
* <li>nullEqual: 指定字段值为null的条件列表</li>
* <li>notNullEqual: 指定字段值不为null的条件列表</li>
* <li>empty: 指定字段值为空的条件列表(如空字符串、空集合等)</li>
* <li>notEmpty: 指定字段值不为空的条件列表</li>
* <li>equal: 指定字段值相等的条件映射(字段名 -> 值)</li>
* <li>notEqual: 指定字段值不相等的条件映射(字段名 -> 值)</li>
* <li>contain: 指定字段包含指定字符串的条件映射(字段名 -> 包含值)</li>
* <li>notContain: 指定字段不包含指定字符串的条件映射(字段名 -> 不包含值)</li>
* <li>startWith: 指定字段以指定字符串开头的条件映射(字段名 -> 开头值)</li>
* <li>notStartWith: 指定字段不以指定字符串开头的条件映射(字段名 -> 不开头值)</li>
* <li>endWith: 指定字段以指定字符串结尾的条件映射(字段名 -> 结尾值)</li>
* <li>notEndWith: 指定字段不以指定字符串结尾的条件映射(字段名 -> 不结尾值)</li>
* <li>great: 指定字段大于条件的映射(字段名 -> 值)</li>
* <li>less: 指定字段小于条件的映射(字段名 -> 值)</li>
* <li>greatEqual: 指定字段大于等于条件的映射(字段名 -> 值)</li>
* <li>lessEqual: 指定字段小于等于条件的映射(字段名 -> 值)</li>
* <li>in: 指定字段值在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>notIn: 指定字段值不在指定范围内的条件映射(字段名 -> 值列表)</li>
* <li>between: 指定字段值在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>notBetween: 指定字段值不在指定区间内的条件映射(字段名 -> 区间范围)</li>
* <li>泛型设计,支持任意实体类型</li>
* <li>事务管理,确保数据一致性</li>
* <li>动态查询条件,支持复杂的业务查询</li>
* <li>部分更新,只更新非空字段</li>
* <li>可扩展的查询条件构建</li>
* </ul>
* </p>
*
* <p>
* 前端传入的JSON格式示例:
* <pre>
* {
* "query": {
* "equal": {
* "status": "ACTIVE"
* },
* "like": {
* "name": "关键字"
* }
* },
* "sort": [
* {
* "column": "createdTime",
* "direction": "DESC"
* }
* ],
* "page": {
* "index": 1,
* "size": 10
* }
* }
* </pre>
* </p>
* <h3>使用说明</h3>
* <p>子类可以重写以下方法:</p>
* <ul>
* <li>listPredicate(): 添加自定义的查询条件</li>
* </ul>
*
* @param <ENTITY> 实体类型必须继承SimpleEntity
* @author lanyuanxiaoyao
*/
@Slf4j
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
@@ -117,7 +80,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* </p>
*
* @param entity 需要保存的实体对象
* @return Long 返回保存后的实体ID
* @return 返回保存后的实体ID
*/
@Transactional(rollbackOn = Throwable.class)
@Override
@@ -144,10 +107,10 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
/**
* 统计符合条件的实体数量
* <p>
* 根据[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件统计实体数量。
* 根据listPredicate方法构建的条件统计实体数量。
* </p>
*
* @return Long 返回符合条件的实体数量
* @return 返回符合条件的实体数量
*/
@Override
public Long count() {
@@ -157,10 +120,10 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
/**
* 获取所有符合条件的实体列表
* <p>
* 根据[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件查询所有实体。
* 根据listPredicate方法构建的条件查询所有实体。
* </p>
*
* @return List<ENTITY> 返回符合条件的实体列表
* @return 返回符合条件的实体列表
*/
@Override
public List<ENTITY> list() {
@@ -170,11 +133,11 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
/**
* 根据ID集合获取实体列表
* <p>
* 根据提供的ID集合查询对应的实体列表并结合[listPredicate](file:///Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java#L261-L263)方法构建的条件。
* 根据提供的ID集合查询对应的实体列表并结合listPredicate方法构建的条件。
* </p>
*
* @param ids ID集合
* @return List<ENTITY> 返回ID集合对应的实体列表
* @return 返回ID集合对应的实体列表
*/
@Override
public List<ENTITY> list(Set<Long> ids) {
@@ -195,14 +158,14 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
/**
* 解析字段路径
* <p>
* 支持多级字段路径解析,使用"/"分隔多级字段。
* 例如: "user/name" 表示实体的user属性的name字段。
* 支持多级字段路径解析,使用"."分隔多级字段。
* 例如: "user.name" 表示实体的user属性的name字段。
* </p>
*
* @param root JPA Criteria查询根节点
* @param column 字段路径字符串
* @param <Y> 字段类型
* @return Path<Y> 返回字段路径对象
* @return 返回字段路径对象
* @throws IllegalArgumentException 当字段路径为空时抛出
*/
private <Y> Path<Y> column(Root<ENTITY> root, String column) {
@@ -221,13 +184,14 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 处理字段值
* <p>
* 对于枚举类型字段,将字符串值转换为对应的枚举值。
* 对于LocalDateTime类型字段将字符串转换为时间对象。
* 其他类型直接返回原值。
* </p>
*
* @param column 字段路径
* @param value 字段值
* @param <Y> 字段类型
* @return Object 处理后的字段值
* @return 处理后的字段值
* @throws IllegalArgumentException 当枚举类型字段的值不是字符串时抛出
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -260,7 +224,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* @param root JPA Criteria查询根节点
* @param query JPA Criteria查询对象
* @param builder JPA Criteria构建器
* @return List<Predicate> 返回构建的谓词列表
* @return 返回构建的谓词列表
*/
@SuppressWarnings("unchecked")
protected Predicate queryPredicates(Query.Queryable queryable, Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
@@ -422,6 +386,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 检查字段类型是否可比较
*
* @param path 字段路径
* @param value 比较值
* @param column 字段名称
* @throws NotComparableException 当字段类型不可比较时抛出
*/
@@ -434,6 +399,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
/**
* 检查区间值是否可比较
*
* @param path 字段路径
* @param value 区间对象
* @param column 字段名称
* @throws NotComparableException 当区间值不可比较时抛出
@@ -473,6 +439,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 检查字段类型是否为字符串
*
* @param path 字段路径
* @param value 比较值
* @param column 字段名称
* @throws NotStringException 当字段类型不是字符串时抛出
*/
@@ -486,13 +453,13 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 构建列表查询条件
* <p>
* 子类可以重写此方法以添加特定的查询条件。
* 默认返回空列表,表示不添加额外条件。
* 默认返回null,表示不添加额外条件。
* </p>
*
* @param root JPA Criteria查询根节点
* @param query JPA Criteria查询对象
* @param builder JPA Criteria构建器
* @return List<Predicate> 返回查询条件谓词列表
* @return 返回查询条件谓词
*/
protected Predicate listPredicate(Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return null;
@@ -506,7 +473,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* </p>
*
* @param listQuery 查询条件对象
* @return Page<ENTITY> 返回分页查询结果
* @return 返回分页查询结果
*/
@Override
public Page<ENTITY> list(Query listQuery) {
@@ -545,7 +512,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* </p>
*
* @param id 实体ID
* @return Optional<ENTITY> 返回实体详情的Optional包装
* @return 返回实体详情的Optional包装
*/
private Optional<ENTITY> detailOptional(Long id) {
if (ObjectHelper.isNull(id)) {
@@ -569,7 +536,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* </p>
*
* @param id 实体ID
* @return ENTITY 返回实体详情不存在时返回null
* @return 返回实体详情不存在时返回null
*/
@Named("detail")
@Override
@@ -584,7 +551,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* </p>
*
* @param id 实体ID
* @return ENTITY 返回实体详情
* @return 返回实体详情
* @throws IdNotFoundException 当实体不存在时抛出
*/
@Named("detailOrThrow")
@@ -600,7 +567,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 如果ID为空则不执行任何操作。
* </p>
*
* @param id 实体ID
* @param id 实体主键ID
*/
@Transactional(rollbackOn = Throwable.class)
@Override
@@ -618,7 +585,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
* 如果ID集合为空则不执行任何操作。
* </p>
*
* @param ids 实体ID集合
* @param ids 实体主键ID集合
*/
@Transactional(rollbackOn = Throwable.class)
@Override

View File

@@ -11,8 +11,6 @@ import org.springframework.util.Assert;
/**
* Helper测试类
* 用于测试驼峰命名法转下划线命名法的功能
*
* @author lanyuanxiaoyao
*/
@Slf4j
public class HelperTest {

View File

@@ -26,10 +26,6 @@ import org.springframework.web.client.RestTemplate;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
/**
* @author lanyuanxiaoyao
* @version 20250814
*/
@SpringBootApplication
@EnableFenix
@EnableJpaAuditing

View File

@@ -10,13 +10,13 @@ import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
@@ -31,13 +31,11 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("企业")
@Table(comment = "企业")
public class Company extends SimpleEntity {
@Column(nullable = false)
@Comment("名称")
@Column(nullable = false, comment = "名称")
private String name;
@Column(nullable = false)
@Comment("成员数")
@Column(nullable = false, comment = "成员数")
private Integer members;
@OneToMany(mappedBy = "company")

View File

@@ -12,13 +12,13 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKeyEnumerated;
import jakarta.persistence.Table;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
@@ -33,17 +33,14 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("员工")
@Table(comment = "员工")
public class Employee extends SimpleEntity {
@Column(nullable = false)
@Comment("名称")
@Column(nullable = false, comment = "名称")
private String name;
@Column(nullable = false)
@Comment("年龄")
@Column(nullable = false, comment = "年龄")
private Integer age;
@Column(nullable = false)
@Column(nullable = false, comment = "角色")
@Enumerated(EnumType.STRING)
@Comment("角色")
private Role role;
@ManyToOne

View File

@@ -5,11 +5,11 @@ import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
@@ -24,18 +24,15 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Comment("报告")
@Table(comment = "报告")
public class Report extends SimpleEntity {
@Column(nullable = false)
@Comment("分数")
@Column(nullable = false, comment = "分数")
private Double score = 0.0;
@Column(nullable = false)
@Column(nullable = false, comment = "等级")
@Enumerated(EnumType.STRING)
@Comment("等级")
private Level level;
@Column(nullable = false)
@Comment("员工id")
@Column(nullable = false, comment = "员工 ID")
private Long employeeId;
public enum Level {

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template</artifactId>
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-service-template-web</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>