| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087 |
- using SqlSugar;
- using System.Diagnostics;
- using System.DirectoryServices;
- using System.Linq;
- using System.Linq.Expressions;
- namespace OASystem.API.OAMethodLib.GenericSearch
- {
- /// <summary>
- /// 动态检索服务
- /// 支持动态字段权重配置、返回字段筛选、智能搜索等功能
- /// </summary>
- /// <typeparam name="T">实体类型</typeparam>
- public class DynamicSearchService<T> where T : class, new()
- {
- private readonly SqlSugarClient _db;
- private readonly ILogger<DynamicSearchService<T>> _logger;
- public DynamicSearchService(SqlSugarClient db, ILogger<DynamicSearchService<T>> logger)
- {
- _db = db;
- _logger = logger;
- }
- /// <summary>
- /// 执行动态搜索(应用层统计匹配度)
- /// </summary>
- /// <param name="request">搜索请求参数</param>
- /// <returns>包含搜索结果和匹配度信息的结果对象</returns>
- public async Task<SearchResult<T>> SearchAsync(DynamicSearchRequest request)
- {
- var resultView = new SearchResult<T>() { Success = false, Message = "异常错误" };
- var stopwatch = Stopwatch.StartNew();
- var searchId = Guid.NewGuid().ToString("N")[..8];
- try
- {
- List<T> data;
- int totalCount;
- // 使用原生SQL方式构建查询
- if (!string.IsNullOrWhiteSpace(request.Keyword))
- {
- var result = await SearchWithNativeSqlAsync(request);
- data = result.Data;
- totalCount = result.TotalCount;
- }
- else
- {
- // 无关键词时使用简单查询
- var query = BuildBaseQuery(request);
- totalCount = await query.CountAsync();
- data = await query.ToPageListAsync(request.PageIndex, request.PageSize);
- }
- // 在应用层计算匹配度
- var scoredItems = CalculateMatchScore(data, request);
- stopwatch.Stop();
- return new SearchResult<T>
- {
- Message = $"搜索成功!耗时:{stopwatch.ElapsedMilliseconds}ms",
- Items = scoredItems,
- TotalCount = totalCount,
- Keyword = request.Keyword,
- FieldWeights = request.FieldWeights,
- ReturnFields = request.ReturnFields,
- PageIndex = request.PageIndex,
- PageSize = request.PageSize,
- ResponseTime = stopwatch.ElapsedMilliseconds,
- SearchId = searchId
- };
- }
- catch (Exception ex)
- {
- stopwatch.Stop();
- resultView.Message = string.Format("【{SearchId}】动态搜索失败: {ErrorMessage}", searchId, ex.Message);
- return resultView;
- }
- }
- /// <summary>
- /// 轻量级搜索 - 只返回指定字段,提升性能(应用层统计匹配度)
- /// </summary>
- /// <typeparam name="TResult">返回的结果类型</typeparam>
- /// <param name="request">搜索请求参数</param>
- /// <param name="selector">字段选择表达式</param>
- /// <returns>包含指定字段和匹配度信息的搜索结果</returns>
- public async Task<SearchResult<TResult>> LightweightSearchAsync<TResult>(
- DynamicSearchRequest request,
- Expression<Func<T, TResult>> selector) where TResult : class, new()
- {
- var stopwatch = Stopwatch.StartNew();
- var searchId = Guid.NewGuid().ToString("N")[..8];
- _logger.LogInformation("【{SearchId}】开始轻量级搜索: 实体{Entity}, 返回类型{ResultType}",
- searchId, typeof(T).Name, typeof(TResult).Name);
- try
- {
- // 构建基础查询
- var baseQuery = _db.Queryable<T>();
- // 应用过滤条件
- baseQuery = ApplyFilters(baseQuery, request.Filters);
- // 应用搜索条件
- if (!string.IsNullOrWhiteSpace(request.Keyword))
- {
- var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
- if (searchAnalysis.HasSearchContent)
- {
- var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
- var searchConditions = BuildSearchConditions(searchAnalysis, searchFields);
- if (searchConditions.Any())
- {
- baseQuery = baseQuery.Where(searchConditions);
- }
- }
- }
- // 应用字段选择 - 在数据库层面进行字段选择
- var finalQuery = baseQuery.Select(selector);
- // 应用排序
- finalQuery = ApplyOrderByForLightweight(finalQuery, request.OrderBy, request.IsDescending);
- // 执行查询获取轻量级数据
- var totalCount = await finalQuery.CountAsync();
- var lightweightData = await finalQuery.ToPageListAsync(request.PageIndex, request.PageSize);
- // 为了计算匹配度,需要查询完整的实体数据
- List<T> fullDataForScoring;
- if (!string.IsNullOrWhiteSpace(request.Keyword))
- {
- var fullResult = await SearchWithNativeSqlAsync(request);
- fullDataForScoring = fullResult.Data;
- }
- else
- {
- var fullQuery = BuildBaseQuery(request);
- fullDataForScoring = await fullQuery.ToPageListAsync(request.PageIndex, request.PageSize);
- }
- // 计算匹配度
- var scoredItems = CalculateMatchScore(fullDataForScoring, request);
- // 将匹配度信息与轻量级数据关联
- var lightweightItems = AssociateMatchScores(lightweightData, scoredItems, selector);
- stopwatch.Stop();
- _logger.LogInformation("【{SearchId}】轻量级搜索完成: 找到 {Count} 条记录, 耗时 {TotalTime}ms",
- searchId, lightweightItems.Count, stopwatch.ElapsedMilliseconds);
- return new SearchResult<TResult>
- {
- Items = lightweightItems,
- TotalCount = totalCount,
- Keyword = request.Keyword,
- FieldWeights = request.FieldWeights,
- PageIndex = request.PageIndex,
- PageSize = request.PageSize,
- ResponseTime = stopwatch.ElapsedMilliseconds,
- SearchId = searchId
- };
- }
- catch (Exception ex)
- {
- stopwatch.Stop();
- _logger.LogError(ex, "【{SearchId}】轻量级搜索失败", searchId);
- throw;
- }
- }
- /// <summary>
- /// 将匹配度信息与轻量级数据关联
- /// </summary>
- private List<SearchResultItem<TResult>> AssociateMatchScores<TResult>(
- List<TResult> lightweightData,
- List<SearchResultItem<T>> scoredItems,
- Expression<Func<T, TResult>> selector) where TResult : class, new()
- {
- var result = new List<SearchResultItem<TResult>>();
- // 构建一个字典来快速查找匹配度信息
- var scoreDict = new Dictionary<int, SearchResultItem<T>>();
- foreach (var scoredItem in scoredItems)
- {
- var id = GetEntityId(scoredItem.Data);
- if (id > 0)
- {
- scoreDict[id] = scoredItem;
- }
- }
- // 关联匹配度信息
- foreach (var lightItem in lightweightData)
- {
- var id = GetEntityId(lightItem);
- if (id > 0 && scoreDict.TryGetValue(id, out var scoredItem))
- {
- result.Add(new SearchResultItem<TResult>
- {
- Data = lightItem,
- MatchScore = scoredItem.MatchScore,
- MatchFields = scoredItem.MatchFields
- });
- }
- else
- {
- result.Add(new SearchResultItem<TResult>
- {
- Data = lightItem,
- MatchScore = 0,
- MatchFields = new List<MatchFieldInfo>()
- });
- }
- }
- return result.OrderByDescending(x => x.MatchScore).ToList();
- }
- /// <summary>
- /// 获取实体ID(通过反射)
- /// </summary>
- private int GetEntityId<TEntity>(TEntity entity)
- {
- if (entity == null) return 0;
- var idProperty = typeof(TEntity).GetProperty("Id");
- if (idProperty != null && idProperty.PropertyType == typeof(int))
- {
- return (int)(idProperty.GetValue(entity) ?? 0);
- }
- return 0;
- }
- /// <summary>
- /// 获取实体可搜索字段信息
- /// </summary>
- /// <returns>可搜索字段信息列表,按权重降序排列</returns>
- public List<FieldInfo> GetSearchableFields()
- {
- var entityType = typeof(T);
- var properties = entityType.GetProperties();
- var searchableFields = new List<FieldInfo>();
- foreach (var prop in properties)
- {
- var fieldInfo = new FieldInfo
- {
- FieldName = prop.Name,
- DisplayName = GetDisplayName(prop),
- DataType = prop.PropertyType.Name,
- IsSearchable = prop.PropertyType == typeof(string),
- DefaultWeight = GetDefaultWeight(prop.Name),
- Description = GetFieldDescription(prop),
- CanFilter = true,
- CanSort = true
- };
- searchableFields.Add(fieldInfo);
- }
- return searchableFields
- .OrderByDescending(f => f.DefaultWeight)
- .ThenBy(f => f.FieldName)
- .ToList();
- }
- /// <summary>
- /// 验证字段配置
- /// </summary>
- /// <param name="fieldWeights">字段权重配置</param>
- /// <param name="returnFields">返回字段列表</param>
- /// <returns>验证结果</returns>
- public (bool IsValid, string Message) ValidateFieldConfig(
- Dictionary<string, int> fieldWeights,
- List<string> returnFields)
- {
- var allFields = GetSearchableFields();
- var validFieldNames = allFields.Select(f => f.FieldName).ToList();
- // 验证搜索字段
- if (fieldWeights != null)
- {
- var invalidSearchFields = fieldWeights.Keys.Except(validFieldNames).ToList();
- if (invalidSearchFields.Any())
- {
- return (false, $"无效的搜索字段: {string.Join(", ", invalidSearchFields)}");
- }
- }
- // 验证返回字段
- if (returnFields != null)
- {
- var invalidReturnFields = returnFields.Except(validFieldNames).ToList();
- if (invalidReturnFields.Any())
- {
- return (false, $"无效的返回字段: {string.Join(", ", invalidReturnFields)}");
- }
- }
- return (true, "字段配置有效");
- }
- #region 私有方法 - 搜索逻辑
- /// <summary>
- /// 使用原生SQL进行搜索
- /// </summary>
- private async Task<(List<T> Data, int TotalCount)> SearchWithNativeSqlAsync(DynamicSearchRequest request)
- {
- var whereConditions = new List<string>();
- var parameters = new List<SugarParameter>();
- // 获取搜索字段
- var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
- var validFields = ValidateSearchFields(searchFields);
- // 构建搜索条件
- if (!string.IsNullOrWhiteSpace(request.Keyword))
- {
- #region and 构建
- var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
- var keywordConditions = new List<string>(); // 专门存放关键字相关条件
- // 符号分割的关键字条件
- foreach (var segment in searchAnalysis.SymbolSegments)
- {
- var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
- if (!string.IsNullOrEmpty(cleanSegment))
- {
- var fieldConditions = validFields.Select(field =>
- {
- var paramName = $"@segment{parameters.Count}";
- parameters.Add(new SugarParameter(paramName, $"%{cleanSegment}%"));
- return $"{field} LIKE {paramName}";
- });
- // 每个片段内部使用 OR 连接不同字段
- keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
- }
- }
- // 单字检索条件
- foreach (var singleChar in searchAnalysis.SingleChars)
- {
- var charStr = singleChar.ToString();
- var fieldConditions = validFields.Select(field =>
- {
- var paramName = $"@char{parameters.Count}";
- parameters.Add(new SugarParameter(paramName, $"%{charStr}%"));
- return $"{field} LIKE {paramName}";
- });
- // 每个单字内部使用 OR 连接不同字段
- keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
- }
- // 所有关键字条件使用 OR 连接
- if (keywordConditions.Any())
- {
- whereConditions.Add($"({string.Join(" OR ", keywordConditions)})");
- }
- #endregion
- #region or 构建
- //var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
- //var keywordConditions = new List<string>();
- //// 统一处理所有搜索词
- //var allSearchTerms = new List<string>();
- //// 添加符号分割的片段(去除特殊字符)
- //allSearchTerms.AddRange(searchAnalysis.SymbolSegments
- // .Select(segment => Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", ""))
- // .Where(segment => !string.IsNullOrEmpty(segment)));
- //// 添加单字(排除重复)
- //foreach (var singleChar in searchAnalysis.SingleChars)
- //{
- // var charStr = singleChar.ToString();
- // // 只有当单字不在任何符号片段中时才添加
- // if (!allSearchTerms.Any(term => term.Contains(charStr)))
- // {
- // allSearchTerms.Add(charStr);
- // }
- //}
- //// 处理每个搜索词
- //foreach (var term in allSearchTerms.Distinct())
- //{
- // var fieldConditions = validFields.Select(field =>
- // {
- // var paramName = $"@term{parameters.Count}";
- // parameters.Add(new SugarParameter(paramName, $"%{term}%"));
- // return $"{field} LIKE {paramName}";
- // });
- // keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
- //}
- //// 所有搜索条件使用 AND 连接
- //if (keywordConditions.Any())
- //{
- // whereConditions.Add($"({string.Join(" AND ", keywordConditions)})");
- //}
- #endregion
- }
- // 构建过滤条件
- var filterConditions = BuildNativeFilterConditions(request.Filters, parameters);
- whereConditions.AddRange(filterConditions);
- // 构建完整SQL
- var whereClause = whereConditions.Any()
- ? "WHERE " + string.Join(" AND ", whereConditions)
- : "";
- var orderByClause = BuildNativeOrderByClause(request.OrderBy, request.IsDescending);
- var tableName = _db.EntityMaintenance.GetTableName(typeof(T));
- // 构建返回字段
- //var returnFields = BuildReturnFields(request.ReturnFields);
- // 先查询总数
- var countSql = $"SELECT COUNT(1) FROM {tableName} {whereClause}";
- var totalCount = await _db.Ado.GetIntAsync(countSql, parameters);
- // 再查询数据
- var offset = (request.PageIndex - 1) * request.PageSize;
- var dataSql = $@"
- SELECT * FROM (
- SELECT *, ROW_NUMBER() OVER ({orderByClause}) AS RowNumber
- FROM {tableName}
- {whereClause}
- ) AS Paginated
- WHERE Paginated.RowNumber > {offset} AND Paginated.RowNumber <= {offset + request.PageSize}
- {orderByClause}";
- var data = await _db.Ado.SqlQueryAsync<T>(dataSql, parameters);
- return (data, totalCount);
- }
- /// <summary>
- /// 构建基础查询
- /// </summary>
- private ISugarQueryable<T> BuildBaseQuery(DynamicSearchRequest request)
- {
- var query = _db.Queryable<T>();
- // 应用过滤条件
- query = ApplyFilters(query, request.Filters);
- // 应用排序
- query = ApplyOrderBy(query, request.OrderBy, request.IsDescending);
- return query;
- }
- /// <summary>
- /// 构建原生过滤条件
- /// </summary>
- private List<string> BuildNativeFilterConditions(List<SearchFilter> filters, List<SugarParameter> parameters)
- {
- var conditions = new List<string>();
- if (filters == null) return conditions;
- foreach (var filter in filters)
- {
- var condition = filter.Operator?.ToLower() switch
- {
- "eq" => BuildNativeCondition(filter, "=", parameters),
- "neq" => BuildNativeCondition(filter, "!=", parameters),
- "contains" => BuildNativeLikeCondition(filter, "%", "%", parameters),
- "startswith" => BuildNativeLikeCondition(filter, "", "%", parameters),
- "endswith" => BuildNativeLikeCondition(filter, "%", "", parameters),
- "gt" => BuildNativeCondition(filter, ">", parameters),
- "gte" => BuildNativeCondition(filter, ">=", parameters),
- "lt" => BuildNativeCondition(filter, "<", parameters),
- "lte" => BuildNativeCondition(filter, "<=", parameters),
- "in" => BuildNativeInCondition(filter, parameters),
- _ => null
- };
- if (!string.IsNullOrEmpty(condition))
- {
- conditions.Add(condition);
- }
- }
- return conditions;
- }
- private string BuildNativeCondition(SearchFilter filter, string op, List<SugarParameter> parameters)
- {
- var paramName = $"@filter{parameters.Count}";
- parameters.Add(new SugarParameter(paramName, filter.Value));
- return $"{filter.Field} {op} {paramName}";
- }
- private string BuildNativeLikeCondition(SearchFilter filter, string prefix, string suffix, List<SugarParameter> parameters)
- {
- var paramName = $"@filter{parameters.Count}";
- parameters.Add(new SugarParameter(paramName, $"{prefix}{filter.Value}{suffix}"));
- return $"{filter.Field} LIKE {paramName}";
- }
- private string BuildNativeInCondition(SearchFilter filter, List<SugarParameter> parameters)
- {
- if (filter.Values == null || !filter.Values.Any())
- return null;
- var paramNames = new List<string>();
- foreach (var value in filter.Values)
- {
- var paramName = $"@filter{parameters.Count}";
- parameters.Add(new SugarParameter(paramName, value));
- paramNames.Add(paramName);
- }
- return $"{filter.Field} IN ({string.Join(",", paramNames)})";
- }
- #endregion
- #region 私有方法 - 匹配度计算含出现位置权重(单字检索时全部出现)
- /// <summary>
- /// 在应用层计算匹配度
- /// </summary>
- private List<SearchResultItem<T>> CalculateMatchScore(List<T> data, DynamicSearchRequest request)
- {
- if (string.IsNullOrWhiteSpace(request.Keyword))
- {
- // 无关键词时,所有记录匹配度为0
- return data.Select(item => new SearchResultItem<T>
- {
- Data = item,
- MatchScore = 0
- }).ToList();
- }
- var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
- var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
- var fieldWeights = request.FieldWeights ?? GetDefaultFieldWeights(searchFields);
- var scoredItems = data.Select(item =>
- {
- var matchResult = CalculateItemMatchScore(item, searchAnalysis, searchFields, fieldWeights, request.RequireAllSingleChars);
- return new SearchResultItem<T>
- {
- Data = item,
- MatchScore = matchResult.TotalScore,
- MatchFields = matchResult.MatchFields
- };
- })
- .Where(item => item.MatchScore > 0) // 只保留有匹配的记录
- .OrderByDescending(item => item.MatchScore)
- .ThenByDescending(item => GetCreateTime(item.Data))
- .ToList();
- return scoredItems;
- }
- /// <summary>
- /// 计算单个项的匹配度详情
- /// </summary>
- private (int TotalScore, List<MatchFieldInfo> MatchFields) CalculateItemMatchScore(
- T item,
- SearchAnalysis analysis,
- List<string> searchFields,
- Dictionary<string, int> fieldWeights,
- bool requireAllSingleChars)
- {
- int totalScore = 0;
- var matchFields = new List<MatchFieldInfo>();
- // 新增:根据参数检查是否所有单字都出现
- if (requireAllSingleChars && analysis.SingleChars.Any())
- {
- bool allSingleCharsExist = CheckAllSingleCharsExist(item, analysis.SingleChars, searchFields);
- // 如果要求所有单字必须出现但未完全匹配,直接返回0分
- if (!allSingleCharsExist)
- {
- return (0, matchFields);
- }
- }
- foreach (var field in searchFields)
- {
- var fieldValue = GetFieldValue(item, field);
- if (string.IsNullOrEmpty(fieldValue))
- continue;
- var weight = fieldWeights.ContainsKey(field) ? fieldWeights[field] : GetDefaultWeight(field);
- int fieldScore = 0;
- var fieldMatchReasons = new List<string>();
- // 符号分割关键字匹配
- foreach (var segment in analysis.SymbolSegments)
- {
- var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
- if (!string.IsNullOrEmpty(cleanSegment) && fieldValue.Contains(cleanSegment))
- {
- int segmentScore = weight;
- if (fieldValue.Equals(cleanSegment))
- {
- segmentScore += 15;
- fieldMatchReasons.Add($"完全匹配 '{cleanSegment}'");
- }
- else if (fieldValue.StartsWith(cleanSegment))
- {
- segmentScore += 10;
- fieldMatchReasons.Add($"开头匹配 '{cleanSegment}'");
- }
- else if (fieldValue.EndsWith(cleanSegment))
- {
- segmentScore += 5;
- fieldMatchReasons.Add($"结尾匹配 '{cleanSegment}'");
- }
- else
- {
- fieldMatchReasons.Add($"包含 '{cleanSegment}'");
- }
- fieldScore += segmentScore;
- }
- }
- // 单字匹配 - 加入位置权重计算
- foreach (var singleChar in analysis.SingleChars)
- {
- int count = fieldValue.Count(c => c == singleChar);
- if (count > 0)
- {
- int charScore = count * (int)(weight * 0.3);
- // 计算位置权重奖励
- var positionBonus = CalculateSingleCharPositionBonus(fieldValue, singleChar, weight);
- charScore += positionBonus.Bonus;
- if (fieldValue.StartsWith(singleChar.ToString()))
- {
- charScore += weight;
- fieldMatchReasons.Add($"开头单字 '{singleChar}' +{weight}");
- }
- else if (positionBonus.Bonus > 0)
- {
- fieldMatchReasons.Add($"靠前单字 '{singleChar}' +{positionBonus.Bonus}");
- }
- // 添加位置信息到原因
- if (positionBonus.FirstPosition >= 0)
- {
- fieldMatchReasons.Add($"位置{positionBonus.FirstPosition + 1}");
- }
- fieldMatchReasons.Add($"包含单字 '{singleChar}'({count}次)");
- fieldScore += charScore;
- }
- }
- if (fieldScore > 0)
- {
- totalScore += fieldScore;
- matchFields.Add(new MatchFieldInfo
- {
- FieldName = field,
- FieldValue = GetDisplayFieldValue(fieldValue),
- Score = fieldScore,
- MatchReason = string.Join("; ", fieldMatchReasons)
- });
- }
- }
- // 按分数排序匹配字段
- matchFields = matchFields.OrderByDescending(m => m.Score).ToList();
- return (totalScore, matchFields);
- }
- /// <summary>
- /// 新增:检查所有单字是否在任意字段中出现
- /// </summary>
- private bool CheckAllSingleCharsExist(T item, List<char> singleChars, List<string> searchFields)
- {
- foreach (var singleChar in singleChars)
- {
- bool charExists = false;
- // 检查所有搜索字段中是否包含该单字
- foreach (var field in searchFields)
- {
- var fieldValue = GetFieldValue(item, field);
- if (!string.IsNullOrEmpty(fieldValue) && fieldValue.Contains(singleChar))
- {
- charExists = true;
- break;
- }
- }
- // 如果有一个单字不存在,直接返回false
- if (!charExists)
- {
- return false;
- }
- }
- return true;
- }
- /// <summary>
- /// 计算单字位置权重奖励
- /// </summary>
- private (int Bonus, int FirstPosition) CalculateSingleCharPositionBonus(string fieldValue, char singleChar, int baseWeight)
- {
- var firstPosition = fieldValue.IndexOf(singleChar);
- if (firstPosition == -1)
- return (0, -1);
- // 计算位置比例 (0-1),0表示开头,1表示结尾
- double positionRatio = (double)firstPosition / Math.Max(fieldValue.Length - 1, 1);
- // 位置权重系数:位置越靠前,奖励越高
- double positionFactor = 1.0 - positionRatio;
- // 计算奖励分数(基于基础权重)
- int positionBonus = (int)(baseWeight * positionFactor * 0.8); // 最大奖励为基础权重的80%
- // 确保奖励至少为1
- positionBonus = Math.Max(positionBonus, 1);
- return (positionBonus, firstPosition);
- }
- /// <summary>
- /// 获取显示字段值(截断过长的值)
- /// </summary>
- private string GetDisplayFieldValue(string fieldValue, int maxLength = 50)
- {
- if (string.IsNullOrEmpty(fieldValue) || fieldValue.Length <= maxLength)
- return fieldValue;
- return fieldValue.Substring(0, maxLength) + "...";
- }
- #endregion
- #region 私有方法 - 辅助功能
- /// <summary>
- /// 应用过滤条件
- /// </summary>
- private ISugarQueryable<T> ApplyFilters(ISugarQueryable<T> query, List<SearchFilter> filters)
- {
- if (filters == null || !filters.Any())
- return query;
- foreach (var filter in filters)
- {
- query = filter.Operator?.ToLower() switch
- {
- "eq" => query.Where($"{filter.Field} = @Value", new { filter.Value }),
- "neq" => query.Where($"{filter.Field} != @Value", new { filter.Value }),
- "contains" => query.Where($"{filter.Field} LIKE '%' + @Value + '%'", new { filter.Value }),
- "startswith" => query.Where($"{filter.Field} LIKE @Value + '%'", new { filter.Value }),
- "endswith" => query.Where($"{filter.Field} LIKE '%' + @Value", new { filter.Value }),
- "gt" => query.Where($"{filter.Field} > @Value", new { filter.Value }),
- "gte" => query.Where($"{filter.Field} >= @Value", new { filter.Value }),
- "lt" => query.Where($"{filter.Field} < @Value", new { filter.Value }),
- "lte" => query.Where($"{filter.Field} <= @Value", new { filter.Value }),
- "in" => ApplyInFilter(query, filter),
- _ => query
- };
- }
- return query;
- }
- /// <summary>
- /// 使用SqlSugar条件构建器构建搜索条件
- /// </summary>
- private List<IConditionalModel> BuildSearchConditions(SearchAnalysis analysis, List<string> searchFields)
- {
- var conditionalModels = new List<IConditionalModel>();
- // 获取有效的搜索字段
- var validFields = ValidateSearchFields(searchFields);
- if (!validFields.Any())
- return conditionalModels;
- // 1. 符号分割的关键字条件
- foreach (var segment in analysis.SymbolSegments)
- {
- var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
- if (!string.IsNullOrEmpty(cleanSegment))
- {
- var segmentGroup = new List<IConditionalModel>();
- foreach (var field in validFields)
- {
- segmentGroup.Add(new ConditionalModel
- {
- FieldName = field,
- ConditionalType = ConditionalType.Like,
- FieldValue = $"%{cleanSegment}%"
- });
- }
- if (segmentGroup.Count > 1)
- {
- conditionalModels.Add(new ConditionalCollections
- {
- ConditionalList = new List<KeyValuePair<WhereType, ConditionalModel>>(
- segmentGroup.Select((model, index) =>
- new KeyValuePair<WhereType, ConditionalModel>(
- index == 0 ? WhereType.And : WhereType.Or,
- (ConditionalModel)model))
- )
- });
- }
- else if (segmentGroup.Count == 1)
- {
- conditionalModels.Add(segmentGroup[0]);
- }
- }
- }
- return conditionalModels;
- }
- /// <summary>
- /// 应用IN过滤条件
- /// </summary>
- private ISugarQueryable<T> ApplyInFilter(ISugarQueryable<T> query, SearchFilter filter)
- {
- if (filter.Values == null || !filter.Values.Any())
- return query;
- var valueList = string.Join(",", filter.Values.Select(v => $"'{v}'"));
- return query.Where($"{filter.Field} IN ({valueList})");
- }
- /// <summary>
- /// 应用排序
- /// </summary>
- private ISugarQueryable<T> ApplyOrderBy(ISugarQueryable<T> query, string orderBy, bool isDescending)
- {
- if (string.IsNullOrWhiteSpace(orderBy))
- {
- // 默认按主键或创建时间排序
- var entityType = typeof(T);
- var idProperty = entityType.GetProperty("Id") ?? entityType.GetProperty("CreateTime");
- if (idProperty != null)
- {
- orderBy = idProperty.Name;
- }
- }
- if (!string.IsNullOrWhiteSpace(orderBy))
- {
- return isDescending
- ? query.OrderBy($"{orderBy} DESC")
- : query.OrderBy($"{orderBy} ASC");
- }
- return query;
- }
- /// <summary>
- /// 为轻量级搜索应用排序
- /// </summary>
- private ISugarQueryable<TResult> ApplyOrderByForLightweight<TResult>(
- ISugarQueryable<TResult> query,
- string orderBy,
- bool isDescending) where TResult : class, new()
- {
- if (string.IsNullOrWhiteSpace(orderBy))
- {
- // 检查结果类型是否有Id或CreateTime字段
- var resultType = typeof(TResult);
- var idProperty = resultType.GetProperty("Id") ?? resultType.GetProperty("CreateTime");
- if (idProperty != null)
- {
- orderBy = idProperty.Name;
- }
- else
- {
- // 如果没有默认排序字段,返回原查询
- return query;
- }
- }
- if (!string.IsNullOrWhiteSpace(orderBy))
- {
- // 验证排序字段是否存在于结果类型中
- var resultType = typeof(TResult);
- var orderByProperty = resultType.GetProperty(orderBy);
- if (orderByProperty != null)
- {
- return isDescending
- ? query.OrderBy($"{orderBy} DESC")
- : query.OrderBy($"{orderBy} ASC");
- }
- else
- {
- _logger.LogWarning("排序字段 {OrderBy} 在返回类型 {ResultType} 中不存在", orderBy, resultType.Name);
- }
- }
- return query;
- }
- /// <summary>
- /// 构建原生排序子句
- /// </summary>
- private string BuildNativeOrderByClause(string orderBy, bool isDescending)
- {
- if (string.IsNullOrWhiteSpace(orderBy))
- //return "ORDER BY Id DESC";
- return "";
- return $"ORDER BY {orderBy} {(isDescending ? "DESC" : "ASC")}";
- }
- /// <summary>
- /// 分析搜索模式
- /// </summary>
- private SearchAnalysis AnalyzeSearchPattern(string keyword)
- {
- var analysis = new SearchAnalysis { OriginalKeyword = keyword };
- // 检查是否包含特殊符号
- var specialSymbols = new[] { ' ', ',', ',', ';', ';', '、', '/', '\\', '|', '-', '_' };
- if (keyword.Any(c => specialSymbols.Contains(c)))
- {
- analysis.HasSpecialSymbols = true;
- analysis.SymbolSegments = keyword.Split(specialSymbols, StringSplitOptions.RemoveEmptyEntries)
- .Select(s => s.Trim())
- .Where(s => !string.IsNullOrEmpty(s))
- .ToList();
- }
- // 清理关键词并提取单字
- var cleanKeyword = Regex.Replace(keyword, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
- if (!string.IsNullOrEmpty(cleanKeyword))
- {
- foreach (char c in cleanKeyword)
- {
- analysis.SingleChars.Add(c);
- }
- analysis.IsSingleChar = true;
- // 如果没有特殊符号但有关键词,也作为符号分割段
- if (cleanKeyword.Length > 1 && !analysis.HasSpecialSymbols)
- {
- analysis.SymbolSegments.Add(cleanKeyword);
- }
- }
- // 去重
- analysis.SingleChars = analysis.SingleChars.Distinct().ToList();
- analysis.SymbolSegments = analysis.SymbolSegments.Distinct().ToList();
- return analysis;
- }
- /// <summary>
- /// 获取创建时间(用于排序)
- /// </summary>
- private DateTime GetCreateTime(T item)
- {
- var createTimeProperty = typeof(T).GetProperty("CreateTime");
- if (createTimeProperty != null)
- {
- return (DateTime)(createTimeProperty.GetValue(item) ?? DateTime.MinValue);
- }
- return DateTime.MinValue;
- }
- /// <summary>
- /// 获取字段值
- /// </summary>
- private string GetFieldValue(T item, string fieldName)
- {
- var property = typeof(T).GetProperty(fieldName);
- return property?.GetValue(item) as string;
- }
- /// <summary>
- /// 获取默认搜索字段
- /// </summary>
- private List<string> GetDefaultSearchFields()
- {
- return typeof(T).GetProperties()
- .Where(p => p.PropertyType == typeof(string))
- .Select(p => p.Name)
- .Take(5)
- .ToList();
- }
- /// <summary>
- /// 获取默认字段权重
- /// </summary>
- private Dictionary<string, int> GetDefaultFieldWeights(List<string> fields)
- {
- var weights = new Dictionary<string, int>();
- foreach (var field in fields)
- {
- weights[field] = GetDefaultWeight(field);
- }
- return weights;
- }
- /// <summary>
- /// 验证搜索字段
- /// </summary>
- private List<string> ValidateSearchFields(List<string> searchFields)
- {
- var validFields = typeof(T).GetProperties()
- .Where(p => p.PropertyType == typeof(string))
- .Select(p => p.Name)
- .ToList();
- return searchFields.Intersect(validFields).ToList();
- }
- /// <summary>
- /// 获取默认权重
- /// </summary>
- private int GetDefaultWeight(string fieldName)
- {
- return fieldName.ToLower() switch
- {
- var name when name.Contains("name") => 10,
- var name when name.Contains("title") => 10,
- var name when name.Contains("code") => 8,
- var name when name.Contains("no") => 8,
- var name when name.Contains("desc") => 6,
- var name when name.Contains("content") => 6,
- var name when name.Contains("remark") => 5,
- var name when name.Contains("phone") => 4,
- var name when name.Contains("tel") => 4,
- var name when name.Contains("address") => 3,
- var name when name.Contains("email") => 3,
- _ => 5
- };
- }
- /// <summary>
- /// 获取显示名称
- /// </summary>
- private string GetDisplayName(System.Reflection.PropertyInfo property)
- {
- var displayAttr = property.GetCustomAttribute<System.ComponentModel.DataAnnotations.DisplayAttribute>();
- return displayAttr?.Name ?? property.Name;
- }
- /// <summary>
- /// 获取字段描述
- /// </summary>
- private string GetFieldDescription(System.Reflection.PropertyInfo property)
- {
- var descriptionAttr = property.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
- return descriptionAttr?.Description ?? string.Empty;
- }
- #endregion
- }
- }
|