feat: 订单列表表头筛选、远程排序与默认90天时间窗护栏

支持 filters[]/whitelist 排序,导出行数与批量 IN 上限,无时间窗时兜底近90天。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-31 11:25:31 +08:00
parent 238eb5d817
commit a55668ec33
13 changed files with 998 additions and 36 deletions

View File

@ -24,4 +24,24 @@ public class CustomerAccountFlowPaginatedInput
public DateTime? StartTime { 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; }
}

View File

@ -41,4 +41,14 @@ public class CustomerPaginatedInput
/// 所属顾问筛选。
/// </summary>
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; }
}

View File

@ -34,4 +34,19 @@ public class OrderExportInput
/// </summary>
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
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; }
}

View 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; }
}

View File

@ -62,4 +62,25 @@ public class OrderPaginatedInput
/// </summary>
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
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; }
}

View 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; }
}

View File

@ -41,11 +41,7 @@ public class CustomerAccountFlowService : ApplicationService
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool>
{
[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<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)

View File

@ -66,7 +66,7 @@ public class CustomerService : ApplicationService
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [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<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)
{
var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id);

View File

@ -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,

View File

@ -68,9 +68,14 @@ public partial class OrderService : ApplicationService
/// 按状态聚合统计订单数量(单次查询,供 Tab 角标展示)。
/// </summary>
[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();
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;
}
/// <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)
{

View 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; }
}

View File

@ -19,6 +19,21 @@ public class OrderListFilter
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<int>? ScopeSecretLevels { get; set; }

View File

@ -19,9 +19,8 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
var db = await GetDbContextAsync(cancellationToken);
var query = ApplyListFilter(db.Queryable<Order>(), 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<Order>, IOrderRepository, I
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
return await ApplyListFilter(db.Queryable<Order>(), filter)
.OrderBy(x => x.UpdateTime, OrderByType.Desc)
.OrderBy(x => x.Id, OrderByType.Desc)
return await ApplyListSort(ApplyListFilter(db.Queryable<Order>(), filter), filter)
.ToListAsync(cancellationToken);
}
@ -98,11 +95,502 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
query = query.Where(x => x.OrderSource == orderSource);
}
query = ApplyFieldFilters(query, filter.FieldFilters);
query = OrderListScopeFilterApplier.Apply(query, filter);
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)
{
return FirstOrDefaultAsync(x => x.ServiceNo == serviceNo, cancellationToken);
@ -523,6 +1011,9 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, 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,