Browse Source

重构搜索逻辑,新增关键字搜索接口

删除原有复杂搜索逻辑,包括关键字分析、字段匹配得分计算等,简化为基于关键字的搜索接口 `GroupItemNativeKeywordSearch`。
优化 `DynamicSearchService`,新增字段验证和 SQL 注入防护逻辑,提升安全性和可维护性。
增强 SQL 日志记录功能,支持参数格式化和完整 SQL 打印,便于调试。
新增异常捕获逻辑,提升系统稳定性。
清理冗余代码和注释,简化代码结构。
Lyyyi 1 day ago
parent
commit
934c1ca6da

+ 52 - 353
OASystem/OASystem.Api/Controllers/GroupsController.cs

@@ -1215,387 +1215,86 @@ namespace OASystem.API.Controllers
             }
 
             return Ok(jw);
-
-            #region 智能搜索
-            //try
-            //{
-            //    // 验证请求参数
-            //    if (string.IsNullOrEmpty(keyword))
-            //    {
-            //        return Ok(JsonView(true, $"暂无数据!"));
-            //    }
-
-            //    var searchRequest = new DynamicSearchRequest
-            //    {
-            //        Keyword = keyword,
-            //        PageIndex = 1,
-            //        PageSize = 9999,
-            //        FieldWeights = new Dictionary<string, int>
-            //        {
-            //            { "TeamName", 10 },
-            //            { "ClientUnit", 8 },
-            //            { "ClientName", 6 }
-            //        },
-            //        Filters = new List<SearchFilter>()
-            //        {
-            //            new(){Field = "IsDel",Operator="eq",Value="1" }
-            //        },
-            //        OrderBy = "CreateTime",
-            //        ReturnFields = new List<string>() { "TeamName", "ClientUnit", "ClientName" }
-            //    };
-
-            //    // 验证字段配置
-            //    var validation = _groupSearchService.ValidateFieldConfig(
-            //        searchRequest.FieldWeights,
-            //        searchRequest.ReturnFields);
-
-            //    if (!validation.IsValid)
-            //    {
-            //        return BadRequest(new { message = validation.Message });
-            //    }
-
-            //    var result = await _groupSearchService.SearchAsync(searchRequest);
-
-            //    if (result.Success)
-            //    {
-            //        return Ok(JsonView(true, "搜索成功!", result.Items, result.Items.Count));
-            //    }
-
-            //    return Ok(JsonView(true, $"暂无数据!"));
-            //}
-            //catch (Exception ex)
-            //{
-            //    return Ok(JsonView(true, $"搜索服务暂时不可用!"));
-            //}
-
-
-            //var stopwatch = Stopwatch.StartNew();
-
-            //if (string.IsNullOrWhiteSpace(keyword))
-            //{
-            //    var result = await GetDefaultResultsAsync();
-            //    stopwatch.Stop();
-            //    return Ok(JsonView(true, $"操作成功!耗时:{stopwatch.ElapsedMilliseconds}ms", result, result.Count));
-            //}
-
-            //// 分析搜索模式
-            //var searchAnalysis = AnalyzeSearchPattern(keyword);
-
-            //if (!searchAnalysis.HasSearchContent)
-            //{
-            //    var result = await GetDefaultResultsAsync();
-            //    stopwatch.Stop();
-            //    return Ok(JsonView(true, $"操作成功!耗时:{stopwatch.ElapsedMilliseconds}ms", result, result.Count));
-            //}
-
-            //// 原生SQL查询
-            //var results = await SearchWithNativeSql(searchAnalysis);
-            //var results1 = CalculateUnifiedMatchScore(results, searchAnalysis);
-            //stopwatch.Stop();
-            //return Ok(JsonView(true, $"搜索成功!耗时:{stopwatch.ElapsedMilliseconds}ms", results1, results1.Count));
-            #endregion
-
-        }
-
-        #region 搜索辅助方法
-
-        private List<GroupInfoWithScore> CalculateUnifiedMatchScore(List<GroupInfoWithScore> results, SearchAnalysis analysis)
-        {
-            return results.Select(info => new
-            {
-                Info = info,
-                Score = CalculateComprehensiveMatchScore(info, analysis)
-            })
-            .Where(x => x.Score > 0)
-            .OrderByDescending(x => x.Score)
-            //.ThenByDescending(x => x.Info.CreateTime)
-            .Select(x =>
-            {
-                x.Info.MatchScore = x.Score;
-                return x.Info;
-            })
-            .ToList();
         }
 
