DynamicSearchService.cs 41 KB

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