From 911511d8f55082d761986feb5a280925f416ae2b Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 14:50:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(opportunity):=20=E7=A5=A814=20=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E8=A7=86=E5=9B=BE=E6=8E=92=E5=BA=8F=E6=8E=A5?= =?UTF-8?q?=E7=BA=BF=EF=BC=88=E8=A1=A5=E5=BA=95=E7=A8=BF=E5=AE=9A=E4=B9=89?= =?UTF-8?q?2=E6=BC=8F=E9=A1=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 底稿 issues/14 定义2 把「排序字段+方向」列为自定义视图组成部分,实体/DTO 已存 sortField/sortDirection,但上一版查询未接排序(列表恒按 createTime)。补齐: - OpportunitySavedViewFilter.orderByClause(field,direction):字段走同一字段池 白名单→主表列名,方向只认 asc/desc(大小写无关,其余/空→DESC);字段不在池/空→null - ViewQueryImpl.applySavedView 返回排序片段,pageOpportunities 有则 wrapper.last(ORDER BY ...) 替代默认 createTime 倒序,无则回落默认; RECENT 路径为时间序不接排序(时间序优先) - 移除不再直接引用的 SavedView import - 补 3 排序单测(asc/desc默认/未知字段回落) mvn -pl crm-opportunity -am test 147 全绿。 --- .../impl/OpportunitySavedViewFilter.java | 18 ++++++++++ .../query/impl/OpportunityViewQueryImpl.java | 36 ++++++++++++------- .../impl/OpportunitySavedViewFilterTest.java | 24 +++++++++++++ 3 files changed, 65 insertions(+), 13 deletions(-) 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