-        private int CalculateComprehensiveMatchScore(GroupInfoWithScore info, SearchAnalysis analysis)
+        /// <summary>
+        ///  接团信息列表 关键字输入提示(智能版)
+        /// </summary>
+        /// <param name="keyword">关键字</param>
+        /// <returns></returns>
+        [HttpGet]
+        [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
+        public async Task<IActionResult> GroupItemNativeKeywordSearch(string keyword)
         {
-            int totalScore = 0;
-
-            var targetFields = new[]
-            {
-                new { Value = info.TeamName, Weight = 10 },
-                new { Value = info.ClientUnit, Weight = 8 },
-                new { Value = info.ClientName, Weight = 6 }
-            };
-
-            // 关键值匹配得分
-            foreach (var field in targetFields)
+            try
             {
-                foreach (var keyValue in analysis.KeyValues)
+                // 验证请求参数
+                if (string.IsNullOrEmpty(keyword))
                 {
-                    totalScore += CalculateFieldMatchScore(field.Value, keyValue, field.Weight);
+                    return Ok(JsonView(true, $"暂无数据!"));
                 }
-            }
 
-            // 符号分割关键字匹配得分
-            foreach (var field in targetFields)
-            {
-                foreach (var segment in analysis.SymbolSegments)
+                var searchRequest = new DynamicSearchRequest
                 {
-                    var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
-                    if (!string.IsNullOrEmpty(cleanSegment))
+                    Keyword = keyword,
+                    PageIndex = 1,
+                    PageSize = 9999,
+                    FieldWeights = new Dictionary<string, int>
                     {
-                        totalScore += CalculateFieldMatchScore(field.Value, cleanSegment, field.Weight);
-                    }
-                }
-            }
-
-            // 单字匹配得分
-            foreach (var field in targetFields)
-            {
-                foreach (var singleChar in analysis.SingleChars)
-                {
-                    totalScore += CalculateSingleCharScore(field.Value, singleChar, (int)(field.Weight * 0.3));
-                }
-            }
-
-            return totalScore;
-        }
-
-        private int CalculateFieldMatchScore(string fieldValue, string keyword, int baseWeight)
-        {
-            if (string.IsNullOrEmpty(fieldValue) || string.IsNullOrEmpty(keyword))
-                return 0;
-
-            if (fieldValue.Contains(keyword))
-            {
-                int score = baseWeight;
-
-                if (fieldValue.Equals(keyword))
-                    score += 15;
-                else if (fieldValue.StartsWith(keyword))
-                    score += 10;
-                else if (fieldValue.EndsWith(keyword))
-                    score += 5;
-
-                return score;
-            }
-
-            return 0;
-        }
-
-        private int CalculateSingleCharScore(string fieldValue, char singleChar, int baseWeight)
-        {
-            if (string.IsNullOrEmpty(fieldValue))
-                return 0;
-
-            int count = fieldValue.Count(c => c == singleChar);
-            int score = count * baseWeight;
-
-            if (fieldValue.Length > 0 && fieldValue[0] == singleChar)
-            {
-                score += baseWeight * 2;
-            }
-
-            return score;
-        }
-
-        /// <summary>
-        /// 分析搜索模式 - 关键值查询改为单字查询
-        /// </summary>
-        private SearchAnalysis AnalyzeSearchPattern(string keyword)
-        {
-            var analysis = new SearchAnalysis { OriginalKeyword = keyword };
-
-            // 1. 检查是否包含特殊符号
-            if (ContainsSpecialSymbols(keyword))
-            {
-                analysis.HasSpecialSymbols = true;
-                analysis.SymbolSegments = SplitBySpecialSymbols(keyword);
-            }
+                        { "TeamName", 10 },
+                        { "ClientUnit", 8 },
+                        { "ClientName", 6 }
+                    },
+                    Filters = new List<SearchFilter>()
+                    {
+                        new(){Field = "IsDel",Operator="eq",Value="0" }
+                    },
+                    OrderBy = "TeamName",
+                    ReturnFields = new List<string>() { "TeamName", "ClientUnit", "ClientName" }
+                };
 
