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