From a55668ec33bf7ac938200b9e5d2e6e7485885d75 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 31 Jul 2026 11:25:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E5=8D=95=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E8=A1=A8=E5=A4=B4=E7=AD=9B=E9=80=89=E3=80=81=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=B8=8E=E9=BB=98=E8=AE=A490=E5=A4=A9?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E7=AA=97=E6=8A=A4=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持 filters[]/whitelist 排序,导出行数与批量 IN 上限,无时间窗时兜底近90天。 Co-authored-by: Cursor --- .../CustomerAccountFlowPaginatedInput.cs | 20 + .../Dtos/Customers/CustomerPaginatedInput.cs | 10 + .../Dtos/Orders/OrderExportInput.cs | 15 + .../Dtos/Orders/OrderListFieldFilterDto.cs | 36 ++ .../Dtos/Orders/OrderPaginatedInput.cs | 21 + .../Dtos/Shared/ListFieldFilterDto.cs | 33 ++ .../Customers/CustomerAccountFlowService.cs | 57 +- .../Services/Customers/CustomerService.cs | 36 +- .../Services/Orders/OrderService.Monitor.cs | 3 + .../Services/Orders/OrderService.cs | 270 +++++++++- .../OrdersAggregate/OrderListFieldFilter.cs | 17 + .../OrdersAggregate/OrderListFilter.cs | 15 + .../Orders/OrderRepository.cs | 501 +++++++++++++++++- 13 files changed, 998 insertions(+), 36 deletions(-) create mode 100644 src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs create mode 100644 src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs create mode 100644 src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs diff --git a/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs index 50198ad..11e5a95 100644 --- a/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs +++ b/src/Oms.Application/Dtos/Customers/CustomerAccountFlowPaginatedInput.cs @@ -24,4 +24,24 @@ public class CustomerAccountFlowPaginatedInput public DateTime? StartTime { get; set; } public DateTime? EndTime { get; set; } + + /// + /// 变动金额下限(含)。 + /// + public decimal? AmountMin { get; set; } + + /// + /// 变动金额上限(含)。 + /// + public decimal? AmountMax { get; set; } + + /// + /// 排序字段:createTime / amount。 + /// + public string? SortField { get; set; } + + /// + /// asc / desc。 + /// + public string? SortOrder { get; set; } } diff --git a/src/Oms.Application/Dtos/Customers/CustomerPaginatedInput.cs b/src/Oms.Application/Dtos/Customers/CustomerPaginatedInput.cs index c361d92..a765c86 100644 --- a/src/Oms.Application/Dtos/Customers/CustomerPaginatedInput.cs +++ b/src/Oms.Application/Dtos/Customers/CustomerPaginatedInput.cs @@ -41,4 +41,14 @@ public class CustomerPaginatedInput /// 所属顾问筛选。 /// public string? CustomerService { get; set; } + + /// + /// 排序字段:createTime / updateTime / customerCode / customerName / balance。 + /// + public string? SortField { get; set; } + + /// + /// asc / desc。 + /// + public string? SortOrder { get; set; } } diff --git a/src/Oms.Application/Dtos/Orders/OrderExportInput.cs b/src/Oms.Application/Dtos/Orders/OrderExportInput.cs index 6d8e8e8..1378635 100644 --- a/src/Oms.Application/Dtos/Orders/OrderExportInput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderExportInput.cs @@ -34,4 +34,19 @@ public class OrderExportInput /// [Newtonsoft.Json.JsonProperty("createTimeEnd")] public DateTime? CreateTimeEnd { get; set; } + + /// + /// 表头字段级筛选(白名单)。 + /// + [Newtonsoft.Json.JsonProperty("filters")] + [global::System.Text.Json.Serialization.JsonPropertyName("filters")] + public List? Filters { get; set; } + + [Newtonsoft.Json.JsonProperty("sortField")] + [global::System.Text.Json.Serialization.JsonPropertyName("sortField")] + public string? SortField { get; set; } + + [Newtonsoft.Json.JsonProperty("sortOrder")] + [global::System.Text.Json.Serialization.JsonPropertyName("sortOrder")] + public string? SortOrder { get; set; } } diff --git a/src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs b/src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs new file mode 100644 index 0000000..d1f5f0f --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs @@ -0,0 +1,36 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 订单列表字段级筛选(表头 filters[],白名单字段 + DataPerm 风格算子)。 +/// +public class OrderListFieldFilterDto +{ + /// + /// 字段白名单见 OrderService.NormalizeOrderFieldFilters。 + /// + [Newtonsoft.Json.JsonProperty("field")] + [global::System.Text.Json.Serialization.JsonPropertyName("field")] + public string Field { get; set; } = string.Empty; + + /// + /// like / eq / ne / in / notIn / gt / gte / lt / lte / between / empty / isNull / isNotNull。 + /// + [Newtonsoft.Json.JsonProperty("op")] + [global::System.Text.Json.Serialization.JsonPropertyName("op")] + public string Op { get; set; } = "like"; + + [Newtonsoft.Json.JsonProperty("value")] + [global::System.Text.Json.Serialization.JsonPropertyName("value")] + public string? Value { get; set; } + + [Newtonsoft.Json.JsonProperty("value2")] + [global::System.Text.Json.Serialization.JsonPropertyName("value2")] + public string? Value2 { get; set; } + + /// + /// IN / NOT IN 多值(优先于 value 逗号拆分)。 + /// + [Newtonsoft.Json.JsonProperty("values")] + [global::System.Text.Json.Serialization.JsonPropertyName("values")] + public List? Values { get; set; } +} diff --git a/src/Oms.Application/Dtos/Orders/OrderPaginatedInput.cs b/src/Oms.Application/Dtos/Orders/OrderPaginatedInput.cs index 90a4787..a33c92b 100644 --- a/src/Oms.Application/Dtos/Orders/OrderPaginatedInput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderPaginatedInput.cs @@ -62,4 +62,25 @@ public class OrderPaginatedInput /// [Newtonsoft.Json.JsonProperty("createTimeEnd")] public DateTime? CreateTimeEnd { get; set; } + + /// + /// 表头字段级筛选(白名单);与 keyword 并存时 AND。 + /// + [Newtonsoft.Json.JsonProperty("filters")] + [global::System.Text.Json.Serialization.JsonPropertyName("filters")] + public List? Filters { get; set; } + + /// + /// 排序字段白名单:createTime / updateTime / serviceNo / clientNo / trackNo / payableAmount / payTime / shipTime / receiveTime / signTime。 + /// + [Newtonsoft.Json.JsonProperty("sortField")] + [global::System.Text.Json.Serialization.JsonPropertyName("sortField")] + public string? SortField { get; set; } + + /// + /// asc / desc。 + /// + [Newtonsoft.Json.JsonProperty("sortOrder")] + [global::System.Text.Json.Serialization.JsonPropertyName("sortOrder")] + public string? SortOrder { get; set; } } diff --git a/src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs b/src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs new file mode 100644 index 0000000..89e3b9e --- /dev/null +++ b/src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs @@ -0,0 +1,33 @@ +namespace Oms.Application.Dtos.Shared; + +/// +/// 列表字段级筛选(表头 filters[],白名单字段由各接口自行校验)。 +/// +public class ListFieldFilterDto +{ + [Newtonsoft.Json.JsonProperty("field")] + [global::System.Text.Json.Serialization.JsonPropertyName("field")] + public string Field { get; set; } = string.Empty; + + /// + /// like / eq / ne / in / notIn / gt / gte / lt / lte / between / empty / isNull / isNotNull。 + /// + [Newtonsoft.Json.JsonProperty("op")] + [global::System.Text.Json.Serialization.JsonPropertyName("op")] + public string Op { get; set; } = "like"; + + [Newtonsoft.Json.JsonProperty("value")] + [global::System.Text.Json.Serialization.JsonPropertyName("value")] + public string? Value { get; set; } + + [Newtonsoft.Json.JsonProperty("value2")] + [global::System.Text.Json.Serialization.JsonPropertyName("value2")] + public string? Value2 { get; set; } + + /// + /// IN / NOT IN 多值(优先于 value 逗号拆分)。 + /// + [Newtonsoft.Json.JsonProperty("values")] + [global::System.Text.Json.Serialization.JsonPropertyName("values")] + public List? Values { get; set; } +} diff --git a/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs b/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs index 1ae82e2..319b263 100644 --- a/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs +++ b/src/Oms.Application/Services/Customers/CustomerAccountFlowService.cs @@ -41,11 +41,7 @@ public class CustomerAccountFlowService : ApplicationService predicate, pageIndex, pageSize, - new Dictionary - { - [nameof(CustomerAccountFlow.CreateTime)] = false, - [nameof(CustomerAccountFlow.Id)] = false - }); + BuildAccountFlowSortDictionary(input)); var total = await FlowRepository.CountAsync(predicate); var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray(); @@ -94,27 +90,48 @@ public class CustomerAccountFlowService : ApplicationService && !string.Equals(bizType, "all", StringComparison.OrdinalIgnoreCase); var hasCustomerFilter = customerId.HasValue; var customerIdValue = customerId.GetValueOrDefault(); - - if (input.StartTime.HasValue && input.EndTime.HasValue) - { - var start = input.StartTime.Value; - var end = input.EndTime.Value; - return flow => - (!hasCustomerFilter || flow.CustomerId == customerIdValue) && - (!hasBizType || flow.BizType == bizType) && - (!hasKeyword || - flow.SerialNumber.Contains(keyword!) || - (flow.BizNo ?? string.Empty).Contains(keyword!)) && - flow.CreateTime >= start && - flow.CreateTime <= end; - } + var hasStart = input.StartTime.HasValue; + var hasEnd = input.EndTime.HasValue; + var start = input.StartTime.GetValueOrDefault(); + var end = input.EndTime.GetValueOrDefault(); + var hasAmountMin = input.AmountMin.HasValue; + var hasAmountMax = input.AmountMax.HasValue; + var amountMin = input.AmountMin.GetValueOrDefault(); + var amountMax = input.AmountMax.GetValueOrDefault(); return flow => (!hasCustomerFilter || flow.CustomerId == customerIdValue) && (!hasBizType || flow.BizType == bizType) && (!hasKeyword || flow.SerialNumber.Contains(keyword!) || - (flow.BizNo ?? string.Empty).Contains(keyword!)); + (flow.BizNo ?? string.Empty).Contains(keyword!)) && + (!hasStart || flow.CreateTime >= start) && + (!hasEnd || flow.CreateTime <= end) && + (!hasAmountMin || flow.Amount >= amountMin) && + (!hasAmountMax || flow.Amount <= amountMax); + } + + private static Dictionary BuildAccountFlowSortDictionary( + CustomerAccountFlowPaginatedInput input) + { + var field = input.SortField?.Trim(); + var asc = string.Equals(input.SortOrder, "asc", StringComparison.OrdinalIgnoreCase); + if (string.Equals(field, "amount", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary + { + [nameof(CustomerAccountFlow.Amount)] = asc, + [nameof(CustomerAccountFlow.Id)] = false + }; + } + + return new Dictionary + { + [nameof(CustomerAccountFlow.CreateTime)] = string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase) + ? asc + : false, + [nameof(CustomerAccountFlow.Id)] = false + }; } private static string GetBizTypeName(string bizType) diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs index 5bb0d7c..07ddc94 100644 --- a/src/Oms.Application/Services/Customers/CustomerService.cs +++ b/src/Oms.Application/Services/Customers/CustomerService.cs @@ -66,7 +66,7 @@ public class CustomerService : ApplicationService predicate, pageIndex, pageSize, - new Dictionary { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false }); + BuildCustomerSortDictionary(input)); var totalCount = await CustomerRepository.CountAsync(predicate); var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync( @@ -424,6 +424,40 @@ public class CustomerService : ApplicationService (string.IsNullOrWhiteSpace(customerService) || customer.CustomerService == customerService); } + private static Dictionary BuildCustomerSortDictionary(CustomerPaginatedInput input) + { + var field = input.SortField?.Trim(); + var asc = string.Equals(input.SortOrder, "asc", StringComparison.OrdinalIgnoreCase); + // Dictionary value: true=asc, false=desc(与仓库约定一致) + if (string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary { [nameof(Customer.CreateTime)] = asc, [nameof(Customer.Id)] = false }; + } + + if (string.Equals(field, "customerCode", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary { [nameof(Customer.CustomerCode)] = asc, [nameof(Customer.Id)] = false }; + } + + if (string.Equals(field, "customerName", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary { [nameof(Customer.CustomerName)] = asc, [nameof(Customer.Id)] = false }; + } + + if (string.Equals(field, "balance", StringComparison.OrdinalIgnoreCase) + || string.Equals(field, "accountBalance", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary { [nameof(Customer.Balance)] = asc, [nameof(Customer.Id)] = false }; + } + + if (string.Equals(field, "updateTime", StringComparison.OrdinalIgnoreCase)) + { + return new Dictionary { [nameof(Customer.UpdateTime)] = asc, [nameof(Customer.Id)] = false }; + } + + return new Dictionary { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false }; + } + private async Task GetCustomerOrThrowAsync(int id) { var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id); diff --git a/src/Oms.Application/Services/Orders/OrderService.Monitor.cs b/src/Oms.Application/Services/Orders/OrderService.Monitor.cs index dcad38a..ac443f2 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Monitor.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Monitor.cs @@ -282,6 +282,9 @@ public partial class OrderService CreateTimeStart = start, CreateTimeEnd = filter.CreateTimeEnd, CustomerId = filter.CustomerId, + FieldFilters = filter.FieldFilters, + SortField = filter.SortField, + SortOrder = filter.SortOrder, ScopeOrganizationIds = filter.ScopeOrganizationIds, ScopeSecretLevels = filter.ScopeSecretLevels, ScopeDataScope = filter.ScopeDataScope, diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index 14f753e..01e3fb2 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -68,9 +68,14 @@ public partial class OrderService : ApplicationService /// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。 /// [HttpGet("/order/status-counts")] - public async Task GetStatusCountsAsync() + public async Task GetStatusCountsAsync( + [FromQuery] DateTime? createTimeStart = null, + [FromQuery] DateTime? createTimeEnd = null) { var filter = await BuildScopedListFilterAsync(); + filter.CreateTimeStart = createTimeStart; + filter.CreateTimeEnd = createTimeEnd; + EnsureOrderListTimeWindow(filter); var rows = await OrderRepository.GetStatusCountsByFilterAsync(filter); var counts = rows.ToDictionary( x => x.Status, @@ -316,7 +321,14 @@ public partial class OrderService : ApplicationService null); var filter = BuildListFilter(input, customerId); await ApplyInternalOrderDataScopeAsync(filter); - var orders = await OrderRepository.GetFilteredListAsync(filter); + + const int maxExportRows = 10000; + var (orders, total) = await OrderRepository.GetFilteredPagedListAsync(1, maxExportRows, filter); + if (total > maxExportRows) + { + throw new BusinessException($"导出结果超过 {maxExportRows} 条(当前 {total}),请缩小筛选条件后再导出"); + } + var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx"; var directory = GetOrderExportDirectory(); Directory.CreateDirectory(directory); @@ -507,28 +519,266 @@ public partial class OrderService : ApplicationService }; } - private static OrderListFilter BuildListFilter(OrderPaginatedInput input, int? customerId) => - new() + private static OrderListFilter BuildListFilter(OrderPaginatedInput input, int? customerId) + { + var filter = new OrderListFilter { Status = input.Status, HandleStatus = input.HandleStatus, OrderSource = input.OrderSource, - Keyword = input.Keyword, + Keyword = NormalizeOrderKeyword(input.Keyword), CreateTimeStart = input.CreateTimeStart, CreateTimeEnd = input.CreateTimeEnd, - CustomerId = customerId + CustomerId = customerId, + FieldFilters = NormalizeOrderFieldFilters(input.Filters), + SortField = NormalizeOrderSortField(input.SortField), + SortOrder = NormalizeOrderSortOrder(input.SortOrder) }; + EnsureOrderListTimeWindow(filter); + return filter; + } - private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId) => - new() + private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId) + { + var filter = new OrderListFilter { Status = input.Status, OrderSource = input.OrderSource, - Keyword = input.Keyword, + Keyword = NormalizeOrderKeyword(input.Keyword), CreateTimeStart = input.CreateTimeStart, CreateTimeEnd = input.CreateTimeEnd, - CustomerId = customerId + CustomerId = customerId, + FieldFilters = NormalizeOrderFieldFilters(input.Filters), + SortField = NormalizeOrderSortField(input.SortField), + SortOrder = NormalizeOrderSortOrder(input.SortOrder) }; + EnsureOrderListTimeWindow(filter); + return filter; + } + + /// 订单列表默认时间窗(天)。无精确单号且未传时间时兜底,防全表扫。 + private const int OrderListDefaultDays = 90; + + /// + /// 无创建时间且非「精确单号查询」时,自动限制近 N 天。 + /// + private static void EnsureOrderListTimeWindow(OrderListFilter filter) + { + if (filter.CreateTimeStart.HasValue || filter.CreateTimeEnd.HasValue) + { + return; + } + + if (HasNarrowOrderLookup(filter)) + { + return; + } + + filter.CreateTimeStart = DateTime.Now.Date.AddDays(-(OrderListDefaultDays - 1)); + } + + private static bool HasNarrowOrderLookup(OrderListFilter filter) + { + if (!string.IsNullOrWhiteSpace(filter.Keyword)) + { + return true; + } + + if (filter.FieldFilters == null || filter.FieldFilters.Count == 0) + { + return false; + } + + var numberFields = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "serviceNo", "clientNo", "trackNo", "downstreamServiceNo" + }; + + return filter.FieldFilters.Any(f => + numberFields.Contains(f.Field) + && (f.Op is "eq" or "in" or "like") + && (!string.IsNullOrWhiteSpace(f.Value) || (f.Values?.Count ?? 0) > 0)); + } + + private static readonly HashSet OrderFieldFilterWhitelist = new(StringComparer.OrdinalIgnoreCase) + { + "serviceNo", + "clientNo", + "trackNo", + "downstreamServiceNo", + "createTime", + "orderStatus", + "productCode", + "channelCode", + "orderSource", + "payableAmount", + "isPaid", + "predeclareFailReason", + "allocationPendingReason", + "payTime", + "shipTime", + "receiveTime", + "signTime" + }; + + private static readonly HashSet OrderFieldFilterOps = new(StringComparer.OrdinalIgnoreCase) + { + "like", "eq", "ne", "in", "notIn", "gt", "gte", "lt", "lte", "between", + "empty", "isNull", "isNotNull" + }; + + private static readonly HashSet OrderSortFieldWhitelist = new(StringComparer.OrdinalIgnoreCase) + { + "createTime", + "updateTime", + "serviceNo", + "clientNo", + "trackNo", + "payableAmount", + "payTime", + "shipTime", + "receiveTime", + "signTime" + }; + + /// 单次列表最多接受的 filters 条数。 + private const int MaxOrderFieldFilters = 20; + + /// IN / NOT IN 单字段最多值数量(防几万单号打爆 SQL/参数包)。 + private const int MaxOrderInValues = 500; + + /// 单值最大长度(单号/文本)。 + private const int MaxOrderFilterValueLength = 128; + + /// keyword 最大长度。 + private const int MaxOrderKeywordLength = 200; + + private static string? NormalizeOrderKeyword(string? keyword) + { + var text = keyword?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + return text.Length <= MaxOrderKeywordLength + ? text + : text[..MaxOrderKeywordLength]; + } + + private static string? TruncateFilterValue(string? value) + { + var text = value?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + return text.Length <= MaxOrderFilterValueLength + ? text + : text[..MaxOrderFilterValueLength]; + } + + private static string? NormalizeOrderSortField(string? sortField) + { + var field = sortField?.Trim(); + if (string.IsNullOrWhiteSpace(field) || !OrderSortFieldWhitelist.Contains(field)) + { + return null; + } + + return field; + } + + private static string? NormalizeOrderSortOrder(string? sortOrder) + { + var order = sortOrder?.Trim(); + if (string.IsNullOrWhiteSpace(order)) + { + return null; + } + + if (string.Equals(order, "asc", StringComparison.OrdinalIgnoreCase)) + { + return "asc"; + } + + if (string.Equals(order, "desc", StringComparison.OrdinalIgnoreCase)) + { + return "desc"; + } + + return null; + } + + private static List? NormalizeOrderFieldFilters( + List? filters) + { + if (filters == null || filters.Count == 0) + { + return null; + } + + var result = new List(); + foreach (var f in filters.Take(MaxOrderFieldFilters)) + { + var field = f.Field?.Trim() ?? string.Empty; + var op = string.IsNullOrWhiteSpace(f.Op) ? "like" : f.Op.Trim(); + if (!OrderFieldFilterWhitelist.Contains(field) || !OrderFieldFilterOps.Contains(op)) + { + continue; + } + + op = op.ToLowerInvariant(); + if (string.Equals(op, "isNull", StringComparison.OrdinalIgnoreCase)) + { + op = "empty"; + } + + var values = (f.Values ?? []) + .Select(x => TruncateFilterValue(x)) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Cast() + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(MaxOrderInValues) + .ToList(); + if (values.Count == 0 && !string.IsNullOrWhiteSpace(f.Value) + && (op is "in" or "notin")) + { + values = f.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(x => TruncateFilterValue(x)) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Cast() + .Distinct(StringComparer.OrdinalIgnoreCase) + .Take(MaxOrderInValues) + .ToList(); + } + + var needsValue = op is not ("empty" or "isnotnull"); + var truncatedValue = TruncateFilterValue(f.Value); + var truncatedValue2 = TruncateFilterValue(f.Value2); + if (needsValue && values.Count == 0 && string.IsNullOrWhiteSpace(truncatedValue)) + { + continue; + } + + if (op is "between" && string.IsNullOrWhiteSpace(truncatedValue2) && values.Count < 2) + { + continue; + } + + result.Add(new OrderListFieldFilter + { + Field = field, + Op = op, + Value = truncatedValue, + Value2 = truncatedValue2, + Values = values.Count > 0 ? values : null + }); + } + + return result.Count == 0 ? null : result; + } private async Task EnsureCustomerCanAccessOrderAsync(Order order) { diff --git a/src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs b/src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs new file mode 100644 index 0000000..f8b4228 --- /dev/null +++ b/src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs @@ -0,0 +1,17 @@ +namespace Oms.Domain.OrdersAggregate; + +/// +/// 订单列表字段级筛选(仓储侧已规范化)。 +/// +public class OrderListFieldFilter +{ + public string Field { get; set; } = string.Empty; + + public string Op { get; set; } = "like"; + + public string? Value { get; set; } + + public string? Value2 { get; set; } + + public List? Values { get; set; } +} diff --git a/src/Oms.Domain/OrdersAggregate/OrderListFilter.cs b/src/Oms.Domain/OrdersAggregate/OrderListFilter.cs index f7e7a34..df7e5c2 100644 --- a/src/Oms.Domain/OrdersAggregate/OrderListFilter.cs +++ b/src/Oms.Domain/OrdersAggregate/OrderListFilter.cs @@ -19,6 +19,21 @@ public class OrderListFilter public int? CustomerId { get; set; } + /// + /// 表头字段级筛选(已规范化的白名单条件)。 + /// + public List? FieldFilters { get; set; } + + /// + /// 已规范化的排序字段(白名单);空则默认 UpdateTime Desc。 + /// + public string? SortField { get; set; } + + /// + /// asc / desc。 + /// + public string? SortOrder { get; set; } + public List? ScopeOrganizationIds { get; set; } public List? ScopeSecretLevels { get; set; } diff --git a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs index 862c0bb..d0279ae 100644 --- a/src/Oms.SqlSugarCore/Orders/OrderRepository.cs +++ b/src/Oms.SqlSugarCore/Orders/OrderRepository.cs @@ -19,9 +19,8 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I var db = await GetDbContextAsync(cancellationToken); var query = ApplyListFilter(db.Queryable(), filter); var total = await query.CountAsync(cancellationToken); + query = ApplyListSort(query, filter); var items = await query - .OrderBy(x => x.UpdateTime, OrderByType.Desc) - .OrderBy(x => x.Id, OrderByType.Desc) .ToPageListAsync(pageIndex, pageSize, cancellationToken); return (items, total); } @@ -31,9 +30,7 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I CancellationToken cancellationToken = default) { var db = await GetDbContextAsync(cancellationToken); - return await ApplyListFilter(db.Queryable(), filter) - .OrderBy(x => x.UpdateTime, OrderByType.Desc) - .OrderBy(x => x.Id, OrderByType.Desc) + return await ApplyListSort(ApplyListFilter(db.Queryable(), filter), filter) .ToListAsync(cancellationToken); } @@ -98,11 +95,502 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I query = query.Where(x => x.OrderSource == orderSource); } + query = ApplyFieldFilters(query, filter.FieldFilters); + query = OrderListScopeFilterApplier.Apply(query, filter); return query; } + private static ISugarQueryable ApplyListSort( + ISugarQueryable query, + OrderListFilter filter) + { + var field = filter.SortField?.Trim(); + var asc = string.Equals(filter.SortOrder, "asc", StringComparison.OrdinalIgnoreCase); + var orderType = asc ? OrderByType.Asc : OrderByType.Desc; + + if (string.IsNullOrWhiteSpace(field)) + { + return query + .OrderBy(x => x.UpdateTime, OrderByType.Desc) + .OrderBy(x => x.Id, OrderByType.Desc); + } + + if (string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.CreateTime, orderType); + } + else if (string.Equals(field, "updateTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.UpdateTime, orderType); + } + else if (string.Equals(field, "serviceNo", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.ServiceNo, orderType); + } + else if (string.Equals(field, "clientNo", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.ClientNo, orderType); + } + else if (string.Equals(field, "trackNo", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.TrackNo, orderType); + } + else if (string.Equals(field, "payableAmount", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.PayableAmount, orderType); + } + else if (string.Equals(field, "payTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.PayTime, orderType); + } + else if (string.Equals(field, "shipTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.ShipTime, orderType); + } + else if (string.Equals(field, "receiveTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.ReceiveTime, orderType); + } + else if (string.Equals(field, "signTime", StringComparison.OrdinalIgnoreCase)) + { + query = query.OrderBy(x => x.SignTime, orderType); + } + else + { + query = query.OrderBy(x => x.UpdateTime, OrderByType.Desc); + } + + return query.OrderBy(x => x.Id, OrderByType.Desc); + } + + private static ISugarQueryable ApplyFieldFilters( + ISugarQueryable query, + List? filters) + { + if (filters == null || filters.Count == 0) + { + return query; + } + + foreach (var filter in filters) + { + var field = filter.Field?.Trim() ?? string.Empty; + var op = (filter.Op ?? "like").Trim().ToLowerInvariant(); + var value = filter.Value?.Trim() ?? string.Empty; + var values = ResolveInValues(filter); + + if (string.Equals(field, "serviceNo", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => x.ServiceNo.Contains(v)), + (q, v) => q.Where(x => x.ServiceNo == v), + (q, v) => q.Where(x => x.ServiceNo != v), + (q, list) => q.Where(x => list.Contains(x.ServiceNo)), + (q, list) => q.Where(x => !list.Contains(x.ServiceNo)), + q => q.Where(x => x.ServiceNo == null || x.ServiceNo == string.Empty), + q => q.Where(x => x.ServiceNo != null && x.ServiceNo != string.Empty)); + } + else if (string.Equals(field, "clientNo", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.ClientNo ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.ClientNo == v), + (q, v) => q.Where(x => x.ClientNo != v), + (q, list) => q.Where(x => x.ClientNo != null && list.Contains(x.ClientNo)), + (q, list) => q.Where(x => x.ClientNo == null || !list.Contains(x.ClientNo)), + q => q.Where(x => x.ClientNo == null || x.ClientNo == string.Empty), + q => q.Where(x => x.ClientNo != null && x.ClientNo != string.Empty)); + } + else if (string.Equals(field, "trackNo", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.TrackNo ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.TrackNo == v), + (q, v) => q.Where(x => x.TrackNo != v), + (q, list) => q.Where(x => x.TrackNo != null && list.Contains(x.TrackNo)), + (q, list) => q.Where(x => x.TrackNo == null || !list.Contains(x.TrackNo)), + q => q.Where(x => x.TrackNo == null || x.TrackNo == string.Empty), + q => q.Where(x => x.TrackNo != null && x.TrackNo != string.Empty)); + } + else if (string.Equals(field, "downstreamServiceNo", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.DownstreamServiceNo ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.DownstreamServiceNo == v), + (q, v) => q.Where(x => x.DownstreamServiceNo != v), + (q, list) => q.Where(x => x.DownstreamServiceNo != null && list.Contains(x.DownstreamServiceNo)), + (q, list) => q.Where(x => x.DownstreamServiceNo == null || !list.Contains(x.DownstreamServiceNo)), + q => q.Where(x => x.DownstreamServiceNo == null || x.DownstreamServiceNo == string.Empty), + q => q.Where(x => x.DownstreamServiceNo != null && x.DownstreamServiceNo != string.Empty)); + } + else if (string.Equals(field, "orderStatus", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => x.OrderStatus.Contains(v)), + (q, v) => q.Where(x => x.OrderStatus == v), + (q, v) => q.Where(x => x.OrderStatus != v), + (q, list) => q.Where(x => list.Contains(x.OrderStatus)), + (q, list) => q.Where(x => !list.Contains(x.OrderStatus)), + q => q.Where(x => x.OrderStatus == null || x.OrderStatus == string.Empty), + q => q.Where(x => x.OrderStatus != null && x.OrderStatus != string.Empty)); + } + else if (string.Equals(field, "productCode", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.ProductCode ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.ProductCode == v), + (q, v) => q.Where(x => x.ProductCode != v), + (q, list) => q.Where(x => x.ProductCode != null && list.Contains(x.ProductCode)), + (q, list) => q.Where(x => x.ProductCode == null || !list.Contains(x.ProductCode)), + q => q.Where(x => x.ProductCode == null || x.ProductCode == string.Empty), + q => q.Where(x => x.ProductCode != null && x.ProductCode != string.Empty)); + } + else if (string.Equals(field, "channelCode", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.ChannelCode ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.ChannelCode == v), + (q, v) => q.Where(x => x.ChannelCode != v), + (q, list) => q.Where(x => x.ChannelCode != null && list.Contains(x.ChannelCode)), + (q, list) => q.Where(x => x.ChannelCode == null || !list.Contains(x.ChannelCode)), + q => q.Where(x => x.ChannelCode == null || x.ChannelCode == string.Empty), + q => q.Where(x => x.ChannelCode != null && x.ChannelCode != string.Empty)); + } + else if (string.Equals(field, "orderSource", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.OrderSource ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.OrderSource == v), + (q, v) => q.Where(x => x.OrderSource != v), + (q, list) => q.Where(x => x.OrderSource != null && list.Contains(x.OrderSource)), + (q, list) => q.Where(x => x.OrderSource == null || !list.Contains(x.OrderSource)), + q => q.Where(x => x.OrderSource == null || x.OrderSource == string.Empty), + q => q.Where(x => x.OrderSource != null && x.OrderSource != string.Empty)); + } + else if (string.Equals(field, "predeclareFailReason", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.PredeclareFailReason ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.PredeclareFailReason == v), + (q, v) => q.Where(x => x.PredeclareFailReason != v), + (q, list) => q.Where(x => x.PredeclareFailReason != null && list.Contains(x.PredeclareFailReason)), + (q, list) => q.Where(x => x.PredeclareFailReason == null || !list.Contains(x.PredeclareFailReason)), + q => q.Where(x => x.PredeclareFailReason == null || x.PredeclareFailReason == string.Empty), + q => q.Where(x => x.PredeclareFailReason != null && x.PredeclareFailReason != string.Empty)); + } + else if (string.Equals(field, "allocationPendingReason", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyStringField(query, op, value, values, + (q, v) => q.Where(x => (x.AllocationPendingReason ?? string.Empty).Contains(v)), + (q, v) => q.Where(x => x.AllocationPendingReason == v), + (q, v) => q.Where(x => x.AllocationPendingReason != v), + (q, list) => q.Where(x => x.AllocationPendingReason != null && list.Contains(x.AllocationPendingReason)), + (q, list) => q.Where(x => x.AllocationPendingReason == null || !list.Contains(x.AllocationPendingReason)), + q => q.Where(x => x.AllocationPendingReason == null || x.AllocationPendingReason == string.Empty), + q => q.Where(x => x.AllocationPendingReason != null && x.AllocationPendingReason != string.Empty)); + } + else if (string.Equals(field, "isPaid", StringComparison.OrdinalIgnoreCase)) + { + if (byte.TryParse(value, out var paid) || TryParseBoolByte(value, out paid)) + { + query = op switch + { + "ne" => query.Where(x => x.IsPaid != paid), + _ => query.Where(x => x.IsPaid == paid) + }; + } + } + else if (string.Equals(field, "payableAmount", StringComparison.OrdinalIgnoreCase)) + { + query = ApplyDecimalField(query, op, value, filter.Value2, + (q, n) => q.Where(x => x.PayableAmount == n), + (q, n) => q.Where(x => x.PayableAmount != n), + (q, n) => q.Where(x => x.PayableAmount > n), + (q, n) => q.Where(x => x.PayableAmount >= n), + (q, n) => q.Where(x => x.PayableAmount < n), + (q, n) => q.Where(x => x.PayableAmount <= n), + (q, a, b) => q.Where(x => x.PayableAmount >= a && x.PayableAmount <= b), + q => q.Where(x => x.PayableAmount == null), + q => q.Where(x => x.PayableAmount != null)); + } + else if (IsDateField(field)) + { + query = ApplyDateField(query, field, op, value, filter.Value2); + } + } + + return query; + } + + private static bool IsDateField(string field) => + string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase) + || string.Equals(field, "payTime", StringComparison.OrdinalIgnoreCase) + || string.Equals(field, "shipTime", StringComparison.OrdinalIgnoreCase) + || string.Equals(field, "receiveTime", StringComparison.OrdinalIgnoreCase) + || string.Equals(field, "signTime", StringComparison.OrdinalIgnoreCase); + + private static List ResolveInValues(OrderListFieldFilter filter) + { + if (filter.Values is { Count: > 0 }) + { + return filter.Values + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + if (string.IsNullOrWhiteSpace(filter.Value)) + { + return []; + } + + return filter.Value + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static bool TryParseBoolByte(string value, out byte paid) + { + paid = 0; + if (string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase) + || value == "是") + { + paid = 1; + return true; + } + + if (string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "0", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "no", StringComparison.OrdinalIgnoreCase) + || value == "否") + { + paid = 0; + return true; + } + + return false; + } + + private static ISugarQueryable ApplyStringField( + ISugarQueryable query, + string op, + string value, + List values, + Func, string, ISugarQueryable> like, + Func, string, ISugarQueryable> eq, + Func, string, ISugarQueryable> ne, + Func, List, ISugarQueryable> inn, + Func, List, ISugarQueryable> notIn, + Func, ISugarQueryable> empty, + Func, ISugarQueryable> notEmpty) + { + return op switch + { + "empty" or "isnull" => empty(query), + "isnotnull" => notEmpty(query), + "eq" => eq(query, value), + "ne" => ne(query, value), + "in" when values.Count > 0 => inn(query, values), + "notin" when values.Count > 0 => notIn(query, values), + "like" => like(query, value), + _ => like(query, value) + }; + } + + private static ISugarQueryable ApplyDecimalField( + ISugarQueryable query, + string op, + string value, + string? value2, + Func, decimal, ISugarQueryable> eq, + Func, decimal, ISugarQueryable> ne, + Func, decimal, ISugarQueryable> gt, + Func, decimal, ISugarQueryable> gte, + Func, decimal, ISugarQueryable> lt, + Func, decimal, ISugarQueryable> lte, + Func, decimal, decimal, ISugarQueryable> between, + Func, ISugarQueryable> empty, + Func, ISugarQueryable> notEmpty) + { + if (op is "empty" or "isnull") + { + return empty(query); + } + + if (op is "isnotnull") + { + return notEmpty(query); + } + + if (!decimal.TryParse(value, out var n)) + { + return query; + } + + if (op == "between") + { + if (!decimal.TryParse(value2, out var n2)) + { + return query; + } + + var lo = Math.Min(n, n2); + var hi = Math.Max(n, n2); + return between(query, lo, hi); + } + + return op switch + { + "eq" or "like" => eq(query, n), + "ne" => ne(query, n), + "gt" => gt(query, n), + "gte" => gte(query, n), + "lt" => lt(query, n), + "lte" => lte(query, n), + _ => eq(query, n) + }; + } + + private static ISugarQueryable ApplyDateField( + ISugarQueryable query, + string field, + string op, + string value, + string? value2) + { + if (op is "empty" or "isnull") + { + return field.ToLowerInvariant() switch + { + "paytime" => query.Where(x => x.PayTime == null), + "shiptime" => query.Where(x => x.ShipTime == null), + "receivetime" => query.Where(x => x.ReceiveTime == null), + "signtime" => query.Where(x => x.SignTime == null), + _ => query + }; + } + + if (op is "isnotnull") + { + return field.ToLowerInvariant() switch + { + "paytime" => query.Where(x => x.PayTime != null), + "shiptime" => query.Where(x => x.ShipTime != null), + "receivetime" => query.Where(x => x.ReceiveTime != null), + "signtime" => query.Where(x => x.SignTime != null), + _ => query + }; + } + + if (!DateTime.TryParse(value, out var dt)) + { + return query; + } + + if (op == "between") + { + if (!DateTime.TryParse(value2, out var dt2)) + { + return query; + } + + var start = dt <= dt2 ? dt : dt2; + var end = dt <= dt2 ? dt2 : dt; + return ApplyDateCompare(query, field, "between", start, end); + } + + return ApplyDateCompare(query, field, op, dt, null); + } + + private static ISugarQueryable ApplyDateCompare( + ISugarQueryable query, + string field, + string op, + DateTime dt, + DateTime? dt2) + { + var key = field.ToLowerInvariant(); + if (key == "createtime") + { + return op switch + { + "eq" => query.Where(x => x.CreateTime >= dt.Date && x.CreateTime < dt.Date.AddDays(1)), + "ne" => query.Where(x => x.CreateTime < dt.Date || x.CreateTime >= dt.Date.AddDays(1)), + "gt" => query.Where(x => x.CreateTime > dt), + "gte" => query.Where(x => x.CreateTime >= dt), + "lt" => query.Where(x => x.CreateTime < dt), + "lte" => query.Where(x => x.CreateTime <= dt), + "between" when dt2.HasValue => query.Where(x => x.CreateTime >= dt && x.CreateTime <= dt2.Value), + _ => query + }; + } + + if (key == "paytime") + { + return op switch + { + "eq" => query.Where(x => x.PayTime >= dt.Date && x.PayTime < dt.Date.AddDays(1)), + "gt" => query.Where(x => x.PayTime > dt), + "gte" => query.Where(x => x.PayTime >= dt), + "lt" => query.Where(x => x.PayTime < dt), + "lte" => query.Where(x => x.PayTime <= dt), + "between" when dt2.HasValue => query.Where(x => x.PayTime >= dt && x.PayTime <= dt2.Value), + _ => query + }; + } + + if (key == "shiptime") + { + return op switch + { + "eq" => query.Where(x => x.ShipTime >= dt.Date && x.ShipTime < dt.Date.AddDays(1)), + "gt" => query.Where(x => x.ShipTime > dt), + "gte" => query.Where(x => x.ShipTime >= dt), + "lt" => query.Where(x => x.ShipTime < dt), + "lte" => query.Where(x => x.ShipTime <= dt), + "between" when dt2.HasValue => query.Where(x => x.ShipTime >= dt && x.ShipTime <= dt2.Value), + _ => query + }; + } + + if (key == "receivetime") + { + return op switch + { + "eq" => query.Where(x => x.ReceiveTime >= dt.Date && x.ReceiveTime < dt.Date.AddDays(1)), + "gt" => query.Where(x => x.ReceiveTime > dt), + "gte" => query.Where(x => x.ReceiveTime >= dt), + "lt" => query.Where(x => x.ReceiveTime < dt), + "lte" => query.Where(x => x.ReceiveTime <= dt), + "between" when dt2.HasValue => query.Where(x => x.ReceiveTime >= dt && x.ReceiveTime <= dt2.Value), + _ => query + }; + } + + if (key == "signtime") + { + return op switch + { + "eq" => query.Where(x => x.SignTime >= dt.Date && x.SignTime < dt.Date.AddDays(1)), + "gt" => query.Where(x => x.SignTime > dt), + "gte" => query.Where(x => x.SignTime >= dt), + "lt" => query.Where(x => x.SignTime < dt), + "lte" => query.Where(x => x.SignTime <= dt), + "between" when dt2.HasValue => query.Where(x => x.SignTime >= dt && x.SignTime <= dt2.Value), + _ => query + }; + } + + return query; + } + public Task GetByServiceNoAsync(string serviceNo, CancellationToken cancellationToken = default) { return FirstOrDefaultAsync(x => x.ServiceNo == serviceNo, cancellationToken); @@ -523,6 +1011,9 @@ public class OrderRepository : OmsSqlSugarRepository, IOrderRepository, I CreateTimeStart = filter.CreateTimeStart, CreateTimeEnd = filter.CreateTimeEnd, CustomerId = filter.CustomerId, + FieldFilters = filter.FieldFilters, + SortField = filter.SortField, + SortOrder = filter.SortOrder, ScopeOrganizationIds = filter.ScopeOrganizationIds, ScopeSecretLevels = filter.ScopeSecretLevels, ScopeDataScope = filter.ScopeDataScope,