-            // 2. 将所有输入字符都作为单字处理
-            var cleanKeyword = Regex.Replace(keyword, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
+                // 验证字段配置
+                var validation = _groupSearchService.ValidateFieldConfig(
+                    searchRequest.FieldWeights,
+                    searchRequest.ReturnFields);
 
-            if (!string.IsNullOrEmpty(cleanKeyword))
-            {
-                // 将所有字符都作为单字处理
-                foreach (char c in cleanKeyword)
+                if (!validation.IsValid)
                 {
-                    analysis.SingleChars.Add(c);
+                    return Ok(JsonView(true, $"暂无数据!{validation.Message}"));
                 }
 
-                analysis.IsSingleChar = true;
+                var result = await _groupSearchService.SearchAsync(searchRequest);
 
-                // 同时将长度>1的连续字符也作为符号分割段处理(为了兼容性)
-                if (cleanKeyword.Length > 1 && !analysis.HasSpecialSymbols)
+                if (result.Success)
                 {
-                    analysis.SymbolSegments.Add(cleanKeyword);
-                }
-            }
+                    var data = new List<dynamic>();
 
-            // 3. 如果包含符号,也从分割结果中提取单字
-            if (analysis.HasSpecialSymbols)
-            {
-                foreach (var segment in analysis.SymbolSegments)
-                {
-                    var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
-                    if (!string.IsNullOrEmpty(cleanSegment))
+                    foreach (var item in result.Items)
                     {
-                        // 将分割段中的每个字符都作为单字
-                        foreach (char c in cleanSegment)
-                        {
-                            if (!analysis.SingleChars.Contains(c))
+                        data.Add(new { 
+                            Data = new
                             {
-                                analysis.SingleChars.Add(c);
-                            }
-                        }
+                                item.Data.TeamName,
+                                item.Data.ClientUnit,
+                                item.Data.ClientName
+                            },
+                            MatchScore = item.MatchScore,
+                            MatchFields = item.MatchFields
+                        });
                     }
+                    
+                    return Ok(JsonView(true, "搜索成功!", data, data.Count));
                 }
-            }
-
-            // 去重
-            analysis.SingleChars = analysis.SingleChars.Distinct().ToList();
-            analysis.SymbolSegments = analysis.SymbolSegments.Distinct().ToList();
 
