diff --git a/backend/readme.md b/backend/readme.md index 0e34df47..411a3607 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -7,4 +7,9 @@ query.setCreateTimeRange( ); query.setEnabled(true); query.setCreateBy("admin"); + + +@QueryField(type = QueryType.IN) +private String status; // 可以传入 "ACTIVE,PENDING,CLOSED" + ``` \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/SoftDelete.java b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/SoftDelete.java new file mode 100644 index 00000000..bd155e1f --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/annotation/SoftDelete.java @@ -0,0 +1,12 @@ +package com.qqchen.deploy.backend.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SoftDelete { + boolean value() default true; // 默认启用软删除 +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/query/BaseQuery.java b/backend/src/main/java/com/qqchen/deploy/backend/common/query/BaseQuery.java index 8084ec9e..58a291d6 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/query/BaseQuery.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/query/BaseQuery.java @@ -10,18 +10,15 @@ import java.time.LocalDateTime; @Data public abstract class BaseQuery implements Serializable { private Integer pageNum = 1; - private Integer pageSize = 10; - private String sortField = "createTime"; - private String sortOrder = "desc"; // 通用状态查询 @QueryField(field = "enabled") private Boolean enabled; - @QueryField(field = "deleted") + @QueryField(field = "deleted", type = QueryType.EQUAL) private Boolean deleted; // 创建时间范围查询 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/repository/BaseRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/common/repository/BaseRepository.java index c264634a..90b0440a 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/repository/BaseRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/repository/BaseRepository.java @@ -11,8 +11,7 @@ import java.util.List; import java.util.Optional; @NoRepositoryBean -public interface BaseRepository, ID extends Serializable> - extends JpaRepository, QuerydslPredicateExecutor { +public interface BaseRepository, ID extends Serializable> extends JpaRepository, QuerydslPredicateExecutor { @Override @Query("select e from #{#entityName} e where e.deleted = false") diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/service/impl/BaseServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/common/service/impl/BaseServiceImpl.java index 536c2105..1dcc69aa 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/common/service/impl/BaseServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/service/impl/BaseServiceImpl.java @@ -1,10 +1,11 @@ package com.qqchen.deploy.backend.common.service.impl; import java.math.BigDecimal; -import java.util.stream.Collectors; -import java.util.Collection; +import java.time.LocalDateTime; +import java.util.Arrays; import java.util.List; +import com.qqchen.deploy.backend.common.annotation.SoftDelete; import com.qqchen.deploy.backend.common.domain.Entity; import com.qqchen.deploy.backend.common.enums.QueryType; import com.qqchen.deploy.backend.common.query.BaseQuery; @@ -13,6 +14,7 @@ import com.qqchen.deploy.backend.common.query.Range; import com.qqchen.deploy.backend.common.repository.BaseRepository; import com.qqchen.deploy.backend.common.annotation.QueryField; import com.qqchen.deploy.backend.common.service.BaseService; +import com.qqchen.deploy.backend.common.utils.EntityPathResolver; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Path; @@ -30,17 +32,233 @@ import java.lang.reflect.Field; import java.util.Date; @Transactional -public abstract class BaseServiceImpl, ID extends Serializable> implements BaseService { +public abstract class BaseServiceImpl, ID extends Serializable> + implements BaseService { protected final BaseRepository repository; - private final EntityPath entityPath; + protected final EntityPath entityPath; protected BaseServiceImpl(BaseRepository repository) { this.repository = repository; this.entityPath = getEntityPath(); } + @SuppressWarnings("unchecked") + private EntityPath getEntityPath() { + Class[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class); + if (genericTypes == null || genericTypes.length < 2) { + throw new IllegalStateException("Could not resolve generic type arguments"); + } + + Class entityClass = (Class) genericTypes[0]; + try { + String qClassName = entityClass.getPackageName() + ".Q" + entityClass.getSimpleName(); + Class qClass = Class.forName(qClassName); + Field instanceField = qClass.getDeclaredField(entityClass.getSimpleName().toLowerCase()); + return (EntityPath) instanceField.get(null); + } catch (Exception e) { + throw new RuntimeException("Failed to get Q class for " + entityClass.getName(), e); + } + } + + @Override + public Page page(BaseQuery query) { + BooleanBuilder builder = new BooleanBuilder().and(Expressions.asBoolean(true).isTrue()); + + if (query != null) { + buildQueryPredicate(query, builder); + } + if (query == null || query.getDeleted() == null) { + Class[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class); + if (genericTypes != null && genericTypes.length > 0) { + Class entityClass = (Class) genericTypes[0]; + SoftDelete softDelete = entityClass.getAnnotation(SoftDelete.class); + + if (softDelete != null && softDelete.value()) { + Path deletedPath = EntityPathResolver.getPath(entityPath, "deleted"); + if (deletedPath instanceof BooleanPath) { + builder.and(((BooleanPath) deletedPath).eq(false)); + } + } + } + } + + return repository.findAll(builder, createPageRequest(query)); + } + + private void buildQueryPredicate(BaseQuery query, BooleanBuilder builder) { + // 处理当前类的字段 + processClassFields(query, query.getClass(), builder); + } + + private void processClassFields(Object query, Class clazz, BooleanBuilder builder) { + // 如果到达Object类或null,则停止递归 + if (clazz == null || clazz == Object.class) { + return; + } + + // 处理父类的字段 + processClassFields(query, clazz.getSuperclass(), builder); + + // 处理当前类的字段 + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + try { + Object value = field.get(query); + if (value != null && StringUtils.hasText(value.toString())) { + QueryField queryField = field.getAnnotation(QueryField.class); + if (queryField != null) { + String fieldName = StringUtils.hasText(queryField.field()) ? + queryField.field() : field.getName(); + Path path = EntityPathResolver.getPath(entityPath, fieldName); + if (path != null) { + addCondition(builder, path, value, queryField.type()); + } + } + } + } catch (Exception e) { + // 忽略无法处理的字段 + } + } + } + + private void addCondition(BooleanBuilder builder, Path path, Object value, QueryType queryType) { + Predicate condition = null; + + if (path instanceof StringPath) { + condition = createStringCondition((StringPath) path, value.toString(), queryType); + } else if (path instanceof BooleanPath) { + condition = ((BooleanPath) path).eq(Boolean.valueOf(value.toString())); + } else if (path instanceof NumberPath) { + condition = createNumberCondition((NumberPath) path, value, queryType); + } else if (path instanceof DateTimePath) { + condition = createDateCondition((DateTimePath) path, value, queryType); + } + + if (condition != null) { + builder.and(condition); + } + } + + private Predicate createStringCondition(StringPath path, String value, QueryType queryType) { + switch (queryType) { + case EQUAL: + return path.eq(value); + case LIKE: + return path.containsIgnoreCase(value); + case START_WITH: + return path.startsWithIgnoreCase(value); + case END_WITH: + return path.endsWithIgnoreCase(value); + case IN: + if (value.contains(",")) { + List values = Arrays.asList(value.split(",")); + return path.in(values); + } + return path.eq(value); + default: + return null; + } + } + + @SuppressWarnings("unchecked") + private > Predicate createNumberCondition(NumberPath path, Object value, QueryType queryType) { + N numValue = (N) parseNumber(value, path.getType()); + if (numValue != null) { + switch (queryType) { + case EQUAL: + return path.eq(numValue); + case GREATER_THAN: + return path.gt(numValue); + case LESS_THAN: + return path.lt(numValue); + case GREATER_EQUAL: + return path.goe(numValue); + case LESS_EQUAL: + return path.loe(numValue); + case BETWEEN: + if (value instanceof Range) { + Range range = (Range) value; + N from = (N) parseNumber(range.getFrom(), path.getType()); + N to = (N) parseNumber(range.getTo(), path.getType()); + if (from != null && to != null) { + return path.between(from, to); + } + } + default: + return null; + } + } + return null; + } + + @SuppressWarnings("unchecked") + private > Predicate createDateCondition(DateTimePath path, Object value, QueryType queryType) { + if (value instanceof DateRange range) { + LocalDateTime from = range.getFrom(); + LocalDateTime to = range.getTo(); + + switch (queryType) { + case EQUAL: + return from != null ? path.eq((T) from) : null; + case GREATER_THAN: + return from != null ? path.gt((T) from) : null; + case LESS_THAN: + return to != null ? path.lt((T) to) : null; + case BETWEEN: + if (from != null && to != null) { + return path.between((T) from, (T) to); + } else if (from != null) { + return path.goe((T) from); + } else if (to != null) { + return path.loe((T) to); + } + default: + return null; + } + } + return null; + } + + private Number parseNumber(Object value, Class targetType) { + if (value == null) return null; + try { + String strValue = String.valueOf(value); + if (targetType == Long.class) { + return Long.valueOf(strValue); + } else if (targetType == Integer.class) { + return Integer.valueOf(strValue); + } else if (targetType == Double.class) { + return Double.valueOf(strValue); + } else if (targetType == Float.class) { + return Float.valueOf(strValue); + } else if (targetType == BigDecimal.class) { + return new BigDecimal(strValue); + } + } catch (NumberFormatException e) { + // 忽略转换错误 + } + return null; + } + + protected PageRequest createPageRequest(BaseQuery query) { + if (query == null) { + return PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createTime")); + } + + Sort sort = StringUtils.hasText(query.getSortField()) ? + Sort.by(Sort.Direction.fromString(query.getSortOrder()), query.getSortField()) : + Sort.by(Sort.Direction.DESC, "createTime"); + + return PageRequest.of( + query.getPageNum() - 1, + query.getPageSize(), + sort + ); + } + @Override public T create(T entity) { return repository.save(entity); @@ -66,259 +284,4 @@ public abstract class BaseServiceImpl, ID extends Serializa public List findAll() { return repository.findAll(); } - - // 自动获取Q类实例 - // 自动获取Q类实例 - @SuppressWarnings("unchecked") - private EntityPath getEntityPath() { - Class[] genericTypes = GenericTypeResolver.resolveTypeArguments(getClass(), BaseServiceImpl.class); - if (genericTypes == null || genericTypes.length < 2) { - throw new IllegalStateException("Could not resolve generic type arguments for " + getClass().getName()); - } - - Class entityClass = (Class) genericTypes[0]; - - try { - // 获取Q类 - String qClassName = entityClass.getPackageName() + ".Q" + entityClass.getSimpleName(); - Class qClass = Class.forName(qClassName); - - // 获取静态实例字段 - Field instanceField = qClass.getDeclaredField(entityClass.getSimpleName().toLowerCase()); - return (EntityPath) instanceField.get(null); - } catch (Exception e) { - throw new RuntimeException("Failed to get Q class for " + entityClass.getName(), e); - } - } - - @Override - public Page page(BaseQuery query) { - return repository.findAll( - createPredicate(query), - createPageRequest(query) - ); - } - - protected Predicate createPredicate(BaseQuery query) { - BooleanBuilder builder = new BooleanBuilder(); - processFields(query, builder, entityPath, ""); - return builder.getValue(); - } - - private void processFields(Object query, BooleanBuilder builder, EntityPath currentPath, String prefix) { - Field[] fields = query.getClass().getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - try { - Object value = field.get(query); - if (value != null) { - QueryField queryField = field.getAnnotation(QueryField.class); - if (queryField != null) { - String fieldName = StringUtils.hasText(queryField.field()) ? - queryField.field() : field.getName(); - String fullPath = StringUtils.hasText(prefix) ? prefix + "." + fieldName : fieldName; - - Path path = EntityPathResolver.getPath(currentPath, fieldName); - if (path != null) { - addPredicate(builder, path, value, queryField.type()); - } - } else if (!isBaseType(field.getType()) && !isCollection(field.getType())) { - // 处理嵌套对象 - String nestedPrefix = StringUtils.hasText(prefix) ? - prefix + "." + field.getName() : field.getName(); - Path nestedPath = EntityPathResolver.getPath(currentPath, field.getName()); - if (nestedPath instanceof EntityPath) { - processFields(value, builder, (EntityPath) nestedPath, nestedPrefix); - } - } - } - } catch (Exception e) { - // 忽略无法处理的字段 - } - } - } - - private void addPredicate(BooleanBuilder builder, Path path, Object value, QueryType queryType) { - if (path instanceof StringPath) { - addStringPredicate(builder, (StringPath) path, value, queryType); - } else if (path instanceof BooleanPath) { - addBooleanPredicate(builder, (BooleanPath) path, value); - } else if (path instanceof NumberPath) { - addNumberPredicate(builder, (NumberPath) path, value, queryType); - } else if (path instanceof DateTimePath) { - addDatePredicate(builder, (DateTimePath) path, value, queryType); - } else if (path instanceof EnumPath) { - addEnumPredicate(builder, (EnumPath) path, value); - } - } - - private void addStringPredicate(BooleanBuilder builder, StringPath path, Object value, QueryType queryType) { - switch (queryType) { - case EQUAL: - builder.and(path.eq(value.toString())); - break; - case LIKE: - builder.and(path.containsIgnoreCase(value.toString())); - break; - case START_WITH: - builder.and(path.startsWithIgnoreCase(value.toString())); - break; - case END_WITH: - builder.and(path.endsWithIgnoreCase(value.toString())); - break; - case IN: - if (value instanceof Collection) { - Collection collection = (Collection) value; - List strings = collection.stream() - .map(Object::toString) - .collect(Collectors.toList()); - builder.and(path.in(strings)); - } - break; - } - } - - @SuppressWarnings("unchecked") - private > void addNumberPredicate(BooleanBuilder builder, - NumberPath path, Object value, QueryType queryType) { - N numValue = (N) parseNumber(value, path.getType()); - if (numValue != null) { - switch (queryType) { - case EQUAL: - builder.and(path.eq(numValue)); - break; - case GREATER_THAN: - builder.and(path.gt(numValue)); - break; - case LESS_THAN: - builder.and(path.lt(numValue)); - break; - case GREATER_EQUAL: - builder.and(path.goe(numValue)); - break; - case LESS_EQUAL: - builder.and(path.loe(numValue)); - break; - case BETWEEN: - if (value instanceof Range) { - Range range = (Range) value; - N from = (N) parseNumber(range.getFrom(), path.getType()); - N to = (N) parseNumber(range.getTo(), path.getType()); - if (from != null && to != null) { - builder.and(path.between(from, to)); - } - } - break; - } - } - } - - @SuppressWarnings("unchecked") - private > void addDatePredicate(BooleanBuilder builder, DateTimePath path, Object value, QueryType queryType) { - if (value instanceof DateRange range) { - switch (queryType) { - case EQUAL: - if (range.getFrom() != null) { - builder.and(path.eq((T) range.getFrom())); - } - break; - case GREATER_THAN: - if (range.getFrom() != null) { - builder.and(path.gt((T) range.getFrom())); - } - break; - case LESS_THAN: - if (range.getTo() != null) { - builder.and(path.lt((T) range.getTo())); - } - break; - case BETWEEN: - if (range.isValid()) { - builder.and(path.between((T) range.getFrom(), (T) range.getTo())); - } else if (range.hasFrom()) { - builder.and(path.goe((T) range.getFrom())); - } else if (range.hasTo()) { - builder.and(path.loe((T) range.getTo())); - } - break; - } - } - } - - private void addBooleanPredicate(BooleanBuilder builder, BooleanPath path, Object value) { - if (value instanceof Boolean) { - builder.and(path.eq((Boolean) value)); - } - } - - private void addEnumPredicate(BooleanBuilder builder, EnumPath path, Object value) { - if (value instanceof Enum) { - builder.and(path.eq(value)); - } - } - - private boolean isBaseType(Class clazz) { - return clazz.isPrimitive() - || Number.class.isAssignableFrom(clazz) - || String.class.equals(clazz) - || Boolean.class.equals(clazz) - || Date.class.isAssignableFrom(clazz) - || clazz.isEnum(); - } - - private boolean isCollection(Class clazz) { - return Collection.class.isAssignableFrom(clazz); - } - - protected PageRequest createPageRequest(BaseQuery query) { - if (query == null) { - // 如果query为null,使用默认值 - return PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createTime")); - } - - Sort sort = StringUtils.hasText(query.getSortField()) ? - Sort.by(Sort.Direction.fromString(query.getSortOrder()), query.getSortField()) : - Sort.by(Sort.Direction.DESC, "createTime"); - - return PageRequest.of( - query.getPageNum() - 1, - query.getPageSize(), - sort - ); - } - - private Number parseNumber(Object value, Class targetType) { - if (value == null) return null; - try { - String strValue = String.valueOf(value); - if (targetType == Long.class) { - return Long.valueOf(strValue); - } else if (targetType == Integer.class) { - return Integer.valueOf(strValue); - } else if (targetType == Double.class) { - return Double.valueOf(strValue); - } else if (targetType == Float.class) { - return Float.valueOf(strValue); - } else if (targetType == BigDecimal.class) { - return new BigDecimal(strValue); - } - } catch (NumberFormatException e) { - // 忽略转换错误 - } - return null; - } - - // EntityPathResolver 用于解析实体路径 - private static class EntityPathResolver { - public static Path getPath(EntityPath entityPath, String fieldName) { - try { - Field field = entityPath.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return (Path) field.get(entityPath); - } catch (Exception e) { - return null; - } - } - } - } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/common/utils/EntityPathResolver.java b/backend/src/main/java/com/qqchen/deploy/backend/common/utils/EntityPathResolver.java new file mode 100644 index 00000000..474f5e33 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/common/utils/EntityPathResolver.java @@ -0,0 +1,46 @@ +package com.qqchen.deploy.backend.common.utils; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; + +public class EntityPathResolver { + private static final Logger log = LoggerFactory.getLogger(EntityPathResolver.class); + + public static Path getPath(EntityPath entityPath, String fieldName) { + try { + // 首先尝试直接获取字段 + try { + Field field = entityPath.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (Path) field.get(entityPath); + } catch (NoSuchFieldException e) { + // 如果找不到,尝试从_super中获取 + Field superField = entityPath.getClass().getDeclaredField("_super"); + superField.setAccessible(true); + Object superInstance = superField.get(entityPath); + + if (superInstance != null) { + try { + Field field = superInstance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (Path) field.get(superInstance); + } catch (NoSuchFieldException ex) { + // 打印调试信息 + log.debug("Field {} not found in _super, available fields:", fieldName); + for (Field field : superInstance.getClass().getDeclaredFields()) { + log.debug("Field name in _super: {}", field.getName()); + } + } + } + } + } catch (Exception e) { + log.error("Error resolving path for field {}: {}", fieldName, e.getMessage()); + } + + return null; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/User.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/User.java index fb65b692..151c70d0 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/User.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/User.java @@ -1,5 +1,6 @@ package com.qqchen.deploy.backend.entity; +import com.qqchen.deploy.backend.common.annotation.SoftDelete; import com.qqchen.deploy.backend.common.domain.Entity; import jakarta.persistence.Column; import jakarta.persistence.Table; @@ -10,6 +11,7 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @Table(name = "t_user") +@SoftDelete public class User extends Entity { @Column(unique = true, nullable = false) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 01f1502d..4d7cf820 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -25,6 +25,7 @@ logging: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE # \u6253\u5370\u6240\u6709\u6CE8\u518C\u7684\u63A5\u53E3\u8DEF\u5F84 org.hibernate.type.descriptor.sql.BasicBinder: TRACE # \u663E\u793ASQL\u53C2\u6570 org.hibernate.type.descriptor.sql: TRACE + com.qqchen.deploy.backend.common.utils.EntityPathResolver: DEBUG jwt: secret: 'thisIsAVeryVerySecretKeyForJwtTokenGenerationAndValidation123456789' expiration: 86400