DynamicSearchService.cs 36 KB

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