Skip to content

首先是ActivityController

@GetMapping("/home/recommended")
    public Response<ActivitySearchResponse> getHomeRecommendedActivities(
            @RequestHeader(value = "Authorization", required = false) String token,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        log.info("首页推荐活动 - page: {}, size: {}", page, size);
        
        try {
            // 1. 【Trigger 层职责】提取用户ID(可选)
            String userId = token != null ? getCurrentUserId(token) : null;
            
            // 2. 【Domain 层职责】调用推荐服务(内部会根据用户画像构建 Criteria)
            ActivitySearchResult result = activityQueryApplicationService
                    .getHomeRecommendedActivities(userId, page, size);
            
            // 3. 【Trigger 层职责】协议转换:Domain 对象 → Response DTO
            ActivitySearchResponse response = buildSearchResponse(result);
            
            return Response.success(response);
            
        } catch (Exception e) {
            log.error("首页推荐失败", e);
            return Response.error("查询失败: " + e.getMessage());
        }
    }

再到防腐层IActivityQueryApplicationService

 /**
     * 首页推荐活动
     * <p>
     * 业务场景:根据用户画像(城市、年龄、性别)推荐活动
     * 
     * @param userId 用户ID
     * @param page 页码
     * @param size 每页大小
     * @return 推荐活动
     */
    ActivitySearchResult getHomeRecommendedActivities(String userId, int page, int size);

然后防腐实现ActivityQueryApplicationService

@Override
    public ActivitySearchResult getHomeRecommendedActivities(
            String userId,
            int page,
            int size) {

        log.info("查询首页推荐活动 - 用户ID: {}, 页码: {}, 大小: {}", userId, page, size);

        try {
            // 1. 获取用户信息
            UserAggregate user = userRepository.findByUserId(userId)
                    .orElseThrow(() -> new ActivityQueryException("用户不存在"));

            // 2. 构建推荐条件
            ActivitySearchCriteria criteria = ActivitySearchCriteria.builder()
                    .city(user.getCity())
                    .age(user.getAge())
                    .gender(user.getGender())
                    .hasAvailableSlots(true)
                    .minRating(BigDecimal.valueOf(4.0))
                    .minReviewCount(10)
                    .sortBy("popularity")  // 按热度排序
                    .build();

            // 3. 执行搜索
            ActivitySearchResult result = searchActivitiesAdvanced(criteria, page, size);

            log.info("查询到 {} 个推荐活动", result.getTotal());
            return result;

        } catch (Exception e) {
            log.error("查询首页推荐活动失败 - 用户ID: {}", userId, e);
            throw new ActivityQueryException("查询首页推荐活动失败", e);
        }
    }


  // ============ 高级搜索方法实现 ============

    @Override
    public ActivitySearchResult searchActivitiesAdvanced(
            ActivitySearchCriteria criteria,
            int page,
            int size) {

        log.info("高级搜索活动 - 条件: {}, 页码: {}, 大小: {}", criteria, page, size);

        try {
            // 参数校验
            if (criteria == null) {
                throw new ActivityQueryException("搜索条件不能为空");
            }

            // 调用 Repository
            ActivitySearchResult result = activityAggregateRepository.searchActivities(
                    criteria, page, size
            );

            log.info("高级搜索成功 - 找到 {} 个活动", result.getTotal());
            return result;

        } catch (Exception e) {
            log.error("高级搜索活动失败 - 条件: {}", criteria, e);
            throw new ActivityQueryException("高级搜索活动失败", e);
        }
    }

可以在这里看到聚合根的activityAggregateRepository.searchActivities(criteria, page, size);到了一个新的防腐层 IActivityAggregateRepository

 /**
     * 高级搜索活动(领域语言)
     * <p>
     * 业务含义:根据用户的搜索意图查找符合条件的活动
     * <p>
     * 设计原则:
     * - 使用 ActivitySearchCriteria 封装复杂查询条件
     * - 返回 Domain 层的 ActivitySearchResult,不依赖 Infrastructure 层类型
     * - 支持动态条件组合和分页排序
     * 
     * @param criteria 搜索条件
     * @param page 页码(从0开始)
     * @param size 每页大小
     * @return 搜索结果(包含活动列表和分页信息)
     */
    ActivitySearchResult searchActivities(ActivitySearchCriteria criteria, int page, int size);

