首先是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);
}