-            return analysis;
-        }
-
-        private bool ContainsSpecialSymbols(string keyword)
-        {
-            var specialSymbols = new[] { ' ', ',', ',', ';', ';', '、', '/', '\\', '|', '-', '_' };
-            return keyword.Any(c => specialSymbols.Contains(c));
-        }
-
-        private List<string> SplitBySpecialSymbols(string keyword)
-        {
-            var separators = new[] { ' ', ',', ',', ';', ';', '、', '/', '\\', '|', '-', '_' };
-            return keyword.Split(separators, StringSplitOptions.RemoveEmptyEntries)
-                         .Select(s => s.Trim())
-                         .Where(s => !string.IsNullOrEmpty(s))
-                         .ToList();
-        }
-
-        /// <summary>
-        /// 使用原生SQL条件(最稳定)
-        /// </summary>
-        private async Task<List<GroupInfoWithScore>> SearchWithNativeSql(SearchAnalysis analysis)
-        {
-            var whereConditions = new List<string>();
-            var parameters = new List<SugarParameter>();
-
-            // 构建关键值条件
-            foreach (var keyValue in analysis.KeyValues)
-            {
-                whereConditions.Add($"(TeamName LIKE @keyValue{parameters.Count} OR ClientUnit LIKE @keyValue{parameters.Count} OR ClientName LIKE @keyValue{parameters.Count})");
-                parameters.Add(new SugarParameter($"@keyValue{parameters.Count}", $"%{keyValue}%"));
-            }
-
-            // 构建符号分割条件
-            foreach (var segment in analysis.SymbolSegments)
-            {
-                var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
-                if (!string.IsNullOrEmpty(cleanSegment))
-                {
-                    whereConditions.Add($"(TeamName LIKE @segment{parameters.Count} OR ClientUnit LIKE @segment{parameters.Count} OR ClientName LIKE @segment{parameters.Count})");
-                    parameters.Add(new SugarParameter($"@segment{parameters.Count}", $"%{cleanSegment}%"));
-                }
+                return Ok(JsonView(true, result.Message));
             }
-
-            // 构建单字条件
-            foreach (var singleChar in analysis.SingleChars)
+            catch (Exception ex)
             {
-                var charStr = singleChar.ToString();
-                whereConditions.Add($"(TeamName LIKE @char{parameters.Count} OR ClientUnit LIKE @char{parameters.Count} OR ClientName LIKE @char{parameters.Count})");
-                parameters.Add(new SugarParameter($"@char{parameters.Count}", $"%{charStr}%"));
+                return Ok(JsonView(true, $"搜索服务暂时不可用!"));
             }
 
-            // 构建SQL
-            var whereClause = whereConditions.Any()
-                ? "AND (" + string.Join(" OR ", whereConditions) + ")"
-                : "";
-
-            var sql = $@"
-            SELECT * FROM Grp_DelegationInfo 
-            WHERE IsDel = 0 {whereClause}
-            ORDER BY CreateTime DESC";
-
-            var result = await _sqlSugar.Ado.SqlQueryAsync<Grp_DelegationInfo>(sql, parameters);
-
-            return result == null 
-                ? new List<GroupInfoWithScore>() 
-                : result.Select(x => new GroupInfoWithScore() { Id = x.Id, TeamName = x.TeamName, ClientUnit = x.ClientUnit, ClientName = x.ClientName }).ToList();
-        }
-
-        /// <summary>
-        /// 获取默认结果
-        /// </summary>
-        private async Task<List<GroupInfoWithScore>> GetDefaultResultsAsync()
-        {
-            return await _sqlSugar.Queryable<Grp_DelegationInfo>()
-                           .Where(x => x.IsDel == 1)
-                           .OrderByDescending(x => x.CreateTime)
-                           .Select(x => new GroupInfoWithScore() { Id = x.Id, TeamName = x.TeamName, ClientUnit = x.ClientUnit, ClientName = x.ClientName })
-                           .ToListAsync();
-        }
-
-        private class GroupInfoWithScore
-        {
-            public int Id { get; set; }
-            public string TeamName { get; set; }
-            public string ClientUnit { get; set; }
-            public string ClientName { get; set; }
-            public int MatchScore { get; set; }
         }
 
-        /// <summary>
-        /// 搜索分析结果
-        /// </summary>
-        public class SearchAnalysis
-        {
-            /// <summary>
-            /// 原始关键词
-            /// </summary>
-            public string OriginalKeyword { get; set; }
-
-            /// <summary>
-            /// 是否有关键值
-            /// </summary>
-            public bool HasKeyValue { get; set; }
-
-            /// <summary>
-            /// 关键值列表
-            /// </summary>
-            public List<string> KeyValues { get; set; } = new List<string>();
-
-            /// <summary>
-            /// 是否有特殊符号
-            /// </summary>
-            public bool HasSpecialSymbols { get; set; }
-
-            /// <summary>
-            /// 符号分割的段
-            /// </summary>
-            public List<string> SymbolSegments { get; set; } = new List<string>();
-
-            /// <summary>
-            /// 是否是单字
-            /// </summary>
-            public bool IsSingleChar { get; set; }
-
-            /// <summary>
-            /// 单字列表
-            /// </summary>
-            public List<char> SingleChars { get; set; } = new List<char>();
-
-            /// <summary>
-            /// 是否有搜索内容
-            /// </summary>
-            public bool HasSearchContent => HasKeyValue || HasSpecialSymbols || IsSingleChar;
-        }
-
-        #endregion
-
         /// <summary>
         ///  接团信息列表 Page
         /// </summary>

+ 87 - 9
OASystem/OASystem.Api/OAMethodLib/GenericSearch/DynamicSearchService.cs