然后就是这个防腐的实现:ActivityAggregateRepository

@Override
    public ActivitySearchResult searchActivities(
            ActivitySearchCriteria criteria, 
            int page, 
            int size) {
        
        log.debug("高级搜索活动 - 条件: {}, 页码: {}, 大小: {}", criteria, page, size);
        
        try {
            LocalDateTime now = LocalDateTime.now();
            
            // 1. 构建 Specification
            Specification<ActivityPO> spec = ActivityPoSpecs.bySearchCriteria(criteria, now);
            
            // 2. 构建排序
            Sort sort = ActivityPoSpecs.buildSort(criteria);
            
            // 3. 构建分页
            Pageable pageable = PageRequest.of(page, size, sort);
            
            // 4. 执行查询
            Page<ActivityPO> poPage = activityJpaRepository.findAll(spec, pageable);
            
            // 5. 转换为 Domain 对象
            List<ActivityAggregate> activities = poPage.getContent().stream()
                .map(activityConverter::poToAggregate)
                .toList();
            
            // 6. 构建结果
            ActivitySearchResult result = ActivitySearchResult.builder()
                .activities(activities)
                .total(poPage.getTotalElements())
                .page(page)
                .size(size)
                .build();
            
            log.debug("搜索完成 - 找到 {} 个活动,总数: {}", activities.size(), result.getTotal());
            return result;
            
        } catch (Exception e) {
            log.error("高级搜索活动失败 - 条件: {}", criteria, e);
            throw new ActivityRepositoryException("高级搜索活动失败", e);
        }
    }
}

我们这里就看下ActivityPoSpecs 构建查询参数:

package com.alisunxin.api.infrastructure.dao.specification;

import com.alisunxin.api.domain.activity.dto.query.ActivitySearchCriteria;
import com.alisunxin.api.infrastructure.dao.po.ActivityPO;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

import jakarta.persistence.criteria.Predicate;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * 活动 PO 查询 Specification 工具类
 * <p>
 * Infrastructure 层 - 用于构建数据库层面的复杂查询条件
 * <p>
 * 与 Domain 层的 ActivitySpecifications 区别:
 * - Domain 层: 操作 ActivityAggregate,内存过滤,纯业务逻辑
 * - Infrastructure 层: 操作 ActivityPO,数据库过滤,生成 SQL
 * 
 * @author alisunxin
 * @version 2.0
 */
public class ActivityPoSpecs {

    /**
     * 基础条件:未删除
     */
    public static Specification<ActivityPO> isNotDeleted() {
        return (root, query, cb) -> cb.equal(root.get("isDeleted"), false);
    }

