|
@@ -326,6 +326,7 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
// 构建搜索条件
|
|
// 构建搜索条件
|
|
|
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
|
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
|
|
{
|
|
{
|
|
|
|
|
+ #region and 构建
|
|
|
var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
|
|
var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
|
|
|
var keywordConditions = new List<string>(); // 专门存放关键字相关条件
|
|
var keywordConditions = new List<string>(); // 专门存放关键字相关条件
|
|
|
|
|
|
|
@@ -365,7 +366,51 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
{
|
|
{
|
|
|
whereConditions.Add($"({string.Join(" OR ", keywordConditions)})");
|
|
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
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 构建过滤条件
|
|
// 构建过滤条件
|
|
@@ -405,83 +450,6 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
return (data, totalCount);
|
|
return (data, totalCount);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// 构建返回字段列表
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- private string BuildReturnFields(List<string> returnFields)
|
|
|
|
|
- {
|
|
|
|
|
- if (returnFields == null || !returnFields.Any())
|
|
|
|
|
- {
|
|
|
|
|
- return "*";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 验证字段是否存在
|
|
|
|
|
- var validFields = ValidateReturnFields(returnFields);
|
|
|
|
|
-
|
|
|
|
|
- if (!validFields.Any())
|
|
|
|
|
- {
|
|
|
|
|
- return "*";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 构建字段列表,处理可能的SQL注入和字段名转义
|
|
|
|
|
- var escapedFields = validFields.Select(field =>
|
|
|
|
|
- {
|
|
|
|
|
- // 如果字段包含空格、特殊字符或关键字,用方括号括起来
|
|
|
|
|
- if (field.Contains(" ") || IsSqlKeyword(field) || field.Contains("."))
|
|
|
|
|
- {
|
|
|
|
|
- return $"[{field}]";
|
|
|
|
|
- }
|
|
|
|
|
- return field;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return string.Join(", ", escapedFields);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// 验证返回字段的有效性
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- private List<string> ValidateReturnFields(List<string> returnFields)
|
|
|
|
|
- {
|
|
|
|
|
- if (returnFields == null) return new List<string>();
|
|
|
|
|
-
|
|
|
|
|
- var entityType = typeof(T);
|
|
|
|
|
- var entityProperties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
|
|
|
- .Select(p => p.Name)
|
|
|
|
|
- .ToList();
|
|
|
|
|
-
|
|
|
|
|
- // 过滤出实体类中存在的字段
|
|
|
|
|
- var validFields = returnFields
|
|
|
|
|
- .Where(field => entityProperties.Contains(field, StringComparer.OrdinalIgnoreCase))
|
|
|
|
|
- .ToList();
|
|
|
|
|
-
|
|
|
|
|
- // 记录无效字段警告
|
|
|
|
|
- var invalidFields = returnFields.Except(validFields, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
|
- if (invalidFields.Any())
|
|
|
|
|
- {
|
|
|
|
|
- _logger.LogWarning("发现无效的返回字段: {InvalidFields}", string.Join(", ", invalidFields));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return validFields;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// 检查是否为SQL关键字
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- private bool IsSqlKeyword(string word)
|
|
|
|
|
- {
|
|
|
|
|
- var sqlKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
- {
|
|
|
|
|
- "SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP",
|
|
|
|
|
- "ALTER", "TABLE", "VIEW", "INDEX", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
|
|
|
|
|
- "AND", "OR", "NOT", "LIKE", "IN", "BETWEEN", "IS", "NULL", "ORDER", "BY",
|
|
|
|
|
- "GROUP", "HAVING", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "ON", "AS",
|
|
|
|
|
- "DISTINCT", "TOP", "COUNT", "SUM", "AVG", "MIN", "MAX", "UNION", "ALL"
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return sqlKeywords.Contains(word);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 构建基础查询
|
|
/// 构建基础查询
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -565,8 +533,7 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
|
|
|
|
|
#endregion
|
|
#endregion
|
|
|
|
|
|
|
|
- #region 私有方法 - 匹配度计算
|
|
|
|
|
-
|
|
|
|
|
|
|
+ #region 私有方法 - 匹配度计算含出现位置权重(单字检索时全部出现)
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 在应用层计算匹配度
|
|
/// 在应用层计算匹配度
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -588,7 +555,7 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
|
|
|
|
|
var scoredItems = data.Select(item =>
|
|
var scoredItems = data.Select(item =>
|
|
|
{
|
|
{
|
|
|
- var matchResult = CalculateItemMatchScore(item, searchAnalysis, searchFields, fieldWeights);
|
|
|
|
|
|
|
+ var matchResult = CalculateItemMatchScore(item, searchAnalysis, searchFields, fieldWeights, request.RequireAllSingleChars);
|
|
|
|
|
|
|
|
return new SearchResultItem<T>
|
|
return new SearchResultItem<T>
|
|
|
{
|
|
{
|
|
@@ -612,11 +579,24 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
T item,
|
|
T item,
|
|
|
SearchAnalysis analysis,
|
|
SearchAnalysis analysis,
|
|
|
List<string> searchFields,
|
|
List<string> searchFields,
|
|
|
- Dictionary<string, int> fieldWeights)
|
|
|
|
|
|
|
+ Dictionary<string, int> fieldWeights,
|
|
|
|
|
+ bool requireAllSingleChars)
|
|
|
{
|
|
{
|
|
|
int totalScore = 0;
|
|
int totalScore = 0;
|
|
|
var matchFields = new List<MatchFieldInfo>();
|
|
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)
|
|
foreach (var field in searchFields)
|
|
|
{
|
|
{
|
|
|
var fieldValue = GetFieldValue(item, field);
|
|
var fieldValue = GetFieldValue(item, field);
|
|
@@ -659,7 +639,7 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 单字匹配
|
|
|
|
|
|
|
+ // 单字匹配 - 加入位置权重计算
|
|
|
foreach (var singleChar in analysis.SingleChars)
|
|
foreach (var singleChar in analysis.SingleChars)
|
|
|
{
|
|
{
|
|
|
int count = fieldValue.Count(c => c == singleChar);
|
|
int count = fieldValue.Count(c => c == singleChar);
|
|
@@ -667,16 +647,28 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
{
|
|
{
|
|
|
int charScore = count * (int)(weight * 0.3);
|
|
int charScore = count * (int)(weight * 0.3);
|
|
|
|
|
|
|
|
|
|
+ // 计算位置权重奖励
|
|
|
|
|
+ var positionBonus = CalculateSingleCharPositionBonus(fieldValue, singleChar, weight);
|
|
|
|
|
+ charScore += positionBonus.Bonus;
|
|
|
|
|
+
|
|
|
if (fieldValue.StartsWith(singleChar.ToString()))
|
|
if (fieldValue.StartsWith(singleChar.ToString()))
|
|
|
{
|
|
{
|
|
|
charScore += weight;
|
|
charScore += weight;
|
|
|
- fieldMatchReasons.Add($"开头单字 '{singleChar}'");
|
|
|
|
|
|
|
+ fieldMatchReasons.Add($"开头单字 '{singleChar}' +{weight}");
|
|
|
}
|
|
}
|
|
|
- else
|
|
|
|
|
|
|
+ else if (positionBonus.Bonus > 0)
|
|
|
{
|
|
{
|
|
|
- fieldMatchReasons.Add($"包含单字 '{singleChar}'({count}次)");
|
|
|
|
|
|
|
+ fieldMatchReasons.Add($"靠前单字 '{singleChar}' +{positionBonus.Bonus}");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 添加位置信息到原因
|
|
|
|
|
+ if (positionBonus.FirstPosition >= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ fieldMatchReasons.Add($"位置{positionBonus.FirstPosition + 1}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fieldMatchReasons.Add($"包含单字 '{singleChar}'({count}次)");
|
|
|
|
|
+
|
|
|
fieldScore += charScore;
|
|
fieldScore += charScore;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -701,15 +693,68 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// 获取显示用的字段值(截断过长的内容)
|
|
|
|
|
|
|
+ /// 新增:检查所有单字是否在任意字段中出现
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- private string GetDisplayFieldValue(string fieldValue)
|
|
|
|
|
|
|
+ private bool CheckAllSingleCharsExist(T item, List<char> singleChars, List<string> searchFields)
|
|
|
{
|
|
{
|
|
|
- if (string.IsNullOrEmpty(fieldValue))
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
|
|
|
- // 如果字段值过长,截断显示
|
|
|
|
|
- return fieldValue.Length > 50 ? fieldValue.Substring(0, 50) + "..." : fieldValue;
|
|
|
|
|
|
|
+ return fieldValue.Substring(0, maxLength) + "...";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
#endregion
|
|
@@ -983,18 +1028,6 @@ namespace OASystem.API.OAMethodLib.GenericSearch
|
|
|
return weights;
|
|
return weights;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /// <summary>
|
|
|
|
|
- /// 验证字段有效性
|
|
|
|
|
- /// </summary>
|
|
|
|
|
- private List<string> ValidateFields(List<string> fields)
|
|
|
|
|
- {
|
|
|
|
|
- var validFields = typeof(T).GetProperties()
|
|
|
|
|
- .Select(p => p.Name)
|
|
|
|
|
- .ToList();
|
|
|
|
|
-
|
|
|
|
|
- return fields.Intersect(validFields).ToList();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// 验证搜索字段
|
|
/// 验证搜索字段
|
|
|
/// </summary>
|
|
/// </summary>
|