fix: avoid SqlSugar invalid SQL from null/HasValue KB filters

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-04 16:53:09 +08:00
parent 95940331ce
commit 7e442180cd
3 changed files with 101 additions and 21 deletions

View File

@ -209,33 +209,68 @@ public class KbArticleService : ApplicationService
private static Expression<Func<KbArticle, bool>> 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<KbArticle>();
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<Func<KbArticle, bool>> 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<KbArticle>(),
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)

View File

@ -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<OmsItemsResult<KbCategoryOutput>> AdminListAsync([FromQuery] bool? enabledOnly = null)
{
var items = await CategoryRepository.GetListAsync(x =>
!enabledOnly.HasValue || x.Enabled == enabledOnly.Value);
Expression<Func<KbCategory, bool>> predicate = ExpressionPredicate.True<KbCategory>();
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<KbCategoryOutput>
{

View File

@ -0,0 +1,38 @@
using System.Linq.Expressions;
namespace Oms.Application.Shared;
/// <summary>
/// 组合 LINQ 谓词,避免把 `HasValue` / `== null` 写进表达式导致 SqlSugar 生成非法 SQL。
/// </summary>
public static class ExpressionPredicate
{
public static Expression<Func<T, bool>> True<T>() => static _ => true;
public static Expression<Func<T, bool>> AndAlso<T>(
Expression<Func<T, bool>> left,
Expression<Func<T, bool>> 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<Func<T, bool>>(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);
}
}