@@ -1,5 +1,6 @@
 using SqlSugar;
 using System.Diagnostics;
+using System.DirectoryServices;
 using System.Linq;
 using System.Linq.Expressions;
 
@@ -28,12 +29,11 @@ namespace OASystem.API.OAMethodLib.GenericSearch
         /// <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];
 
-            _logger.LogInformation("【{SearchId}】开始动态搜索: 实体{Entity}, 关键词'{Keyword}'",
-                searchId, typeof(T).Name, request.Keyword);
-
             try
             {
                 List<T> data;
@@ -59,9 +59,6 @@ namespace OASystem.API.OAMethodLib.GenericSearch
 
                 stopwatch.Stop();
 
-                _logger.LogInformation("【{SearchId}】动态搜索完成: 找到 {Count} 条记录, 耗时 {TotalTime}ms",
-                    searchId, scoredItems.Count, stopwatch.ElapsedMilliseconds);
-
                 return new SearchResult<T>
                 {
                     Items = scoredItems,
@@ -78,8 +75,8 @@ namespace OASystem.API.OAMethodLib.GenericSearch
             catch (Exception ex)
             {
                 stopwatch.Stop();
-                _logger.LogError(ex, "【{SearchId}】动态搜索失败: {ErrorMessage}", searchId, ex.Message);
-                throw;
+                resultView.Message = string.Format("【{SearchId}】动态搜索失败: {ErrorMessage}", searchId, ex.Message);
+                return resultView;
             }
         }
 
@@ -373,6 +370,9 @@ namespace OASystem.API.OAMethodLib.GenericSearch
 
             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);
@@ -394,6 +394,83 @@ namespace OASystem.API.OAMethodLib.GenericSearch
             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>
@@ -798,7 +875,8 @@ namespace OASystem.API.OAMethodLib.GenericSearch
         private string BuildNativeOrderByClause(string orderBy, bool isDescending)
         {
             if (string.IsNullOrWhiteSpace(orderBy))
-                return "ORDER BY Id DESC";
+                //return "ORDER BY Id DESC";
+                return "";
 
             return $"ORDER BY {orderBy} {(isDescending ? "DESC" : "ASC")}";
         }

+ 27 - 3
OASystem/OASystem.Api/Program.cs

@@ -210,21 +210,45 @@ builder.Services.AddScoped(options =>
         db.Aop.OnLogExecuting = (sql, pars) =>
         {
             //获取原生SQL推荐 5.1.4.63  性能OK
-            //UtilMethods.GetNativeSql(sql, pars);
+            UtilMethods.GetNativeSql(sql, pars);
 
             //获取无参数化SQL 影响性能只适合调试
-            //UtilMethods.GetSqlString(DbType.SqlServer,sql,pars)
+            //UtilMethods.GetSqlString(DbType.SqlServer, sql, pars);
+
+           
+            if (pars != null || pars.Length > 0)
+            {
+                Console.WriteLine("============== 完整 SQL ==============");
+                var resultSql = sql;
+
+                foreach (var param in pars)
+                {
+                    var formattedValue = param.Value switch
+                    {
+                        string str => $"'{str.Replace("'", "''")}'",
+                        DateTime date => $"'{date:yyyy-MM-dd HH:mm:ss}'",
+                        bool b => b ? "1" : "0",
+                        _ => param.Value.ToString()
+                    };
+                    resultSql = resultSql.Replace(param.ParameterName, formattedValue);
+                }
+                Console.WriteLine(resultSql);
+                Console.WriteLine("=====================================");
+            }
         };
         //SQL报错
         db.Aop.OnError = (exp) =>
         {
             //获取原生SQL推荐 5.1.4.63  性能OK
-            //UtilMethods.GetNativeSql(exp.sql, exp.parameters);
+            //UtilMethods.GetNativeSql(exp.Sql, exp.Parametres);
 
             //获取无参数SQL对性能有影响,特别大的SQL参数多的,调试使用
             //UtilMethods.GetSqlString(DbType.SqlServer, exp.sql, exp.parameters);
 
 
+
+
+
         };
         //修改SQL和参数的值
         db.Aop.OnExecutingChangeSql = (sql, pars) =>