DynamicSearchService.cs 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. using SqlSugar;
  2. using System.Diagnostics;
  3. using System.DirectoryServices;
  4. using System.Linq;
  5. using System.Linq.Expressions;
  6. namespace OASystem.API.OAMethodLib.GenericSearch
  7. {
  8. /// <summary>
  9. /// 动态检索服务
  10. /// 支持动态字段权重配置、返回字段筛选、智能搜索等功能
  11. /// </summary>
  12. /// <typeparam name="T">实体类型</typeparam>
  13. public class DynamicSearchService<T> where T : class, new()
  14. {
  15. private readonly SqlSugarClient _db;
  16. private readonly ILogger<DynamicSearchService<T>> _logger;
  17. public DynamicSearchService(SqlSugarClient db, ILogger<DynamicSearchService<T>> logger)
  18. {
  19. _db = db;
  20. _logger = logger;
  21. }
  22. /// <summary>
  23. /// 执行动态搜索(应用层统计匹配度)
  24. /// </summary>
  25. /// <param name="request">搜索请求参数</param>
  26. /// <returns>包含搜索结果和匹配度信息的结果对象</returns>
  27. public async Task<SearchResult<T>> SearchAsync(DynamicSearchRequest request)
  28. {
  29. var resultView = new SearchResult<T>() { Success = false, Message = "异常错误" };
  30. var stopwatch = Stopwatch.StartNew();
  31. var searchId = Guid.NewGuid().ToString("N")[..8];
  32. try
  33. {
  34. List<T> data;
  35. int totalCount;
  36. // 使用原生SQL方式构建查询
  37. if (!string.IsNullOrWhiteSpace(request.Keyword))
  38. {
  39. var result = await SearchWithNativeSqlAsync(request);
  40. data = result.Data;
  41. totalCount = result.TotalCount;
  42. }
  43. else
  44. {
  45. // 无关键词时使用简单查询
  46. var query = BuildBaseQuery(request);
  47. totalCount = await query.CountAsync();
  48. data = await query.ToPageListAsync(request.PageIndex, request.PageSize);
  49. }
  50. // 在应用层计算匹配度
  51. var scoredItems = CalculateMatchScore(data, request);
  52. stopwatch.Stop();
  53. return new SearchResult<T>
  54. {
  55. Message = $"搜索成功!耗时:{stopwatch.ElapsedMilliseconds}ms",
  56. Items = scoredItems,
  57. TotalCount = totalCount,
  58. Keyword = request.Keyword,
  59. FieldWeights = request.FieldWeights,
  60. ReturnFields = request.ReturnFields,
  61. PageIndex = request.PageIndex,
  62. PageSize = request.PageSize,
  63. ResponseTime = stopwatch.ElapsedMilliseconds,
  64. SearchId = searchId
  65. };
  66. }
  67. catch (Exception ex)
  68. {
  69. stopwatch.Stop();
  70. resultView.Message = string.Format("【{SearchId}】动态搜索失败: {ErrorMessage}", searchId, ex.Message);
  71. return resultView;
  72. }
  73. }
  74. /// <summary>
  75. /// 轻量级搜索 - 只返回指定字段,提升性能(应用层统计匹配度)
  76. /// </summary>
  77. /// <typeparam name="TResult">返回的结果类型</typeparam>
  78. /// <param name="request">搜索请求参数</param>
  79. /// <param name="selector">字段选择表达式</param>
  80. /// <returns>包含指定字段和匹配度信息的搜索结果</returns>
  81. public async Task<SearchResult<TResult>> LightweightSearchAsync<TResult>(
  82. DynamicSearchRequest request,
  83. Expression<Func<T, TResult>> selector) where TResult : class, new()
  84. {
  85. var stopwatch = Stopwatch.StartNew();
  86. var searchId = Guid.NewGuid().ToString("N")[..8];
  87. _logger.LogInformation("【{SearchId}】开始轻量级搜索: 实体{Entity}, 返回类型{ResultType}",
  88. searchId, typeof(T).Name, typeof(TResult).Name);
  89. try
  90. {
  91. // 构建基础查询
  92. var baseQuery = _db.Queryable<T>();
  93. // 应用过滤条件
  94. baseQuery = ApplyFilters(baseQuery, request.Filters);
  95. // 应用搜索条件
  96. if (!string.IsNullOrWhiteSpace(request.Keyword))
  97. {
  98. var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
  99. if (searchAnalysis.HasSearchContent)
  100. {
  101. var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
  102. var searchConditions = BuildSearchConditions(searchAnalysis, searchFields);
  103. if (searchConditions.Any())
  104. {
  105. baseQuery = baseQuery.Where(searchConditions);
  106. }
  107. }
  108. }
  109. // 应用字段选择 - 在数据库层面进行字段选择
  110. var finalQuery = baseQuery.Select(selector);
  111. // 应用排序
  112. finalQuery = ApplyOrderByForLightweight(finalQuery, request.OrderBy, request.IsDescending);
  113. // 执行查询获取轻量级数据
  114. var totalCount = await finalQuery.CountAsync();
  115. var lightweightData = await finalQuery.ToPageListAsync(request.PageIndex, request.PageSize);
  116. // 为了计算匹配度,需要查询完整的实体数据
  117. List<T> fullDataForScoring;
  118. if (!string.IsNullOrWhiteSpace(request.Keyword))
  119. {
  120. var fullResult = await SearchWithNativeSqlAsync(request);
  121. fullDataForScoring = fullResult.Data;
  122. }
  123. else
  124. {
  125. var fullQuery = BuildBaseQuery(request);
  126. fullDataForScoring = await fullQuery.ToPageListAsync(request.PageIndex, request.PageSize);
  127. }
  128. // 计算匹配度
  129. var scoredItems = CalculateMatchScore(fullDataForScoring, request);
  130. // 将匹配度信息与轻量级数据关联
  131. var lightweightItems = AssociateMatchScores(lightweightData, scoredItems, selector);
  132. stopwatch.Stop();
  133. _logger.LogInformation("【{SearchId}】轻量级搜索完成: 找到 {Count} 条记录, 耗时 {TotalTime}ms",
  134. searchId, lightweightItems.Count, stopwatch.ElapsedMilliseconds);
  135. return new SearchResult<TResult>
  136. {
  137. Items = lightweightItems,
  138. TotalCount = totalCount,
  139. Keyword = request.Keyword,
  140. FieldWeights = request.FieldWeights,
  141. PageIndex = request.PageIndex,
  142. PageSize = request.PageSize,
  143. ResponseTime = stopwatch.ElapsedMilliseconds,
  144. SearchId = searchId
  145. };
  146. }
  147. catch (Exception ex)
  148. {
  149. stopwatch.Stop();
  150. _logger.LogError(ex, "【{SearchId}】轻量级搜索失败", searchId);
  151. throw;
  152. }
  153. }
  154. /// <summary>
  155. /// 将匹配度信息与轻量级数据关联
  156. /// </summary>
  157. private List<SearchResultItem<TResult>> AssociateMatchScores<TResult>(
  158. List<TResult> lightweightData,
  159. List<SearchResultItem<T>> scoredItems,
  160. Expression<Func<T, TResult>> selector) where TResult : class, new()
  161. {
  162. var result = new List<SearchResultItem<TResult>>();
  163. // 构建一个字典来快速查找匹配度信息
  164. var scoreDict = new Dictionary<int, SearchResultItem<T>>();
  165. foreach (var scoredItem in scoredItems)
  166. {
  167. var id = GetEntityId(scoredItem.Data);
  168. if (id > 0)
  169. {
  170. scoreDict[id] = scoredItem;
  171. }
  172. }
  173. // 关联匹配度信息
  174. foreach (var lightItem in lightweightData)
  175. {
  176. var id = GetEntityId(lightItem);
  177. if (id > 0 && scoreDict.TryGetValue(id, out var scoredItem))
  178. {
  179. result.Add(new SearchResultItem<TResult>
  180. {
  181. Data = lightItem,
  182. MatchScore = scoredItem.MatchScore,
  183. MatchFields = scoredItem.MatchFields
  184. });
  185. }
  186. else
  187. {
  188. result.Add(new SearchResultItem<TResult>
  189. {
  190. Data = lightItem,
  191. MatchScore = 0,
  192. MatchFields = new List<MatchFieldInfo>()
  193. });
  194. }
  195. }
  196. return result.OrderByDescending(x => x.MatchScore).ToList();
  197. }
  198. /// <summary>
  199. /// 获取实体ID(通过反射)
  200. /// </summary>
  201. private int GetEntityId<TEntity>(TEntity entity)
  202. {
  203. if (entity == null) return 0;
  204. var idProperty = typeof(TEntity).GetProperty("Id");
  205. if (idProperty != null && idProperty.PropertyType == typeof(int))
  206. {
  207. return (int)(idProperty.GetValue(entity) ?? 0);
  208. }
  209. return 0;
  210. }
  211. /// <summary>
  212. /// 获取实体可搜索字段信息
  213. /// </summary>
  214. /// <returns>可搜索字段信息列表,按权重降序排列</returns>
  215. public List<FieldInfo> GetSearchableFields()
  216. {
  217. var entityType = typeof(T);
  218. var properties = entityType.GetProperties();
  219. var searchableFields = new List<FieldInfo>();
  220. foreach (var prop in properties)
  221. {
  222. var fieldInfo = new FieldInfo
  223. {
  224. FieldName = prop.Name,
  225. DisplayName = GetDisplayName(prop),
  226. DataType = prop.PropertyType.Name,
  227. IsSearchable = prop.PropertyType == typeof(string),
  228. DefaultWeight = GetDefaultWeight(prop.Name),
  229. Description = GetFieldDescription(prop),
  230. CanFilter = true,
  231. CanSort = true
  232. };
  233. searchableFields.Add(fieldInfo);
  234. }
  235. return searchableFields
  236. .OrderByDescending(f => f.DefaultWeight)
  237. .ThenBy(f => f.FieldName)
  238. .ToList();
  239. }
  240. /// <summary>
  241. /// 验证字段配置
  242. /// </summary>
  243. /// <param name="fieldWeights">字段权重配置</param>
  244. /// <param name="returnFields">返回字段列表</param>
  245. /// <returns>验证结果</returns>
  246. public (bool IsValid, string Message) ValidateFieldConfig(
  247. Dictionary<string, int> fieldWeights,
  248. List<string> returnFields)
  249. {
  250. var allFields = GetSearchableFields();
  251. var validFieldNames = allFields.Select(f => f.FieldName).ToList();
  252. // 验证搜索字段
  253. if (fieldWeights != null)
  254. {
  255. var invalidSearchFields = fieldWeights.Keys.Except(validFieldNames).ToList();
  256. if (invalidSearchFields.Any())
  257. {
  258. return (false, $"无效的搜索字段: {string.Join(", ", invalidSearchFields)}");
  259. }
  260. }
  261. // 验证返回字段
  262. if (returnFields != null)
  263. {
  264. var invalidReturnFields = returnFields.Except(validFieldNames).ToList();
  265. if (invalidReturnFields.Any())
  266. {
  267. return (false, $"无效的返回字段: {string.Join(", ", invalidReturnFields)}");
  268. }
  269. }
  270. return (true, "字段配置有效");
  271. }
  272. #region 私有方法 - 搜索逻辑
  273. /// <summary>
  274. /// 使用原生SQL进行搜索
  275. /// </summary>
  276. private async Task<(List<T> Data, int TotalCount)> SearchWithNativeSqlAsync(DynamicSearchRequest request)
  277. {
  278. var whereConditions = new List<string>();
  279. var parameters = new List<SugarParameter>();
  280. // 获取搜索字段
  281. var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
  282. var validFields = ValidateSearchFields(searchFields);
  283. // 构建搜索条件
  284. if (!string.IsNullOrWhiteSpace(request.Keyword))
  285. {
  286. var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
  287. var keywordConditions = new List<string>(); // 专门存放关键字相关条件
  288. // 符号分割的关键字条件
  289. foreach (var segment in searchAnalysis.SymbolSegments)
  290. {
  291. var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  292. if (!string.IsNullOrEmpty(cleanSegment))
  293. {
  294. var fieldConditions = validFields.Select(field =>
  295. {
  296. var paramName = $"@segment{parameters.Count}";
  297. parameters.Add(new SugarParameter(paramName, $"%{cleanSegment}%"));
  298. return $"{field} LIKE {paramName}";
  299. });
  300. // 每个片段内部使用 OR 连接不同字段
  301. keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
  302. }
  303. }
  304. // 单字检索条件
  305. foreach (var singleChar in searchAnalysis.SingleChars)
  306. {
  307. var charStr = singleChar.ToString();
  308. var fieldConditions = validFields.Select(field =>
  309. {
  310. var paramName = $"@char{parameters.Count}";
  311. parameters.Add(new SugarParameter(paramName, $"%{charStr}%"));
  312. return $"{field} LIKE {paramName}";
  313. });
  314. // 每个单字内部使用 OR 连接不同字段
  315. keywordConditions.Add($"({string.Join(" OR ", fieldConditions)})");
  316. }
  317. // 所有关键字条件使用 OR 连接
  318. if (keywordConditions.Any())
  319. {
  320. whereConditions.Add($"({string.Join(" OR ", keywordConditions)})");
  321. }
  322. }
  323. // 构建过滤条件
  324. var filterConditions = BuildNativeFilterConditions(request.Filters, parameters);
  325. whereConditions.AddRange(filterConditions);
  326. // 构建完整SQL
  327. var whereClause = whereConditions.Any()
  328. ? "WHERE " + string.Join(" AND ", whereConditions)
  329. : "";
  330. var orderByClause = BuildNativeOrderByClause(request.OrderBy, request.IsDescending);
  331. var tableName = _db.EntityMaintenance.GetTableName(typeof(T));
  332. // 构建返回字段
  333. //var returnFields = BuildReturnFields(request.ReturnFields);
  334. // 先查询总数
  335. var countSql = $"SELECT COUNT(1) FROM {tableName} {whereClause}";
  336. var totalCount = await _db.Ado.GetIntAsync(countSql, parameters);
  337. // 再查询数据
  338. var offset = (request.PageIndex - 1) * request.PageSize;
  339. var dataSql = $@"
  340. SELECT * FROM (
  341. SELECT *, ROW_NUMBER() OVER ({orderByClause}) AS RowNumber
  342. FROM {tableName}
  343. {whereClause}
  344. ) AS Paginated
  345. WHERE Paginated.RowNumber > {offset} AND Paginated.RowNumber <= {offset + request.PageSize}
  346. {orderByClause}";
  347. var data = await _db.Ado.SqlQueryAsync<T>(dataSql, parameters);
  348. return (data, totalCount);
  349. }
  350. /// <summary>
  351. /// 构建返回字段列表
  352. /// </summary>
  353. private string BuildReturnFields(List<string> returnFields)
  354. {
  355. if (returnFields == null || !returnFields.Any())
  356. {
  357. return "*";
  358. }
  359. // 验证字段是否存在
  360. var validFields = ValidateReturnFields(returnFields);
  361. if (!validFields.Any())
  362. {
  363. return "*";
  364. }
  365. // 构建字段列表,处理可能的SQL注入和字段名转义
  366. var escapedFields = validFields.Select(field =>
  367. {
  368. // 如果字段包含空格、特殊字符或关键字,用方括号括起来
  369. if (field.Contains(" ") || IsSqlKeyword(field) || field.Contains("."))
  370. {
  371. return $"[{field}]";
  372. }
  373. return field;
  374. });
  375. return string.Join(", ", escapedFields);
  376. }
  377. /// <summary>
  378. /// 验证返回字段的有效性
  379. /// </summary>
  380. private List<string> ValidateReturnFields(List<string> returnFields)
  381. {
  382. if (returnFields == null) return new List<string>();
  383. var entityType = typeof(T);
  384. var entityProperties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
  385. .Select(p => p.Name)
  386. .ToList();
  387. // 过滤出实体类中存在的字段
  388. var validFields = returnFields
  389. .Where(field => entityProperties.Contains(field, StringComparer.OrdinalIgnoreCase))
  390. .ToList();
  391. // 记录无效字段警告
  392. var invalidFields = returnFields.Except(validFields, StringComparer.OrdinalIgnoreCase).ToList();
  393. if (invalidFields.Any())
  394. {
  395. _logger.LogWarning("发现无效的返回字段: {InvalidFields}", string.Join(", ", invalidFields));
  396. }
  397. return validFields;
  398. }
  399. /// <summary>
  400. /// 检查是否为SQL关键字
  401. /// </summary>
  402. private bool IsSqlKeyword(string word)
  403. {
  404. var sqlKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
  405. {
  406. "SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP",
  407. "ALTER", "TABLE", "VIEW", "INDEX", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
  408. "AND", "OR", "NOT", "LIKE", "IN", "BETWEEN", "IS", "NULL", "ORDER", "BY",
  409. "GROUP", "HAVING", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "ON", "AS",
  410. "DISTINCT", "TOP", "COUNT", "SUM", "AVG", "MIN", "MAX", "UNION", "ALL"
  411. };
  412. return sqlKeywords.Contains(word);
  413. }
  414. /// <summary>
  415. /// 构建基础查询
  416. /// </summary>
  417. private ISugarQueryable<T> BuildBaseQuery(DynamicSearchRequest request)
  418. {
  419. var query = _db.Queryable<T>();
  420. // 应用过滤条件
  421. query = ApplyFilters(query, request.Filters);
  422. // 应用排序
  423. query = ApplyOrderBy(query, request.OrderBy, request.IsDescending);
  424. return query;
  425. }
  426. /// <summary>
  427. /// 构建原生过滤条件
  428. /// </summary>
  429. private List<string> BuildNativeFilterConditions(List<SearchFilter> filters, List<SugarParameter> parameters)
  430. {
  431. var conditions = new List<string>();
  432. if (filters == null) return conditions;
  433. foreach (var filter in filters)
  434. {
  435. var condition = filter.Operator?.ToLower() switch
  436. {
  437. "eq" => BuildNativeCondition(filter, "=", parameters),
  438. "neq" => BuildNativeCondition(filter, "!=", parameters),
  439. "contains" => BuildNativeLikeCondition(filter, "%", "%", parameters),
  440. "startswith" => BuildNativeLikeCondition(filter, "", "%", parameters),
  441. "endswith" => BuildNativeLikeCondition(filter, "%", "", parameters),
  442. "gt" => BuildNativeCondition(filter, ">", parameters),
  443. "gte" => BuildNativeCondition(filter, ">=", parameters),
  444. "lt" => BuildNativeCondition(filter, "<", parameters),
  445. "lte" => BuildNativeCondition(filter, "<=", parameters),
  446. "in" => BuildNativeInCondition(filter, parameters),
  447. _ => null
  448. };
  449. if (!string.IsNullOrEmpty(condition))
  450. {
  451. conditions.Add(condition);
  452. }
  453. }
  454. return conditions;
  455. }
  456. private string BuildNativeCondition(SearchFilter filter, string op, List<SugarParameter> parameters)
  457. {
  458. var paramName = $"@filter{parameters.Count}";
  459. parameters.Add(new SugarParameter(paramName, filter.Value));
  460. return $"{filter.Field} {op} {paramName}";
  461. }
  462. private string BuildNativeLikeCondition(SearchFilter filter, string prefix, string suffix, List<SugarParameter> parameters)
  463. {
  464. var paramName = $"@filter{parameters.Count}";
  465. parameters.Add(new SugarParameter(paramName, $"{prefix}{filter.Value}{suffix}"));
  466. return $"{filter.Field} LIKE {paramName}";
  467. }
  468. private string BuildNativeInCondition(SearchFilter filter, List<SugarParameter> parameters)
  469. {
  470. if (filter.Values == null || !filter.Values.Any())
  471. return null;
  472. var paramNames = new List<string>();
  473. foreach (var value in filter.Values)
  474. {
  475. var paramName = $"@filter{parameters.Count}";
  476. parameters.Add(new SugarParameter(paramName, value));
  477. paramNames.Add(paramName);
  478. }
  479. return $"{filter.Field} IN ({string.Join(",", paramNames)})";
  480. }
  481. #endregion
  482. #region 私有方法 - 匹配度计算
  483. /// <summary>
  484. /// 在应用层计算匹配度
  485. /// </summary>
  486. private List<SearchResultItem<T>> CalculateMatchScore(List<T> data, DynamicSearchRequest request)
  487. {
  488. if (string.IsNullOrWhiteSpace(request.Keyword))
  489. {
  490. // 无关键词时,所有记录匹配度为0
  491. return data.Select(item => new SearchResultItem<T>
  492. {
  493. Data = item,
  494. MatchScore = 0
  495. }).ToList();
  496. }
  497. var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
  498. var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
  499. var fieldWeights = request.FieldWeights ?? GetDefaultFieldWeights(searchFields);
  500. var scoredItems = data.Select(item =>
  501. {
  502. var matchResult = CalculateItemMatchScore(item, searchAnalysis, searchFields, fieldWeights);
  503. return new SearchResultItem<T>
  504. {
  505. Data = item,
  506. MatchScore = matchResult.TotalScore,
  507. MatchFields = matchResult.MatchFields
  508. };
  509. })
  510. .Where(item => item.MatchScore > 0) // 只保留有匹配的记录
  511. .OrderByDescending(item => item.MatchScore)
  512. .ThenByDescending(item => GetCreateTime(item.Data))
  513. .ToList();
  514. return scoredItems;
  515. }
  516. /// <summary>
  517. /// 计算单个项的匹配度详情
  518. /// </summary>
  519. private (int TotalScore, List<MatchFieldInfo> MatchFields) CalculateItemMatchScore(
  520. T item,
  521. SearchAnalysis analysis,
  522. List<string> searchFields,
  523. Dictionary<string, int> fieldWeights)
  524. {
  525. int totalScore = 0;
  526. var matchFields = new List<MatchFieldInfo>();
  527. foreach (var field in searchFields)
  528. {
  529. var fieldValue = GetFieldValue(item, field);
  530. if (string.IsNullOrEmpty(fieldValue))
  531. continue;
  532. var weight = fieldWeights.ContainsKey(field) ? fieldWeights[field] : GetDefaultWeight(field);
  533. int fieldScore = 0;
  534. var fieldMatchReasons = new List<string>();
  535. // 符号分割关键字匹配
  536. foreach (var segment in analysis.SymbolSegments)
  537. {
  538. var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  539. if (!string.IsNullOrEmpty(cleanSegment) && fieldValue.Contains(cleanSegment))
  540. {
  541. int segmentScore = weight;
  542. if (fieldValue.Equals(cleanSegment))
  543. {
  544. segmentScore += 15;
  545. fieldMatchReasons.Add($"完全匹配 '{cleanSegment}'");
  546. }
  547. else if (fieldValue.StartsWith(cleanSegment))
  548. {
  549. segmentScore += 10;
  550. fieldMatchReasons.Add($"开头匹配 '{cleanSegment}'");
  551. }
  552. else if (fieldValue.EndsWith(cleanSegment))
  553. {
  554. segmentScore += 5;
  555. fieldMatchReasons.Add($"结尾匹配 '{cleanSegment}'");
  556. }
  557. else
  558. {
  559. fieldMatchReasons.Add($"包含 '{cleanSegment}'");
  560. }
  561. fieldScore += segmentScore;
  562. }
  563. }
  564. // 单字匹配
  565. foreach (var singleChar in analysis.SingleChars)
  566. {
  567. int count = fieldValue.Count(c => c == singleChar);
  568. if (count > 0)
  569. {
  570. int charScore = count * (int)(weight * 0.3);
  571. if (fieldValue.StartsWith(singleChar.ToString()))
  572. {
  573. charScore += weight;
  574. fieldMatchReasons.Add($"开头单字 '{singleChar}'");
  575. }
  576. else
  577. {
  578. fieldMatchReasons.Add($"包含单字 '{singleChar}'({count}次)");
  579. }
  580. fieldScore += charScore;
  581. }
  582. }
  583. if (fieldScore > 0)
  584. {
  585. totalScore += fieldScore;
  586. matchFields.Add(new MatchFieldInfo
  587. {
  588. FieldName = field,
  589. FieldValue = GetDisplayFieldValue(fieldValue),
  590. Score = fieldScore,
  591. MatchReason = string.Join("; ", fieldMatchReasons)
  592. });
  593. }
  594. }
  595. // 按分数排序匹配字段
  596. matchFields = matchFields.OrderByDescending(m => m.Score).ToList();
  597. return (totalScore, matchFields);
  598. }
  599. /// <summary>
  600. /// 获取显示用的字段值(截断过长的内容)
  601. /// </summary>
  602. private string GetDisplayFieldValue(string fieldValue)
  603. {
  604. if (string.IsNullOrEmpty(fieldValue))
  605. return fieldValue;
  606. // 如果字段值过长,截断显示
  607. return fieldValue.Length > 50 ? fieldValue.Substring(0, 50) + "..." : fieldValue;
  608. }
  609. #endregion
  610. #region 私有方法 - 辅助功能
  611. /// <summary>
  612. /// 应用过滤条件
  613. /// </summary>
  614. private ISugarQueryable<T> ApplyFilters(ISugarQueryable<T> query, List<SearchFilter> filters)
  615. {
  616. if (filters == null || !filters.Any())
  617. return query;
  618. foreach (var filter in filters)
  619. {
  620. query = filter.Operator?.ToLower() switch
  621. {
  622. "eq" => query.Where($"{filter.Field} = @Value", new { filter.Value }),
  623. "neq" => query.Where($"{filter.Field} != @Value", new { filter.Value }),
  624. "contains" => query.Where($"{filter.Field} LIKE '%' + @Value + '%'", new { filter.Value }),
  625. "startswith" => query.Where($"{filter.Field} LIKE @Value + '%'", new { filter.Value }),
  626. "endswith" => query.Where($"{filter.Field} LIKE '%' + @Value", new { filter.Value }),
  627. "gt" => query.Where($"{filter.Field} > @Value", new { filter.Value }),
  628. "gte" => query.Where($"{filter.Field} >= @Value", new { filter.Value }),
  629. "lt" => query.Where($"{filter.Field} < @Value", new { filter.Value }),
  630. "lte" => query.Where($"{filter.Field} <= @Value", new { filter.Value }),
  631. "in" => ApplyInFilter(query, filter),
  632. _ => query
  633. };
  634. }
  635. return query;
  636. }
  637. /// <summary>
  638. /// 使用SqlSugar条件构建器构建搜索条件
  639. /// </summary>
  640. private List<IConditionalModel> BuildSearchConditions(SearchAnalysis analysis, List<string> searchFields)
  641. {
  642. var conditionalModels = new List<IConditionalModel>();
  643. // 获取有效的搜索字段
  644. var validFields = ValidateSearchFields(searchFields);
  645. if (!validFields.Any())
  646. return conditionalModels;
  647. // 1. 符号分割的关键字条件
  648. foreach (var segment in analysis.SymbolSegments)
  649. {
  650. var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  651. if (!string.IsNullOrEmpty(cleanSegment))
  652. {
  653. var segmentGroup = new List<IConditionalModel>();
  654. foreach (var field in validFields)
  655. {
  656. segmentGroup.Add(new ConditionalModel
  657. {
  658. FieldName = field,
  659. ConditionalType = ConditionalType.Like,
  660. FieldValue = $"%{cleanSegment}%"
  661. });
  662. }
  663. if (segmentGroup.Count > 1)
  664. {
  665. conditionalModels.Add(new ConditionalCollections
  666. {
  667. ConditionalList = new List<KeyValuePair<WhereType, ConditionalModel>>(
  668. segmentGroup.Select((model, index) =>
  669. new KeyValuePair<WhereType, ConditionalModel>(
  670. index == 0 ? WhereType.And : WhereType.Or,
  671. (ConditionalModel)model))
  672. )
  673. });
  674. }
  675. else if (segmentGroup.Count == 1)
  676. {
  677. conditionalModels.Add(segmentGroup[0]);
  678. }
  679. }
  680. }
  681. return conditionalModels;
  682. }
  683. /// <summary>
  684. /// 应用IN过滤条件
  685. /// </summary>
  686. private ISugarQueryable<T> ApplyInFilter(ISugarQueryable<T> query, SearchFilter filter)
  687. {
  688. if (filter.Values == null || !filter.Values.Any())
  689. return query;
  690. var valueList = string.Join(",", filter.Values.Select(v => $"'{v}'"));
  691. return query.Where($"{filter.Field} IN ({valueList})");
  692. }
  693. /// <summary>
  694. /// 应用排序
  695. /// </summary>
  696. private ISugarQueryable<T> ApplyOrderBy(ISugarQueryable<T> query, string orderBy, bool isDescending)
  697. {
  698. if (string.IsNullOrWhiteSpace(orderBy))
  699. {
  700. // 默认按主键或创建时间排序
  701. var entityType = typeof(T);
  702. var idProperty = entityType.GetProperty("Id") ?? entityType.GetProperty("CreateTime");
  703. if (idProperty != null)
  704. {
  705. orderBy = idProperty.Name;
  706. }
  707. }
  708. if (!string.IsNullOrWhiteSpace(orderBy))
  709. {
  710. return isDescending
  711. ? query.OrderBy($"{orderBy} DESC")
  712. : query.OrderBy($"{orderBy} ASC");
  713. }
  714. return query;
  715. }
  716. /// <summary>
  717. /// 为轻量级搜索应用排序
  718. /// </summary>
  719. private ISugarQueryable<TResult> ApplyOrderByForLightweight<TResult>(
  720. ISugarQueryable<TResult> query,
  721. string orderBy,
  722. bool isDescending) where TResult : class, new()
  723. {
  724. if (string.IsNullOrWhiteSpace(orderBy))
  725. {
  726. // 检查结果类型是否有Id或CreateTime字段
  727. var resultType = typeof(TResult);
  728. var idProperty = resultType.GetProperty("Id") ?? resultType.GetProperty("CreateTime");
  729. if (idProperty != null)
  730. {
  731. orderBy = idProperty.Name;
  732. }
  733. else
  734. {
  735. // 如果没有默认排序字段,返回原查询
  736. return query;
  737. }
  738. }
  739. if (!string.IsNullOrWhiteSpace(orderBy))
  740. {
  741. // 验证排序字段是否存在于结果类型中
  742. var resultType = typeof(TResult);
  743. var orderByProperty = resultType.GetProperty(orderBy);
  744. if (orderByProperty != null)
  745. {
  746. return isDescending
  747. ? query.OrderBy($"{orderBy} DESC")
  748. : query.OrderBy($"{orderBy} ASC");
  749. }
  750. else
  751. {
  752. _logger.LogWarning("排序字段 {OrderBy} 在返回类型 {ResultType} 中不存在", orderBy, resultType.Name);
  753. }
  754. }
  755. return query;
  756. }
  757. /// <summary>
  758. /// 构建原生排序子句
  759. /// </summary>
  760. private string BuildNativeOrderByClause(string orderBy, bool isDescending)
  761. {
  762. if (string.IsNullOrWhiteSpace(orderBy))
  763. //return "ORDER BY Id DESC";
  764. return "";
  765. return $"ORDER BY {orderBy} {(isDescending ? "DESC" : "ASC")}";
  766. }
  767. /// <summary>
  768. /// 分析搜索模式
  769. /// </summary>
  770. private SearchAnalysis AnalyzeSearchPattern(string keyword)
  771. {
  772. var analysis = new SearchAnalysis { OriginalKeyword = keyword };
  773. // 检查是否包含特殊符号
  774. var specialSymbols = new[] { ' ', ',', ',', ';', ';', '、', '/', '\\', '|', '-', '_' };
  775. if (keyword.Any(c => specialSymbols.Contains(c)))
  776. {
  777. analysis.HasSpecialSymbols = true;
  778. analysis.SymbolSegments = keyword.Split(specialSymbols, StringSplitOptions.RemoveEmptyEntries)
  779. .Select(s => s.Trim())
  780. .Where(s => !string.IsNullOrEmpty(s))
  781. .ToList();
  782. }
  783. // 清理关键词并提取单字
  784. var cleanKeyword = Regex.Replace(keyword, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  785. if (!string.IsNullOrEmpty(cleanKeyword))
  786. {
  787. foreach (char c in cleanKeyword)
  788. {
  789. analysis.SingleChars.Add(c);
  790. }
  791. analysis.IsSingleChar = true;
  792. // 如果没有特殊符号但有关键词,也作为符号分割段
  793. if (cleanKeyword.Length > 1 && !analysis.HasSpecialSymbols)
  794. {
  795. analysis.SymbolSegments.Add(cleanKeyword);
  796. }
  797. }
  798. // 去重
  799. analysis.SingleChars = analysis.SingleChars.Distinct().ToList();
  800. analysis.SymbolSegments = analysis.SymbolSegments.Distinct().ToList();
  801. return analysis;
  802. }
  803. /// <summary>
  804. /// 获取创建时间(用于排序)
  805. /// </summary>
  806. private DateTime GetCreateTime(T item)
  807. {
  808. var createTimeProperty = typeof(T).GetProperty("CreateTime");
  809. if (createTimeProperty != null)
  810. {
  811. return (DateTime)(createTimeProperty.GetValue(item) ?? DateTime.MinValue);
  812. }
  813. return DateTime.MinValue;
  814. }
  815. /// <summary>
  816. /// 获取字段值
  817. /// </summary>
  818. private string GetFieldValue(T item, string fieldName)
  819. {
  820. var property = typeof(T).GetProperty(fieldName);
  821. return property?.GetValue(item) as string;
  822. }
  823. /// <summary>
  824. /// 获取默认搜索字段
  825. /// </summary>
  826. private List<string> GetDefaultSearchFields()
  827. {
  828. return typeof(T).GetProperties()
  829. .Where(p => p.PropertyType == typeof(string))
  830. .Select(p => p.Name)
  831. .Take(5)
  832. .ToList();
  833. }
  834. /// <summary>
  835. /// 获取默认字段权重
  836. /// </summary>
  837. private Dictionary<string, int> GetDefaultFieldWeights(List<string> fields)
  838. {
  839. var weights = new Dictionary<string, int>();
  840. foreach (var field in fields)
  841. {
  842. weights[field] = GetDefaultWeight(field);
  843. }
  844. return weights;
  845. }
  846. /// <summary>
  847. /// 验证字段有效性
  848. /// </summary>
  849. private List<string> ValidateFields(List<string> fields)
  850. {
  851. var validFields = typeof(T).GetProperties()
  852. .Select(p => p.Name)
  853. .ToList();
  854. return fields.Intersect(validFields).ToList();
  855. }
  856. /// <summary>
  857. /// 验证搜索字段
  858. /// </summary>
  859. private List<string> ValidateSearchFields(List<string> searchFields)
  860. {
  861. var validFields = typeof(T).GetProperties()
  862. .Where(p => p.PropertyType == typeof(string))
  863. .Select(p => p.Name)
  864. .ToList();
  865. return searchFields.Intersect(validFields).ToList();
  866. }
  867. /// <summary>
  868. /// 获取默认权重
  869. /// </summary>
  870. private int GetDefaultWeight(string fieldName)
  871. {
  872. return fieldName.ToLower() switch
  873. {
  874. var name when name.Contains("name") => 10,
  875. var name when name.Contains("title") => 10,
  876. var name when name.Contains("code") => 8,
  877. var name when name.Contains("no") => 8,
  878. var name when name.Contains("desc") => 6,
  879. var name when name.Contains("content") => 6,
  880. var name when name.Contains("remark") => 5,
  881. var name when name.Contains("phone") => 4,
  882. var name when name.Contains("tel") => 4,
  883. var name when name.Contains("address") => 3,
  884. var name when name.Contains("email") => 3,
  885. _ => 5
  886. };
  887. }
  888. /// <summary>
  889. /// 获取显示名称
  890. /// </summary>
  891. private string GetDisplayName(System.Reflection.PropertyInfo property)
  892. {
  893. var displayAttr = property.GetCustomAttribute<System.ComponentModel.DataAnnotations.DisplayAttribute>();
  894. return displayAttr?.Name ?? property.Name;
  895. }
  896. /// <summary>
  897. /// 获取字段描述
  898. /// </summary>
  899. private string GetFieldDescription(System.Reflection.PropertyInfo property)
  900. {
  901. var descriptionAttr = property.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
  902. return descriptionAttr?.Description ?? string.Empty;
  903. }
  904. #endregion
  905. }
  906. }