    /**
     * 按状态查询
     */
    public static Specification<ActivityPO> hasStatus(String status) {
        return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status);
    }

    /**
     * 查询已发布的活动
     */
    public static Specification<ActivityPO> isPublished() {
        return (root, query, cb) -> cb.equal(root.get("status"), "PUBLISHED");
    }

    /**
     * 按审核状态查询
     */
    public static Specification<ActivityPO> hasApprovalStatus(String approvalStatus) {
        return (root, query, cb) -> approvalStatus == null ? null : cb.equal(root.get("approvalStatus"), approvalStatus);
    }

    /**
     * 查询待审核的活动
     */
    public static Specification<ActivityPO> isPendingApproval() {
        return (root, query, cb) -> cb.equal(root.get("approvalStatus"), "PENDING");
    }

    /**
     * 查询已审核通过的活动
     */
    public static Specification<ActivityPO> isApproved() {
        return (root, query, cb) -> cb.equal(root.get("approvalStatus"), "APPROVED");
    }

    /**
     * 查询进行中的活动
     */
    public static Specification<ActivityPO> isOngoing(LocalDateTime now) {
        return (root, query, cb) -> cb.and(
            cb.equal(root.get("status"), "PUBLISHED"),
            cb.lessThanOrEqualTo(root.get("startTime"), now),
            cb.greaterThanOrEqualTo(root.get("endTime"), now)
        );
    }

    /**
     * 查询即将开始的活动
     */
    public static Specification<ActivityPO> isUpcoming(LocalDateTime now, LocalDateTime futureTime) {
        return (root, query, cb) -> cb.and(
            cb.equal(root.get("status"), "PUBLISHED"),
            cb.greaterThan(root.get("startTime"), now),
            cb.lessThanOrEqualTo(root.get("startTime"), futureTime)
        );
    }

    /**
     * 查询已结束的活动
     */
    public static Specification<ActivityPO> isEnded(LocalDateTime now) {
        return (root, query, cb) -> cb.lessThan(root.get("endTime"), now);
    }

    /**
     * 查询可报名的活动
     */
    public static Specification<ActivityPO> isRegistrable(LocalDateTime now) {
        return (root, query, cb) -> cb.and(
            cb.equal(root.get("status"), "PUBLISHED"),
            cb.equal(root.get("approvalStatus"), "APPROVED"),
            cb.greaterThan(root.get("registrationDeadline"), now),
            cb.lessThan(root.get("currentParticipants"), root.get("maxParticipants"))
        );
    }

    /**
     * 按活动类型查询
     */
    public static Specification<ActivityPO> hasActivityType(String activityType) {
        return (root, query, cb) -> activityType == null ? null : cb.equal(root.get("activityType"), activityType);
    }

    /**
     * 按分类查询
     */
    public static Specification<ActivityPO> hasCategory(String category) {
        return (root, query, cb) -> category == null ? null : cb.equal(root.get("category"), category);
    }

    /**
     * 按省份查询
     */
    public static Specification<ActivityPO> inProvince(String province) {
        return (root, query, cb) -> province == null ? null : cb.equal(root.get("province"), province);
    }

    /**
     * 按城市查询
     */
    public static Specification<ActivityPO> inCity(String city) {
        return (root, query, cb) -> city == null ? null : cb.equal(root.get("city"), city);
    }

    /**
     * 按区域查询
     */
    public static Specification<ActivityPO> inDistrict(String district) {
        return (root, query, cb) -> district == null ? null : cb.equal(root.get("district"), district);
    }

    /**
     * 按详细地址模糊查询
     */
    public static Specification<ActivityPO> addressContains(String address) {
        return (root, query, cb) -> address == null ? null : cb.like(root.get("address"), "%" + address + "%");
    }

    /**
     * 按开始时间范围查询
     */
    public static Specification<ActivityPO> startTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
        return (root, query, cb) -> {
            if (startTime == null && endTime == null) return null;
            if (startTime == null) return cb.lessThanOrEqualTo(root.get("startTime"), endTime);
            if (endTime == null) return cb.greaterThanOrEqualTo(root.get("startTime"), startTime);
            return cb.between(root.get("startTime"), startTime, endTime);
        };
    }

    /**
     * 按结束时间范围查询
     */
    public static Specification<ActivityPO> endTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
        return (root, query, cb) -> {
            if (startTime == null && endTime == null) return null;
            if (startTime == null) return cb.lessThanOrEqualTo(root.get("endTime"), endTime);
            if (endTime == null) return cb.greaterThanOrEqualTo(root.get("endTime"), startTime);
            return cb.between(root.get("endTime"), startTime, endTime);
        };
    }

    /**
     * 按报名时间范围查询
     */
    public static Specification<ActivityPO> registrationTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
        return (root, query, cb) -> {
            if (startTime == null || endTime == null) return null;
            return cb.and(
                cb.lessThanOrEqualTo(root.get("registrationStartTime"), endTime),
                cb.greaterThanOrEqualTo(root.get("registrationDeadline"), startTime)
            );
        };
    }

    /**
     * 查询免费活动
     */
    public static Specification<ActivityPO> isFree() {
        return (root, query, cb) -> cb.equal(root.get("costAmount"), BigDecimal.ZERO);
    }

    /**
     * 按费用范围查询
     */
    public static Specification<ActivityPO> costBetween(BigDecimal minFee, BigDecimal maxFee) {
        return (root, query, cb) -> {
            if (minFee == null && maxFee == null) return null;
            if (minFee == null) return cb.lessThanOrEqualTo(root.get("costAmount"), maxFee);
            if (maxFee == null) return cb.greaterThanOrEqualTo(root.get("costAmount"), minFee);
            return cb.between(root.get("costAmount"), minFee, maxFee);
        };
    }

    /**
     * 按参与者数量范围查询
     */
    public static Specification<ActivityPO> participantCountBetween(Integer minCount, Integer maxCount) {
        return (root, query, cb) -> {
            if (minCount == null && maxCount == null) return null;
            if (minCount == null) return cb.lessThanOrEqualTo(root.get("currentParticipants"), maxCount);
            if (maxCount == null) return cb.greaterThanOrEqualTo(root.get("currentParticipants"), minCount);
            return cb.between(root.get("currentParticipants"), minCount, maxCount);
        };
    }

    /**
     * 查询还有名额的活动
     */
    public static Specification<ActivityPO> hasAvailableSlots() {
        return (root, query, cb) -> cb.lessThan(root.get("currentParticipants"), root.get("maxParticipants"));
    }

    /**
     * 查询已满员的活动
     */
    public static Specification<ActivityPO> isFull() {
        return (root, query, cb) -> cb.greaterThanOrEqualTo(root.get("currentParticipants"), root.get("maxParticipants"));
    }

    /**
     * 按年龄限制查询
     */
    public static Specification<ActivityPO> ageInRange(Integer age) {
        return (root, query, cb) -> {
            if (age == null) return null;
            List<Predicate> predicates = new ArrayList<>();
            // minAge 为 null 或 minAge <= age
            predicates.add(cb.or(
                cb.isNull(root.get("minAge")),
                cb.lessThanOrEqualTo(root.get("minAge"), age)
            ));
            // maxAge 为 null 或 maxAge >= age
            predicates.add(cb.or(
                cb.isNull(root.get("maxAge")),
                cb.greaterThanOrEqualTo(root.get("maxAge"), age)
            ));
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    /**
     * 按性别限制查询
     */
    public static Specification<ActivityPO> genderAllowed(String gender) {
        return (root, query, cb) -> {
            if (gender == null) return null;
            return cb.or(
                cb.isNull(root.get("genderRestriction")),
                cb.equal(root.get("genderRestriction"), "ALL"),
                cb.equal(root.get("genderRestriction"), gender)
            );
        };
    }

    /**
     * 按技能等级查询
     */
    public static Specification<ActivityPO> hasSkillLevel(String skillLevel) {
        return (root, query, cb) -> skillLevel == null ? null : cb.equal(root.get("skillLevel"), skillLevel);
    }

    /**
     * 查询适合初学者的活动
     * <p>
     * 支持:BEGINNER、ALL、或无技能要求(null)
     */
    public static Specification<ActivityPO> isBeginnerFriendly() {
        return (root, query, cb) -> cb.or(
            cb.equal(root.get("skillLevel"), "BEGINNER"),
            cb.equal(root.get("skillLevel"), "ALL"),
            cb.isNull(root.get("skillLevel"))
        );
    }

    /**
     * 按评分范围查询
     */
    public static Specification<ActivityPO> ratingBetween(BigDecimal minRating, BigDecimal maxRating) {
        return (root, query, cb) -> {
            if (minRating == null && maxRating == null) return null;
            if (minRating == null) return cb.lessThanOrEqualTo(root.get("rating"), maxRating);
            if (maxRating == null) return cb.greaterThanOrEqualTo(root.get("rating"), minRating);
            return cb.between(root.get("rating"), minRating, maxRating);
        };
    }

    /**
     * 查询高评分活动
     */
    public static Specification<ActivityPO> isHighlyRated(BigDecimal minRating, Integer minReviewCount) {
        return (root, query, cb) -> cb.and(
            cb.greaterThanOrEqualTo(root.get("rating"), minRating),
            cb.greaterThanOrEqualTo(root.get("reviewCount"), minReviewCount)
        );
    }

    /**
     * 按标签精确查询(防止部分匹配)
     * <p>
     * 支持逗号分隔的标签字符串:
     * - "户外" → 匹配 "户外"、"户外,亲子"、"亲子,户外,免费" 等
     * - 不会误匹配 "户外活动" 或 "免费户外"
     * <p>
     * 注意:如果标签存储在关联表中,建议使用 JOIN 查询
     * 
     * @param tag 标签
     * @return Specification
     */
    public static Specification<ActivityPO> hasTag(String tag) {
        if (tag == null || tag.isBlank()) {
            return null;
        }
        
        String normalizedTag = tag.trim();
        return (root, query, cb) -> cb.or(
            cb.equal(root.get("tags"), normalizedTag),                      // 唯一标签:"户外"
            cb.like(root.get("tags"), normalizedTag + ",%"),                // 开头:"户外,亲子"
            cb.like(root.get("tags"), "%," + normalizedTag),                // 结尾:"亲子,户外"
            cb.like(root.get("tags"), "%," + normalizedTag + ",%")          // 中间:"免费,户外,亲子"
        );
    }
    
    /**
     * 按标签模糊查询(兼容旧逻辑)
     * <p>
     * 已废弃:建议使用 {@link #hasTag(String)} 进行精确匹配
     * 
     * @param keyword 关键词
     * @return Specification
     * @deprecated 使用 {@link #hasTag(String)} 替代
     */
    @Deprecated
    public static Specification<ActivityPO> tagContains(String keyword) {
        return (root, query, cb) -> keyword == null ? null : cb.like(root.get("tags"), "%" + keyword + "%");
    }

    /**
     * 按创建者查询
     */
    public static Specification<ActivityPO> createdBy(String creatorId) {
        return (root, query, cb) -> creatorId == null ? null : cb.equal(root.get("creatorId"), creatorId);
    }

    /**
     * 按标题模糊查询
     */
    public static Specification<ActivityPO> titleContains(String title) {
        return (root, query, cb) -> title == null ? null : cb.like(root.get("title"), "%" + title + "%");
    }

    /**
     * 按描述模糊查询
     */
    public static Specification<ActivityPO> descriptionContains(String description) {
        return (root, query, cb) -> description == null ? null : cb.like(root.get("description"), "%" + description + "%");
    }

    /**
     * 组合查询:热门活动
     * 已发布 + 高评分 + 有名额
     */
    public static Specification<ActivityPO> isPopular(BigDecimal minRating, Integer minReviewCount) {
        return Specification.where(isNotDeleted())
            .and(isPublished())
            .and(isHighlyRated(minRating, minReviewCount))
            .and(hasAvailableSlots());
    }

    /**
     * 组合查询:推荐活动
     * 已发布 + 已审核 + 可报名 + 未满员
     */
    public static Specification<ActivityPO> isRecommended(LocalDateTime now) {
        return Specification.where(isNotDeleted())
            .and(isPublished())
            .and(isApproved())
            .and(isRegistrable(now));
    }

    /**
     * 组合查询:附近的活动
     * 按城市 + 已发布 + 进行中或即将开始
     */
    public static Specification<ActivityPO> isNearby(String city, LocalDateTime now, LocalDateTime futureTime) {
        return Specification.where(isNotDeleted())
            .and(inCity(city))
            .and(isPublished())
            .and(Specification.where(isOngoing(now)).or(isUpcoming(now, futureTime)));
    }

    /**
     * 组合查询:适合用户的活动
     * 按年龄、性别、技能等级筛选
     */
    public static Specification<ActivityPO> isSuitableForUser(Integer age, String gender, String skillLevel) {
        return Specification.where(isNotDeleted())
            .and(isPublished())
            .and(ageInRange(age))
            .and(genderAllowed(gender))
            .and(hasSkillLevel(skillLevel));
    }
    
    // ============ 排序支持 ============
    
    /**
     * 默认排序(按创建时间倒序)
     * <p>
     * 注意:仅在 query.getOrderList().isEmpty() 时生效,避免覆盖上层排序
     */
    public static Specification<ActivityPO> withDefaultOrder() {
        return (root, query, cb) -> {
            if (query != null && query.getOrderList().isEmpty()) {
                query.orderBy(cb.desc(root.get("createTime")));
            }
            return cb.conjunction(); // 不影响 WHERE 条件
        };
    }
    
    /**
     * 按热度排序(评分 + 评论数)
     */
    public static Specification<ActivityPO> orderByPopularity() {
        return (root, query, cb) -> {
            if (query != null && query.getOrderList().isEmpty()) {
                query.orderBy(
                    cb.desc(root.get("rating")),
                    cb.desc(root.get("reviewCount"))
                );
            }
            return cb.conjunction();
        };
    }
    
    /**
     * 按开始时间排序(最近的优先)
     */
    public static Specification<ActivityPO> orderByStartTime() {
        return (root, query, cb) -> {
            if (query != null && query.getOrderList().isEmpty()) {
                query.orderBy(cb.asc(root.get("startTime")));
            }
            return cb.conjunction();
        };
    }
    
    // ============ 高频组合查询 ============
    
    /**
     * 搜索活动(关键词 + 城市 + 可报名)
     * <p>
     * 适用场景:用户搜索页、活动列表筛选
     * 
     * @param keyword 关键词(搜索标题、描述、标签)
     * @param city 城市(可选)
     * @param now 当前时间
     * @return Specification
     */
    public static Specification<ActivityPO> searchActivities(
            String keyword, String city, LocalDateTime now) {
        
        Specification<ActivityPO> spec = Specification.where(isNotDeleted())
            .and(isPublished())
            .and(hasAvailableSlots());
        
        // 关键词搜索(标题 OR 描述 OR 标签)
        if (keyword != null && !keyword.isBlank()) {
            spec = spec.and(
                Specification.where(titleContains(keyword))
                    .or(descriptionContains(keyword))
                    .or(hasTag(keyword))
            );
        }
        
        // 城市筛选
        if (city != null && !city.isBlank()) {
            spec = spec.and(inCity(city));
        }
        
        return spec;
    }
    
    /**
     * 首页推荐活动(热门 + 附近 + 适合用户)
     * 
     * @param city 用户所在城市
     * @param age 用户年龄
     * @param gender 用户性别
     * @param now 当前时间
     * @param minRating 最低评分
     * @param minReviewCount 最低评论数
     * @return Specification
     */
    public static Specification<ActivityPO> homeRecommended(
            String city, Integer age, String gender, LocalDateTime now,
            BigDecimal minRating, Integer minReviewCount) {
        
        return Specification.where(isNotDeleted())
            .and(isPublished())
            .and(isApproved())
            .and(isRegistrable(now))
            .and(inCity(city))
            .and(ageInRange(age))
            .and(genderAllowed(gender))
            .and(isHighlyRated(minRating, minReviewCount));
    }
    
    /**
     * 我的活动(按创建者 + 状态分类)
     * 
     * @param creatorId 创建者ID
     * @param status 活动状态(可选)
     * @return Specification
     */
    public static Specification<ActivityPO> myActivities(String creatorId, String status) {
        Specification<ActivityPO> spec = Specification.where(isNotDeleted())
            .and(createdBy(creatorId));
        
        if (status != null && !status.isBlank()) {
            spec = spec.and(hasStatus(status));
        }
        
        return spec;
    }
    
    /**
     * 即将开始的活动(未来N小时内)
     * 
     * @param now 当前时间
     * @param hours 未来小时数
     * @return Specification
     */
    public static Specification<ActivityPO> upcomingWithinHours(LocalDateTime now, int hours) {
        LocalDateTime futureTime = now.plusHours(hours);
        return Specification.where(isNotDeleted())
            .and(isPublished())
            .and(isUpcoming(now, futureTime));
    }
    
    // ============ 动态搜索条件构建 ============
    
    /**
     * 根据高级搜索条件动态构建 Specification
     * <p>
     * 设计原则:
     * - 所有条件都是可选的(null 表示不筛选)
     * - 使用 AND 连接所有条件
     * - 关键词使用 OR 连接(标题 OR 描述 OR 标签)
     * 
     * @param criteria 搜索条件
     * @param now 当前时间(用于时间相关判断)
     * @return Specification
     */
    public static Specification<ActivityPO> bySearchCriteria(
            ActivitySearchCriteria criteria, 
            LocalDateTime now) {
        
        // 基础条件:未删除 + 已发布 + 已审核
        Specification<ActivityPO> spec = Specification.where(isNotDeleted())
            .and(hasStatus(criteria.getStatus()))
            .and(hasApprovalStatus(criteria.getApprovalStatus()));
        
        // ============ 关键词搜索(标题 OR 描述 OR 标签)============
        if (criteria.getKeyword() != null && !criteria.getKeyword().isBlank()) {
            String keyword = criteria.getKeyword().trim();
            Specification<ActivityPO> keywordSpec = Specification
                .where(titleContains(keyword))
                .or(descriptionContains(keyword))
                .or(hasTag(keyword));
            spec = spec.and(keywordSpec);
        }
        
        // ============ 地理位置 ============
        if (criteria.getProvince() != null && !criteria.getProvince().isBlank()) {
            spec = spec.and(inProvince(criteria.getProvince()));
        }
        if (criteria.getCity() != null && !criteria.getCity().isBlank()) {
            spec = spec.and(inCity(criteria.getCity()));
        }
        if (criteria.getDistrict() != null && !criteria.getDistrict().isBlank()) {
            spec = spec.and(inDistrict(criteria.getDistrict()));
        }
        
        // ============ 活动分类 ============
        if (criteria.getActivityType() != null && !criteria.getActivityType().isBlank()) {
            spec = spec.and(hasActivityType(criteria.getActivityType()));
        }
        if (criteria.getCategory() != null && !criteria.getCategory().isBlank()) {
            spec = spec.and(hasCategory(criteria.getCategory()));
        }
        if (criteria.getTag() != null && !criteria.getTag().isBlank()) {
            spec = spec.and(hasTag(criteria.getTag()));
        }
        
        // ============ 时间范围 ============
        if (criteria.getStartTimeFrom() != null || criteria.getStartTimeTo() != null) {
            spec = spec.and(startTimeBetween(
                criteria.getStartTimeFrom(), 
                criteria.getStartTimeTo()
            ));
        }
        
        // ============ 费用相关 ============
        if (Boolean.TRUE.equals(criteria.getOnlyFree())) {
            spec = spec.and(isFree());
        } else if (criteria.getMaxCost() != null) {
            spec = spec.and(costBetween(null, criteria.getMaxCost()));
        }
        
        // ============ 名额相关 ============
        if (Boolean.TRUE.equals(criteria.getHasAvailableSlots())) {
            spec = spec.and(hasAvailableSlots());
        }
        
        // ============ 用户适配条件 ============
        if (criteria.getAge() != null) {
            spec = spec.and(ageInRange(criteria.getAge()));
        }
        if (criteria.getGender() != null && !criteria.getGender().isBlank()) {
            spec = spec.and(genderAllowed(criteria.getGender()));
        }
        if (criteria.getSkillLevel() != null && !criteria.getSkillLevel().isBlank()) {
            spec = spec.and(hasSkillLevel(criteria.getSkillLevel()));
        }
        
        // ============ 质量筛选 ============
        if (criteria.getMinRating() != null) {
            spec = spec.and((root, query, cb) -> 
                cb.greaterThanOrEqualTo(root.get("rating"), criteria.getMinRating())
            );
        }
        if (criteria.getMinReviewCount() != null) {
            spec = spec.and((root, query, cb) -> 
                cb.greaterThanOrEqualTo(root.get("reviewCount"), criteria.getMinReviewCount())
            );
        }
        
        return spec;
    }
    
    /**
     * 根据 SearchCriteria 构建排序
     * <p>
     * 支持的排序字段:
     * - createTime: 创建时间
     * - startTime: 开始时间
     * - rating: 评分
     * - popularity: 热度(评分 + 评论数)
     * 
     * @param criteria 搜索条件
     * @return Sort
     */
    public static Sort buildSort(ActivitySearchCriteria criteria) {
        String sortBy = criteria.getSortBy() != null ? criteria.getSortBy() : "createTime";
        Sort.Direction direction = "ASC".equalsIgnoreCase(criteria.getSortDirection()) 
            ? Sort.Direction.ASC 
            : Sort.Direction.DESC;
        
        // 特殊处理:热度排序(多字段)
        if ("popularity".equalsIgnoreCase(sortBy)) {
            return Sort.by(direction, "rating", "reviewCount");
        }
        
        return Sort.by(direction, sortBy);
    }
}

最后就是 Page<ActivityPO> poPage = activityJpaRepository.findAll(spec, pageable);

package org.springframework.data.jpa.repository;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.repository.query.FluentQuery;

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(Specification<T> spec);

    List<T> findAll(Specification<T> spec);

    Page<T> findAll(Specification<T> spec, Pageable pageable);

    List<T> findAll(Specification<T> spec, Sort sort);

    long count(Specification<T> spec);

    boolean exists(Specification<T> spec);

    long delete(Specification<T> spec);

    <S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}

Powered by VitePress