diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilter.java b/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilter.java index a522d23..729859c 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilter.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilter.java @@ -86,4 +86,22 @@ public class OpportunitySavedViewFilter { case IS_NOT_EMPTY -> wrapper.apply("(" + column + " IS NOT NULL AND " + column + " <> '')"); } } + + /** + * 自定义视图排序翻译(票 14 定义 2:排序字段 + 方向,排序字段同属字段池)。 + *

返回可直接拼入 {@code ORDER BY} 的列片段(如 {@code "owner_user_id DESC"}); + * 字段不在字段池白名单 → {@code null}(调用方回落默认排序)。方向只认 asc/desc + * (大小写无关),其余 / 空 → 默认 DESC。列名走白名单常量,方向只产 ASC/DESC 字面量,无注入面。

+ */ + public String orderByClause(String sortField, String sortDirection) { + if (StrUtil.isBlank(sortField)) { + return null; + } + String column = FIELD_COLUMN.get(sortField); + if (column == null) { + return null; // 排序字段不在字段池 → 回落默认排序 + } + boolean asc = "asc".equalsIgnoreCase(StrUtil.trimToEmpty(sortDirection)); + return column + (asc ? " ASC" : " DESC"); + } } diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java b/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java index a56d4e2..ba80576 100644 --- a/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java +++ b/crm-opportunity/src/main/java/com/crm/opportunity/query/impl/OpportunityViewQueryImpl.java @@ -30,7 +30,6 @@ import com.crm.opportunity.mapper.OpportunitySchemeCardMapper; import com.crm.opportunity.mapper.OpportunityStageHistoryMapper; import com.crm.opportunity.mapper.OpportunityViewLogMapper; import com.crm.opportunity.query.OpportunityViewQuery; -import com.crm.preference.domain.dto.SavedView; import com.crm.preference.service.SavedViewService; import com.crm.rule.domain.dto.SysRegionDTO; import com.crm.rule.domain.entity.OpportunityStageNode; @@ -101,8 +100,13 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyViewFilters(wrapper, param, currentUserId); - applySavedView(wrapper, param, currentUserId); - wrapper.orderByDesc(Opportunity::getCreateTime); + String savedViewOrderBy = applySavedView(wrapper, param, currentUserId); + if (StrUtil.isNotBlank(savedViewOrderBy)) { + // 自定义视图排序(票 14 定义 2)替代默认 createTime 倒序 + wrapper.last("ORDER BY " + savedViewOrderBy); + } else { + wrapper.orderByDesc(Opportunity::getCreateTime); + } Page page = opportunityMapper.selectPage(PageConverter.toMpPage(param), wrapper); PageResult result = new PageResult<>(page).convert(OpportunityDTO::fromEntity); @@ -131,6 +135,7 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(Opportunity::getId, orderedIds); applyCommonFilters(wrapper, param); + // RECENT 为时间序(view_log 倒序),自定义视图仅叠加条件过滤,不接排序(时间序优先) applySavedView(wrapper, param, currentUserId); wrapper.last("order by field(id," + orderedIds.stream() .map(String::valueOf).collect(Collectors.joining(",")) + ")"); @@ -449,21 +454,26 @@ public class OpportunityViewQueryImpl implements OpportunityViewQuery { } /** - * 自定义视图条件叠加(票 14):{@code param.savedViewId} 非空时,读当前用户 - * {@code opportunity.sales} scope 下该视图的检索条件,翻译后 AND 叠在当前数据集之上。 - *

视图不存在(已删 / 非本人)→ 无额外条件(等价写回未选自定义视图)。 - * 排序(sortField/sortDirection)本期不入库级排序(列表默认按 createTime),留后续实现。

+ * 自定义视图叠加(票 14):{@code param.savedViewId} 非空时,读当前用户 + * {@code opportunity.sales} scope 下该视图,把检索条件翻译后 AND 叠在当前数据集之上, + * 并返回排序列片段供调用方替代默认排序。 + *

视图不存在(已删 / 非本人)或 savedViewId 空 → 返回 {@code null}(无额外条件且回落默认排序)。

+ * + * @return 自定义视图排序列片段(如 {@code "owner_user_id DESC"});无排序 / 排序字段不在字段池 → {@code null} */ - private void applySavedView(LambdaQueryWrapper wrapper, - OpportunityPageParam param, Long currentUserId) { + private String applySavedView(LambdaQueryWrapper wrapper, + OpportunityPageParam param, Long currentUserId) { if (StrUtil.isBlank(param.getSavedViewId())) { - return; + return null; } - savedViewService.list(currentUserId, SAVED_VIEW_SCOPE).stream() + return savedViewService.list(currentUserId, SAVED_VIEW_SCOPE).stream() .filter(v -> param.getSavedViewId().equals(v.viewId())) .findFirst() - .map(SavedView::conditions) - .ifPresent(conditions -> savedViewFilter.apply(wrapper, conditions)); + .map(v -> { + savedViewFilter.apply(wrapper, v.conditions()); + return savedViewFilter.orderByClause(v.sortField(), v.sortDirection()); + }) + .orElse(null); } private Long getCurrentUserId() { diff --git a/crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilterTest.java b/crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilterTest.java index 25123b7..19a5da7 100644 --- a/crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilterTest.java +++ b/crm-opportunity/src/test/java/com/crm/opportunity/query/impl/OpportunitySavedViewFilterTest.java @@ -125,6 +125,30 @@ class OpportunitySavedViewFilterTest { assertThat(sql).contains("AND"); } + // ==================== 排序翻译 ==================== + + @Test + @DisplayName("orderByClause:字段在池 + asc → 列 ASC") + void orderBy_ascInPool() { + assertThat(filter.orderByClause("ownerUserId", "asc")).isEqualTo("owner_user_id ASC"); + } + + @Test + @DisplayName("orderByClause:desc / 方向空 / 非法方向 → 列 DESC(默认)") + void orderBy_descDefault() { + assertThat(filter.orderByClause("currentStageId", "desc")).isEqualTo("current_stage_id DESC"); + assertThat(filter.orderByClause("currentStageId", null)).isEqualTo("current_stage_id DESC"); + assertThat(filter.orderByClause("currentStageId", "nonsense")).isEqualTo("current_stage_id DESC"); + } + + @Test + @DisplayName("orderByClause:排序字段不在字段池 / 空 → null(回落默认排序)") + void orderBy_unknownOrBlank_null() { + assertThat(filter.orderByClause("secretColumn", "asc")).isNull(); + assertThat(filter.orderByClause(null, "asc")).isNull(); + assertThat(filter.orderByClause(" ", "asc")).isNull(); + } + // ==================== 操作符枚举 ==================== @Test