feat: 订单列表表头筛选、远程排序与默认90天时间窗护栏
支持 filters[]/whitelist 排序,导出行数与批量 IN 上限,无时间窗时兜底近90天。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
238eb5d817
commit
a55668ec33
@ -24,4 +24,24 @@ public class CustomerAccountFlowPaginatedInput
|
|||||||
public DateTime? StartTime { get; set; }
|
public DateTime? StartTime { get; set; }
|
||||||
|
|
||||||
public DateTime? EndTime { get; set; }
|
public DateTime? EndTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变动金额下限(含)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? AmountMin { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变动金额上限(含)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? AmountMax { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段:createTime / amount。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// asc / desc。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,4 +41,14 @@ public class CustomerPaginatedInput
|
|||||||
/// 所属顾问筛选。
|
/// 所属顾问筛选。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CustomerService { get; set; }
|
public string? CustomerService { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段:createTime / updateTime / customerCode / customerName / balance。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// asc / desc。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,4 +34,19 @@ public class OrderExportInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
||||||
public DateTime? CreateTimeEnd { get; set; }
|
public DateTime? CreateTimeEnd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表头字段级筛选(白名单)。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("filters")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("filters")]
|
||||||
|
public List<OrderListFieldFilterDto>? 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs
Normal file
36
src/Oms.Application/Dtos/Orders/OrderListFieldFilterDto.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单列表字段级筛选(表头 filters[],白名单字段 + DataPerm 风格算子)。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderListFieldFilterDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 字段白名单见 OrderService.NormalizeOrderFieldFilters。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("field")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("field")]
|
||||||
|
public string Field { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// like / eq / ne / in / notIn / gt / gte / lt / lte / between / empty / isNull / isNotNull。
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IN / NOT IN 多值(优先于 value 逗号拆分)。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("values")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("values")]
|
||||||
|
public List<string>? Values { get; set; }
|
||||||
|
}
|
||||||
@ -62,4 +62,25 @@ public class OrderPaginatedInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
||||||
public DateTime? CreateTimeEnd { get; set; }
|
public DateTime? CreateTimeEnd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表头字段级筛选(白名单);与 keyword 并存时 AND。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("filters")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("filters")]
|
||||||
|
public List<OrderListFieldFilterDto>? Filters { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段白名单:createTime / updateTime / serviceNo / clientNo / trackNo / payableAmount / payTime / shipTime / receiveTime / signTime。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("sortField")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("sortField")]
|
||||||
|
public string? SortField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// asc / desc。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("sortOrder")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("sortOrder")]
|
||||||
|
public string? SortOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs
Normal file
33
src/Oms.Application/Dtos/Shared/ListFieldFilterDto.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
namespace Oms.Application.Dtos.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 列表字段级筛选(表头 filters[],白名单字段由各接口自行校验)。
|
||||||
|
/// </summary>
|
||||||
|
public class ListFieldFilterDto
|
||||||
|
{
|
||||||
|
[Newtonsoft.Json.JsonProperty("field")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("field")]
|
||||||
|
public string Field { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// like / eq / ne / in / notIn / gt / gte / lt / lte / between / empty / isNull / isNotNull。
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IN / NOT IN 多值(优先于 value 逗号拆分)。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("values")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("values")]
|
||||||
|
public List<string>? Values { get; set; }
|
||||||
|
}
|
||||||
@ -41,11 +41,7 @@ public class CustomerAccountFlowService : ApplicationService
|
|||||||
predicate,
|
predicate,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
new Dictionary<string, bool>
|
BuildAccountFlowSortDictionary(input));
|
||||||
{
|
|
||||||
[nameof(CustomerAccountFlow.CreateTime)] = false,
|
|
||||||
[nameof(CustomerAccountFlow.Id)] = false
|
|
||||||
});
|
|
||||||
|
|
||||||
var total = await FlowRepository.CountAsync(predicate);
|
var total = await FlowRepository.CountAsync(predicate);
|
||||||
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
|
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
|
||||||
@ -94,27 +90,48 @@ public class CustomerAccountFlowService : ApplicationService
|
|||||||
&& !string.Equals(bizType, "all", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(bizType, "all", StringComparison.OrdinalIgnoreCase);
|
||||||
var hasCustomerFilter = customerId.HasValue;
|
var hasCustomerFilter = customerId.HasValue;
|
||||||
var customerIdValue = customerId.GetValueOrDefault();
|
var customerIdValue = customerId.GetValueOrDefault();
|
||||||
|
var hasStart = input.StartTime.HasValue;
|
||||||
if (input.StartTime.HasValue && input.EndTime.HasValue)
|
var hasEnd = input.EndTime.HasValue;
|
||||||
{
|
var start = input.StartTime.GetValueOrDefault();
|
||||||
var start = input.StartTime.Value;
|
var end = input.EndTime.GetValueOrDefault();
|
||||||
var end = input.EndTime.Value;
|
var hasAmountMin = input.AmountMin.HasValue;
|
||||||
return flow =>
|
var hasAmountMax = input.AmountMax.HasValue;
|
||||||
(!hasCustomerFilter || flow.CustomerId == customerIdValue) &&
|
var amountMin = input.AmountMin.GetValueOrDefault();
|
||||||
(!hasBizType || flow.BizType == bizType) &&
|
var amountMax = input.AmountMax.GetValueOrDefault();
|
||||||
(!hasKeyword ||
|
|
||||||
flow.SerialNumber.Contains(keyword!) ||
|
|
||||||
(flow.BizNo ?? string.Empty).Contains(keyword!)) &&
|
|
||||||
flow.CreateTime >= start &&
|
|
||||||
flow.CreateTime <= end;
|
|
||||||
}
|
|
||||||
|
|
||||||
return flow =>
|
return flow =>
|
||||||
(!hasCustomerFilter || flow.CustomerId == customerIdValue) &&
|
(!hasCustomerFilter || flow.CustomerId == customerIdValue) &&
|
||||||
(!hasBizType || flow.BizType == bizType) &&
|
(!hasBizType || flow.BizType == bizType) &&
|
||||||
(!hasKeyword ||
|
(!hasKeyword ||
|
||||||
flow.SerialNumber.Contains(keyword!) ||
|
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<string, bool> 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<string, bool>
|
||||||
|
{
|
||||||
|
[nameof(CustomerAccountFlow.Amount)] = asc,
|
||||||
|
[nameof(CustomerAccountFlow.Id)] = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dictionary<string, bool>
|
||||||
|
{
|
||||||
|
[nameof(CustomerAccountFlow.CreateTime)] = string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? asc
|
||||||
|
: false,
|
||||||
|
[nameof(CustomerAccountFlow.Id)] = false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetBizTypeName(string bizType)
|
private static string GetBizTypeName(string bizType)
|
||||||
|
|||||||
@ -66,7 +66,7 @@ public class CustomerService : ApplicationService
|
|||||||
predicate,
|
predicate,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false });
|
BuildCustomerSortDictionary(input));
|
||||||
|
|
||||||
var totalCount = await CustomerRepository.CountAsync(predicate);
|
var totalCount = await CustomerRepository.CountAsync(predicate);
|
||||||
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
|
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
|
||||||
@ -424,6 +424,40 @@ public class CustomerService : ApplicationService
|
|||||||
(string.IsNullOrWhiteSpace(customerService) || customer.CustomerService == customerService);
|
(string.IsNullOrWhiteSpace(customerService) || customer.CustomerService == customerService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, bool> 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<string, bool> { [nameof(Customer.CreateTime)] = asc, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(field, "customerCode", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new Dictionary<string, bool> { [nameof(Customer.CustomerCode)] = asc, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(field, "customerName", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new Dictionary<string, bool> { [nameof(Customer.CustomerName)] = asc, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(field, "balance", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(field, "accountBalance", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new Dictionary<string, bool> { [nameof(Customer.Balance)] = asc, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(field, "updateTime", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = asc, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false };
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Customer> GetCustomerOrThrowAsync(int id)
|
private async Task<Customer> GetCustomerOrThrowAsync(int id)
|
||||||
{
|
{
|
||||||
var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id);
|
var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|||||||
@ -282,6 +282,9 @@ public partial class OrderService
|
|||||||
CreateTimeStart = start,
|
CreateTimeStart = start,
|
||||||
CreateTimeEnd = filter.CreateTimeEnd,
|
CreateTimeEnd = filter.CreateTimeEnd,
|
||||||
CustomerId = filter.CustomerId,
|
CustomerId = filter.CustomerId,
|
||||||
|
FieldFilters = filter.FieldFilters,
|
||||||
|
SortField = filter.SortField,
|
||||||
|
SortOrder = filter.SortOrder,
|
||||||
ScopeOrganizationIds = filter.ScopeOrganizationIds,
|
ScopeOrganizationIds = filter.ScopeOrganizationIds,
|
||||||
ScopeSecretLevels = filter.ScopeSecretLevels,
|
ScopeSecretLevels = filter.ScopeSecretLevels,
|
||||||
ScopeDataScope = filter.ScopeDataScope,
|
ScopeDataScope = filter.ScopeDataScope,
|
||||||
|
|||||||
@ -68,9 +68,14 @@ public partial class OrderService : ApplicationService
|
|||||||
/// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。
|
/// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("/order/status-counts")]
|
[HttpGet("/order/status-counts")]
|
||||||
public async Task<OrderStatusCountsOutput> GetStatusCountsAsync()
|
public async Task<OrderStatusCountsOutput> GetStatusCountsAsync(
|
||||||
|
[FromQuery] DateTime? createTimeStart = null,
|
||||||
|
[FromQuery] DateTime? createTimeEnd = null)
|
||||||
{
|
{
|
||||||
var filter = await BuildScopedListFilterAsync();
|
var filter = await BuildScopedListFilterAsync();
|
||||||
|
filter.CreateTimeStart = createTimeStart;
|
||||||
|
filter.CreateTimeEnd = createTimeEnd;
|
||||||
|
EnsureOrderListTimeWindow(filter);
|
||||||
var rows = await OrderRepository.GetStatusCountsByFilterAsync(filter);
|
var rows = await OrderRepository.GetStatusCountsByFilterAsync(filter);
|
||||||
var counts = rows.ToDictionary(
|
var counts = rows.ToDictionary(
|
||||||
x => x.Status,
|
x => x.Status,
|
||||||
@ -316,7 +321,14 @@ public partial class OrderService : ApplicationService
|
|||||||
null);
|
null);
|
||||||
var filter = BuildListFilter(input, customerId);
|
var filter = BuildListFilter(input, customerId);
|
||||||
await ApplyInternalOrderDataScopeAsync(filter);
|
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 fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx";
|
||||||
var directory = GetOrderExportDirectory();
|
var directory = GetOrderExportDirectory();
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
@ -507,28 +519,266 @@ public partial class OrderService : ApplicationService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OrderListFilter BuildListFilter(OrderPaginatedInput input, int? customerId) =>
|
private static OrderListFilter BuildListFilter(OrderPaginatedInput input, int? customerId)
|
||||||
new()
|
{
|
||||||
|
var filter = new OrderListFilter
|
||||||
{
|
{
|
||||||
Status = input.Status,
|
Status = input.Status,
|
||||||
HandleStatus = input.HandleStatus,
|
HandleStatus = input.HandleStatus,
|
||||||
OrderSource = input.OrderSource,
|
OrderSource = input.OrderSource,
|
||||||
Keyword = input.Keyword,
|
Keyword = NormalizeOrderKeyword(input.Keyword),
|
||||||
CreateTimeStart = input.CreateTimeStart,
|
CreateTimeStart = input.CreateTimeStart,
|
||||||
CreateTimeEnd = input.CreateTimeEnd,
|
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) =>
|
private static OrderListFilter BuildListFilter(OrderExportInput input, int? customerId)
|
||||||
new()
|
{
|
||||||
|
var filter = new OrderListFilter
|
||||||
{
|
{
|
||||||
Status = input.Status,
|
Status = input.Status,
|
||||||
OrderSource = input.OrderSource,
|
OrderSource = input.OrderSource,
|
||||||
Keyword = input.Keyword,
|
Keyword = NormalizeOrderKeyword(input.Keyword),
|
||||||
CreateTimeStart = input.CreateTimeStart,
|
CreateTimeStart = input.CreateTimeStart,
|
||||||
CreateTimeEnd = input.CreateTimeEnd,
|
CreateTimeEnd = input.CreateTimeEnd,
|
||||||
CustomerId = customerId
|
CustomerId = customerId,
|
||||||
|
FieldFilters = NormalizeOrderFieldFilters(input.Filters),
|
||||||
|
SortField = NormalizeOrderSortField(input.SortField),
|
||||||
|
SortOrder = NormalizeOrderSortOrder(input.SortOrder)
|
||||||
};
|
};
|
||||||
|
EnsureOrderListTimeWindow(filter);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>订单列表默认时间窗(天)。无精确单号且未传时间时兜底,防全表扫。</summary>
|
||||||
|
private const int OrderListDefaultDays = 90;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无创建时间且非「精确单号查询」时,自动限制近 N 天。
|
||||||
|
/// </summary>
|
||||||
|
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<string>(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<string> 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<string> OrderFieldFilterOps = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"like", "eq", "ne", "in", "notIn", "gt", "gte", "lt", "lte", "between",
|
||||||
|
"empty", "isNull", "isNotNull"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> OrderSortFieldWhitelist = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"createTime",
|
||||||
|
"updateTime",
|
||||||
|
"serviceNo",
|
||||||
|
"clientNo",
|
||||||
|
"trackNo",
|
||||||
|
"payableAmount",
|
||||||
|
"payTime",
|
||||||
|
"shipTime",
|
||||||
|
"receiveTime",
|
||||||
|
"signTime"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>单次列表最多接受的 filters 条数。</summary>
|
||||||
|
private const int MaxOrderFieldFilters = 20;
|
||||||
|
|
||||||
|
/// <summary>IN / NOT IN 单字段最多值数量(防几万单号打爆 SQL/参数包)。</summary>
|
||||||
|
private const int MaxOrderInValues = 500;
|
||||||
|
|
||||||
|
/// <summary>单值最大长度(单号/文本)。</summary>
|
||||||
|
private const int MaxOrderFilterValueLength = 128;
|
||||||
|
|
||||||
|
/// <summary>keyword 最大长度。</summary>
|
||||||
|
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<OrderListFieldFilter>? NormalizeOrderFieldFilters(
|
||||||
|
List<OrderListFieldFilterDto>? filters)
|
||||||
|
{
|
||||||
|
if (filters == null || filters.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<OrderListFieldFilter>();
|
||||||
|
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<string>()
|
||||||
|
.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<string>()
|
||||||
|
.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)
|
private async Task EnsureCustomerCanAccessOrderAsync(Order order)
|
||||||
{
|
{
|
||||||
|
|||||||
17
src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs
Normal file
17
src/Oms.Domain/OrdersAggregate/OrderListFieldFilter.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单列表字段级筛选(仓储侧已规范化)。
|
||||||
|
/// </summary>
|
||||||
|
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<string>? Values { get; set; }
|
||||||
|
}
|
||||||
@ -19,6 +19,21 @@ public class OrderListFilter
|
|||||||
|
|
||||||
public int? CustomerId { get; set; }
|
public int? CustomerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表头字段级筛选(已规范化的白名单条件)。
|
||||||
|
/// </summary>
|
||||||
|
public List<OrderListFieldFilter>? FieldFilters { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已规范化的排序字段(白名单);空则默认 UpdateTime Desc。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// asc / desc。
|
||||||
|
/// </summary>
|
||||||
|
public string? SortOrder { get; set; }
|
||||||
|
|
||||||
public List<string>? ScopeOrganizationIds { get; set; }
|
public List<string>? ScopeOrganizationIds { get; set; }
|
||||||
|
|
||||||
public List<int>? ScopeSecretLevels { get; set; }
|
public List<int>? ScopeSecretLevels { get; set; }
|
||||||
|
|||||||
@ -19,9 +19,8 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
var db = await GetDbContextAsync(cancellationToken);
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
var query = ApplyListFilter(db.Queryable<Order>(), filter);
|
var query = ApplyListFilter(db.Queryable<Order>(), filter);
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
|
query = ApplyListSort(query, filter);
|
||||||
var items = await query
|
var items = await query
|
||||||
.OrderBy(x => x.UpdateTime, OrderByType.Desc)
|
|
||||||
.OrderBy(x => x.Id, OrderByType.Desc)
|
|
||||||
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
|
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
|
||||||
return (items, total);
|
return (items, total);
|
||||||
}
|
}
|
||||||
@ -31,9 +30,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var db = await GetDbContextAsync(cancellationToken);
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
return await ApplyListFilter(db.Queryable<Order>(), filter)
|
return await ApplyListSort(ApplyListFilter(db.Queryable<Order>(), filter), filter)
|
||||||
.OrderBy(x => x.UpdateTime, OrderByType.Desc)
|
|
||||||
.OrderBy(x => x.Id, OrderByType.Desc)
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +95,502 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
query = query.Where(x => x.OrderSource == orderSource);
|
query = query.Where(x => x.OrderSource == orderSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query = ApplyFieldFilters(query, filter.FieldFilters);
|
||||||
|
|
||||||
query = OrderListScopeFilterApplier.Apply(query, filter);
|
query = OrderListScopeFilterApplier.Apply(query, filter);
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ISugarQueryable<Order> ApplyListSort(
|
||||||
|
ISugarQueryable<Order> 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<Order> ApplyFieldFilters(
|
||||||
|
ISugarQueryable<Order> query,
|
||||||
|
List<OrderListFieldFilter>? 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<string> 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<Order> ApplyStringField(
|
||||||
|
ISugarQueryable<Order> query,
|
||||||
|
string op,
|
||||||
|
string value,
|
||||||
|
List<string> values,
|
||||||
|
Func<ISugarQueryable<Order>, string, ISugarQueryable<Order>> like,
|
||||||
|
Func<ISugarQueryable<Order>, string, ISugarQueryable<Order>> eq,
|
||||||
|
Func<ISugarQueryable<Order>, string, ISugarQueryable<Order>> ne,
|
||||||
|
Func<ISugarQueryable<Order>, List<string>, ISugarQueryable<Order>> inn,
|
||||||
|
Func<ISugarQueryable<Order>, List<string>, ISugarQueryable<Order>> notIn,
|
||||||
|
Func<ISugarQueryable<Order>, ISugarQueryable<Order>> empty,
|
||||||
|
Func<ISugarQueryable<Order>, ISugarQueryable<Order>> 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<Order> ApplyDecimalField(
|
||||||
|
ISugarQueryable<Order> query,
|
||||||
|
string op,
|
||||||
|
string value,
|
||||||
|
string? value2,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> eq,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> ne,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> gt,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> gte,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> lt,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, ISugarQueryable<Order>> lte,
|
||||||
|
Func<ISugarQueryable<Order>, decimal, decimal, ISugarQueryable<Order>> between,
|
||||||
|
Func<ISugarQueryable<Order>, ISugarQueryable<Order>> empty,
|
||||||
|
Func<ISugarQueryable<Order>, ISugarQueryable<Order>> 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<Order> ApplyDateField(
|
||||||
|
ISugarQueryable<Order> 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<Order> ApplyDateCompare(
|
||||||
|
ISugarQueryable<Order> 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<Order?> GetByServiceNoAsync(string serviceNo, CancellationToken cancellationToken = default)
|
public Task<Order?> GetByServiceNoAsync(string serviceNo, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return FirstOrDefaultAsync(x => x.ServiceNo == serviceNo, cancellationToken);
|
return FirstOrDefaultAsync(x => x.ServiceNo == serviceNo, cancellationToken);
|
||||||
@ -523,6 +1011,9 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
CreateTimeStart = filter.CreateTimeStart,
|
CreateTimeStart = filter.CreateTimeStart,
|
||||||
CreateTimeEnd = filter.CreateTimeEnd,
|
CreateTimeEnd = filter.CreateTimeEnd,
|
||||||
CustomerId = filter.CustomerId,
|
CustomerId = filter.CustomerId,
|
||||||
|
FieldFilters = filter.FieldFilters,
|
||||||
|
SortField = filter.SortField,
|
||||||
|
SortOrder = filter.SortOrder,
|
||||||
ScopeOrganizationIds = filter.ScopeOrganizationIds,
|
ScopeOrganizationIds = filter.ScopeOrganizationIds,
|
||||||
ScopeSecretLevels = filter.ScopeSecretLevels,
|
ScopeSecretLevels = filter.ScopeSecretLevels,
|
||||||
ScopeDataScope = filter.ScopeDataScope,
|
ScopeDataScope = filter.ScopeDataScope,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user