From 7e442180cdfb94508555ce896ed0756a46baa66c Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Tue, 4 Aug 2026 16:53:09 +0800 Subject: [PATCH] fix: avoid SqlSugar invalid SQL from null/HasValue KB filters Co-authored-by: Cursor --- .../Services/Kb/KbArticleService.cs | 73 ++++++++++++++----- .../Services/Kb/KbCategoryService.cs | 11 ++- .../Shared/ExpressionPredicate.cs | 38 ++++++++++ 3 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 src/Oms.Application/Shared/ExpressionPredicate.cs diff --git a/src/Oms.Application/Services/Kb/KbArticleService.cs b/src/Oms.Application/Services/Kb/KbArticleService.cs index 8c6cd17..003bbb9 100644 --- a/src/Oms.Application/Services/Kb/KbArticleService.cs +++ b/src/Oms.Application/Services/Kb/KbArticleService.cs @@ -209,33 +209,68 @@ public class KbArticleService : ApplicationService private static Expression> BuildAdminPredicate(KbArticleListInput input) { - var keyword = input.Keyword?.Trim(); + var keyword = NormalizeOptional(input.Keyword); var status = NormalizeOptional(input.Status); var audience = NormalizeOptional(input.Audience); - return x => - (!input.CategoryId.HasValue || x.CategoryId == input.CategoryId.Value) - && (status == null || x.Status == status) - && (audience == null || x.Audience == audience) - && (!input.IsPinned.HasValue || x.IsPinned == input.IsPinned.Value) - && (keyword == null - || x.Title.Contains(keyword) - || (x.Summary != null && x.Summary.Contains(keyword)) - || (x.Tags != null && x.Tags.Contains(keyword))); + var predicate = ExpressionPredicate.True(); + + if (input.CategoryId is int categoryId) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.CategoryId == categoryId); + } + + if (status != null) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.Status == status); + } + + if (audience != null) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.Audience == audience); + } + + if (input.IsPinned is bool isPinned) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.IsPinned == isPinned); + } + + if (keyword != null) + { + predicate = ExpressionPredicate.AndAlso( + predicate, + x => x.Title.Contains(keyword) + || (x.Summary != null && x.Summary.Contains(keyword)) + || (x.Tags != null && x.Tags.Contains(keyword))); + } + + return predicate; } private static Expression> BuildPortalPredicate(string? keyword, int? categoryId) { var kw = NormalizeOptional(keyword); var now = DateTime.Now; - return x => - x.Status == KbArticleStatusCodes.Published - && x.Audience == KbAudienceCodes.Customer - && (x.ExpireTime == null || x.ExpireTime > now) - && (!categoryId.HasValue || x.CategoryId == categoryId.Value) - && (kw == null - || x.Title.Contains(kw) - || (x.Summary != null && x.Summary.Contains(kw)) - || (x.Tags != null && x.Tags.Contains(kw))); + var predicate = ExpressionPredicate.AndAlso( + ExpressionPredicate.True(), + x => x.Status == KbArticleStatusCodes.Published + && x.Audience == KbAudienceCodes.Customer + && (x.ExpireTime == null || x.ExpireTime > now)); + + if (categoryId is int cid) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.CategoryId == cid); + } + + if (kw != null) + { + predicate = ExpressionPredicate.AndAlso( + predicate, + x => x.Title.Contains(kw) + || (x.Summary != null && x.Summary.Contains(kw)) + || (x.Tags != null && x.Tags.Contains(kw))); + } + + return predicate; } private static void EnsurePortalVisible(KbArticle entity) diff --git a/src/Oms.Application/Services/Kb/KbCategoryService.cs b/src/Oms.Application/Services/Kb/KbCategoryService.cs index d045c1c..a42186e 100644 --- a/src/Oms.Application/Services/Kb/KbCategoryService.cs +++ b/src/Oms.Application/Services/Kb/KbCategoryService.cs @@ -1,8 +1,10 @@ +using System.Linq.Expressions; using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Kb; using Oms.Application.Dtos.Shared; +using Oms.Application.Shared; using Oms.Domain.KbAggregate.Entities; using Oms.Domain.KbAggregate.Repositories; @@ -23,8 +25,13 @@ public class KbCategoryService : ApplicationService [HttpGet("/kb/admin/category/list")] public async Task> AdminListAsync([FromQuery] bool? enabledOnly = null) { - var items = await CategoryRepository.GetListAsync(x => - !enabledOnly.HasValue || x.Enabled == enabledOnly.Value); + Expression> predicate = ExpressionPredicate.True(); + if (enabledOnly is bool enabled) + { + predicate = ExpressionPredicate.AndAlso(predicate, x => x.Enabled == enabled); + } + + var items = await CategoryRepository.GetListAsync(predicate); var ordered = items.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToList(); return new OmsItemsResult { diff --git a/src/Oms.Application/Shared/ExpressionPredicate.cs b/src/Oms.Application/Shared/ExpressionPredicate.cs new file mode 100644 index 0000000..1a8810a --- /dev/null +++ b/src/Oms.Application/Shared/ExpressionPredicate.cs @@ -0,0 +1,38 @@ +using System.Linq.Expressions; + +namespace Oms.Application.Shared; + +/// +/// 组合 LINQ 谓词,避免把 `HasValue` / `== null` 写进表达式导致 SqlSugar 生成非法 SQL。 +/// +public static class ExpressionPredicate +{ + public static Expression> True() => static _ => true; + + public static Expression> AndAlso( + Expression> left, + Expression> right) + { + var parameter = Expression.Parameter(typeof(T), "x"); + var leftBody = new ReplaceParameterVisitor(left.Parameters[0], parameter).Visit(left.Body) + ?? throw new InvalidOperationException("left predicate body is null"); + var rightBody = new ReplaceParameterVisitor(right.Parameters[0], parameter).Visit(right.Body) + ?? throw new InvalidOperationException("right predicate body is null"); + return Expression.Lambda>(Expression.AndAlso(leftBody, rightBody), parameter); + } + + private sealed class ReplaceParameterVisitor : ExpressionVisitor + { + private readonly ParameterExpression _from; + private readonly ParameterExpression _to; + + public ReplaceParameterVisitor(ParameterExpression from, ParameterExpression to) + { + _from = from; + _to = to; + } + + protected override Expression VisitParameter(ParameterExpression node) => + ReferenceEquals(node, _from) ? _to : base.VisitParameter(node); + } +}