DynamicSearchService.cs 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455
  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 (int TotalScore, List<MatchFieldInfo> MatchFields) CalculateItemMatchScore(
  463. T item,
  464. SearchAnalysis analysis,
  465. List<string> searchFields,
  466. Dictionary<string, int> fieldWeights,
  467. bool requireAllSingleChars)
  468. {
  469. int totalScore = 0;
  470. var matchFields = new List<MatchFieldInfo>();
  471. if (requireAllSingleChars && analysis.SingleChars.Any())
  472. {
  473. bool allSingleCharsExist = CheckAllSingleCharsExist(item, analysis.SingleChars, searchFields);
  474. if (!allSingleCharsExist)
  475. {
  476. return (0, matchFields);
  477. }
  478. }
  479. foreach (var field in searchFields)
  480. {
  481. var fieldValue = GetFieldValue(item, field);
  482. if (string.IsNullOrEmpty(fieldValue))
  483. continue;
  484. var weight = fieldWeights.ContainsKey(field) ? fieldWeights[field] : GetDefaultWeight(field);
  485. int fieldScore = 0;
  486. var fieldMatchReasons = new List<string>();
  487. // ========== 1. 符号分割关键字匹配 ==========
  488. foreach (var segment in analysis.SymbolSegments)
  489. {
  490. var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  491. if (string.IsNullOrEmpty(cleanSegment))
  492. continue;
  493. // ========== ★ 完全匹配:绝对优先 ==========
  494. if (fieldValue.Equals(cleanSegment))
  495. {
  496. int segmentScore = weight * 50; // 超级权重,确保绝对第一
  497. fieldScore += segmentScore;
  498. fieldMatchReasons.Add($"★★★ 完全匹配 '{cleanSegment}' +{segmentScore}");
  499. continue;
  500. }
  501. // 检查是否包含完整关键字
  502. if (fieldValue.Contains(cleanSegment))
  503. {
  504. int segmentScore = weight;
  505. if (fieldValue.StartsWith(cleanSegment))
  506. {
  507. segmentScore += weight * 2;
  508. fieldMatchReasons.Add($"开头匹配 '{cleanSegment}' +{segmentScore}");
  509. }
  510. else if (fieldValue.EndsWith(cleanSegment))
  511. {
  512. segmentScore += weight;
  513. fieldMatchReasons.Add($"结尾匹配 '{cleanSegment}' +{segmentScore}");
  514. }
  515. else
  516. {
  517. // 包含匹配:大幅降低得分,避免干扰完全匹配
  518. segmentScore = (int)(weight * 0.2);
  519. // 只有连续出现多次才稍微加分
  520. int consecutiveCount = CountConsecutiveOccurrences(fieldValue, cleanSegment);
  521. if (consecutiveCount > 1)
  522. {
  523. segmentScore += (int)(weight * 0.1 * consecutiveCount);
  524. fieldMatchReasons.Add($"连续出现 {consecutiveCount} 次 +{segmentScore}");
  525. }
  526. else
  527. {
  528. fieldMatchReasons.Add($"包含 '{cleanSegment}' +{segmentScore}");
  529. }
  530. }
  531. fieldScore += segmentScore;
  532. }
  533. else
  534. {
  535. // ========== 按顺序匹配:大幅降低权重 ==========
  536. var orderedResult = CheckOrderedMatchWithConsecutive(fieldValue, cleanSegment);
  537. if (orderedResult.IsMatch && orderedResult.MatchedCount >= 2)
  538. {
  539. // 只有匹配率达到一定阈值才给分
  540. double matchRate = (double)orderedResult.MatchedCount / cleanSegment.Length;
  541. // 如果匹配率低于70%,几乎不给分
  542. if (matchRate < 0.7)
  543. {
  544. continue;
  545. }
  546. int orderedScore = (int)(weight * 0.1); // 极低基础分
  547. // 只有完全连续匹配且匹配率高才加分
  548. if (orderedResult.ConsecutiveLength >= cleanSegment.Length * 0.8)
  549. {
  550. orderedScore += (int)(weight * 0.3);
  551. fieldMatchReasons.Add($"连续匹配 {orderedResult.ConsecutiveLength} 个字符 +{orderedScore}");
  552. }
  553. fieldMatchReasons.Add($"按顺序匹配 '{cleanSegment}' ({orderedResult.MatchedCount}/{cleanSegment.Length}) +{orderedScore}");
  554. fieldScore += orderedScore;
  555. }
  556. }
  557. }
  558. // ========== 2. 单字匹配:大幅降低权重 ==========
  559. if (analysis.SingleChars.Any())
  560. {
  561. // 单字连续匹配检测:降低权重
  562. string singleCharString = new string(analysis.SingleChars.ToArray());
  563. int maxConsecutiveSingle = 0;
  564. string bestConsecutiveMatch = "";
  565. for (int i = 0; i < singleCharString.Length; i++)
  566. {
  567. for (int j = i + 1; j <= singleCharString.Length; j++)
  568. {
  569. string subPattern = singleCharString.Substring(i, j - i);
  570. if (subPattern.Length >= 2 && fieldValue.Contains(subPattern))
  571. {
  572. if (subPattern.Length > maxConsecutiveSingle)
  573. {
  574. maxConsecutiveSingle = subPattern.Length;
  575. bestConsecutiveMatch = subPattern;
  576. }
  577. }
  578. }
  579. }
  580. // 连续单字匹配加分(降低权重)
  581. if (maxConsecutiveSingle >= 3) // 至少3个连续单字才加分
  582. {
  583. int consecutiveSingleBonus = (int)(weight * 0.2 * maxConsecutiveSingle);
  584. fieldScore += consecutiveSingleBonus;
  585. fieldMatchReasons.Add($"连续单字匹配 '{bestConsecutiveMatch}'({maxConsecutiveSingle}个) +{consecutiveSingleBonus}");
  586. }
  587. // 原有单字匹配逻辑(大幅降低权重)
  588. foreach (var singleChar in analysis.SingleChars)
  589. {
  590. int count = fieldValue.Count(c => c == singleChar);
  591. if (count > 0)
  592. {
  593. int charScore = count * (int)(weight * 0.1); // 从0.35降到0.1
  594. var positionBonus = CalculateSingleCharPositionBonus(fieldValue, singleChar, weight);
  595. charScore += (int)(positionBonus.Bonus * 0.3); // 降低位置权重
  596. if (fieldValue.StartsWith(singleChar.ToString()))
  597. {
  598. charScore += (int)(weight * 0.2);
  599. fieldMatchReasons.Add($"开头单字 '{singleChar}' +{charScore}");
  600. }
  601. fieldMatchReasons.Add($"包含单字 '{singleChar}'({count}次)");
  602. fieldScore += charScore;
  603. }
  604. }
  605. }
  606. // ========== 3. 完整关键词匹配 ==========
  607. string fullKeyword = string.Join("", analysis.SymbolSegments)
  608. .Replace(" ", "")
  609. .Replace(",", "")
  610. .Replace(",", "")
  611. .Replace("、", "")
  612. .Replace(";", "");
  613. if (!string.IsNullOrEmpty(fullKeyword) && fullKeyword.Length >= 3)
  614. {
  615. if (fieldValue.Contains(fullKeyword))
  616. {
  617. int fullMatchScore = weight * 3;
  618. fieldScore += fullMatchScore;
  619. fieldMatchReasons.Add($"完整关键词匹配 '{fullKeyword}' +{fullMatchScore}");
  620. }
  621. }
  622. if (fieldScore > 0)
  623. {
  624. totalScore += fieldScore;
  625. matchFields.Add(new MatchFieldInfo
  626. {
  627. FieldName = field,
  628. FieldValue = GetDisplayFieldValue(fieldValue),
  629. Score = fieldScore,
  630. MatchReason = string.Join("; ", fieldMatchReasons)
  631. });
  632. }
  633. }
  634. matchFields = matchFields.OrderByDescending(m => m.Score).ToList();
  635. return (totalScore, matchFields);
  636. }
  637. /// <summary>
  638. /// 检查按顺序匹配(含连续匹配信息)
  639. /// </summary>
  640. private OrderedMatchResult CheckOrderedMatchWithConsecutive(string fieldValue, string searchText)
  641. {
  642. var result = new OrderedMatchResult { IsMatch = false };
  643. if (string.IsNullOrEmpty(fieldValue) || string.IsNullOrEmpty(searchText))
  644. return result;
  645. int searchIndex = 0;
  646. int fieldIndex = 0;
  647. var matchedPositions = new List<int>();
  648. int consecutiveCount = 0;
  649. int maxConsecutive = 0;
  650. while (searchIndex < searchText.Length && fieldIndex < fieldValue.Length)
  651. {
  652. if (searchText[searchIndex] == fieldValue[fieldIndex])
  653. {
  654. matchedPositions.Add(fieldIndex);
  655. searchIndex++;
  656. consecutiveCount++;
  657. maxConsecutive = Math.Max(maxConsecutive, consecutiveCount);
  658. }
  659. else
  660. {
  661. consecutiveCount = 0;
  662. }
  663. fieldIndex++;
  664. }
  665. if (searchIndex >= 2)
  666. {
  667. result.IsMatch = true;
  668. result.MatchedCount = searchIndex;
  669. result.ConsecutiveLength = maxConsecutive;
  670. result.StartPosition = matchedPositions.Any() ? matchedPositions.First() : -1;
  671. // 计算平均间隔
  672. if (matchedPositions.Count > 1)
  673. {
  674. int totalGap = 0;
  675. for (int i = 1; i < matchedPositions.Count; i++)
  676. {
  677. totalGap += matchedPositions[i] - matchedPositions[i - 1] - 1;
  678. }
  679. result.AverageGap = totalGap / (matchedPositions.Count - 1);
  680. }
  681. else
  682. {
  683. result.AverageGap = 0;
  684. }
  685. }
  686. return result;
  687. }
  688. /// <summary>
  689. /// 计算连续出现次数
  690. /// </summary>
  691. private int CountConsecutiveOccurrences(string text, string pattern)
  692. {
  693. if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(pattern))
  694. return 0;
  695. int count = 0;
  696. int index = 0;
  697. while ((index = text.IndexOf(pattern, index, StringComparison.Ordinal)) != -1)
  698. {
  699. count++;
  700. index += pattern.Length;
  701. // 检查是否紧接着再次出现
  702. if (index < text.Length && text.IndexOf(pattern, index, StringComparison.Ordinal) == index)
  703. {
  704. continue;
  705. }
  706. break;
  707. }
  708. return count;
  709. }
  710. /// <summary>
  711. /// 计算按顺序匹配的分数
  712. /// </summary>
  713. private (int Score, List<string> Reasons) CalculateOrderedMatchScore(
  714. string fieldValue,
  715. string searchText,
  716. int weight)
  717. {
  718. var reasons = new List<string>();
  719. int score = 0;
  720. if (string.IsNullOrEmpty(fieldValue) || string.IsNullOrEmpty(searchText))
  721. return (0, reasons);
  722. // 使用双指针检查是否按顺序出现
  723. var orderedMatchResult = CheckOrderedMatch(fieldValue, searchText);
  724. if (orderedMatchResult.IsMatch)
  725. {
  726. // 基础匹配分
  727. int baseScore = weight / 2;
  728. // 匹配率加分
  729. double matchRate = (double)orderedMatchResult.MatchedCount / searchText.Length;
  730. if (matchRate >= 0.9)
  731. {
  732. int matchRateBonus = (int)(weight * 0.3);
  733. baseScore += matchRateBonus;
  734. reasons.Add($"按顺序匹配 '{searchText}' (匹配率{matchRate:P0}) +{matchRateBonus}");
  735. }
  736. else
  737. {
  738. reasons.Add($"按顺序匹配 '{searchText}' (匹配率{matchRate:P0})");
  739. }
  740. // 连续匹配加分(匹配的字符在字段值中连续出现)
  741. if (orderedMatchResult.ConsecutiveLength >= 2)
  742. {
  743. int consecutiveBonus = (int)(weight * 0.2 * orderedMatchResult.ConsecutiveLength);
  744. baseScore += consecutiveBonus;
  745. reasons.Add($"连续匹配 {orderedMatchResult.ConsecutiveLength} 个字符 +{consecutiveBonus}");
  746. }
  747. // 开头匹配加分
  748. if (orderedMatchResult.StartPosition == 0)
  749. {
  750. int startBonus = weight / 3;
  751. baseScore += startBonus;
  752. reasons.Add($"开头匹配 +{startBonus}");
  753. }
  754. // 完全匹配(搜索词完全包含在字段值中且顺序一致)
  755. if (orderedMatchResult.MatchedCount == searchText.Length &&
  756. orderedMatchResult.ConsecutiveLength >= searchText.Length)
  757. {
  758. int fullMatchBonus = weight * 2;
  759. baseScore += fullMatchBonus;
  760. reasons.Add($"完全连续匹配 +{fullMatchBonus}");
  761. }
  762. score += baseScore;
  763. }
  764. else
  765. {
  766. // 部分匹配:搜索词的部分字符按顺序出现
  767. var partialMatch = FindLongestOrderedSubsequence(fieldValue, searchText);
  768. if (partialMatch.Length >= 2)
  769. {
  770. int partialScore = (int)(weight * 0.3 * (partialMatch.Length / (double)searchText.Length));
  771. score += partialScore;
  772. reasons.Add($"部分按顺序匹配 '{partialMatch}' ({partialMatch.Length}/{searchText.Length}) +{partialScore}");
  773. }
  774. }
  775. return (score, reasons);
  776. }
  777. /// <summary>
  778. /// 检查是否按顺序匹配
  779. /// </summary>
  780. private OrderedMatchResult CheckOrderedMatch(string fieldValue, string searchText)
  781. {
  782. var result = new OrderedMatchResult { IsMatch = false };
  783. // 使用双指针检查是否按顺序出现
  784. int searchIndex = 0;
  785. int fieldIndex = 0;
  786. var matchedPositions = new List<int>();
  787. int consecutiveCount = 0;
  788. int maxConsecutive = 0;
  789. while (searchIndex < searchText.Length && fieldIndex < fieldValue.Length)
  790. {
  791. if (searchText[searchIndex] == fieldValue[fieldIndex])
  792. {
  793. matchedPositions.Add(fieldIndex);
  794. searchIndex++;
  795. consecutiveCount++;
  796. maxConsecutive = Math.Max(maxConsecutive, consecutiveCount);
  797. }
  798. else
  799. {
  800. consecutiveCount = 0;
  801. }
  802. fieldIndex++;
  803. }
  804. // 如果至少匹配了2个字符
  805. if (searchIndex >= 2)
  806. {
  807. result.IsMatch = true;
  808. result.MatchedCount = searchIndex;
  809. result.ConsecutiveLength = maxConsecutive;
  810. result.StartPosition = matchedPositions.Any() ? matchedPositions.First() : -1;
  811. // 计算平均间隔
  812. if (matchedPositions.Count > 1)
  813. {
  814. int totalGap = 0;
  815. for (int i = 1; i < matchedPositions.Count; i++)
  816. {
  817. totalGap += matchedPositions[i] - matchedPositions[i - 1] - 1;
  818. }
  819. result.AverageGap = totalGap / (matchedPositions.Count - 1);
  820. }
  821. else
  822. {
  823. result.AverageGap = 0;
  824. }
  825. }
  826. return result;
  827. }
  828. /// <summary>
  829. /// 查找最长有序子序列(LCS算法)
  830. /// </summary>
  831. private string FindLongestOrderedSubsequence(string fieldValue, string searchText)
  832. {
  833. if (string.IsNullOrEmpty(fieldValue) || string.IsNullOrEmpty(searchText))
  834. return "";
  835. // 动态规划求最长公共子序列
  836. int m = fieldValue.Length;
  837. int n = searchText.Length;
  838. var dp = new int[m + 1, n + 1];
  839. for (int i = 1; i <= m; i++)
  840. {
  841. for (int j = 1; j <= n; j++)
  842. {
  843. if (fieldValue[i - 1] == searchText[j - 1])
  844. dp[i, j] = dp[i - 1, j - 1] + 1;
  845. else
  846. dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]);
  847. }
  848. }
  849. // 回溯获取最长公共子序列
  850. int length = dp[m, n];
  851. if (length < 2) return "";
  852. var result = new char[length];
  853. int idx = length - 1;
  854. int x = m, y = n;
  855. while (x > 0 && y > 0)
  856. {
  857. if (fieldValue[x - 1] == searchText[y - 1])
  858. {
  859. result[idx--] = fieldValue[x - 1];
  860. x--;
  861. y--;
  862. }
  863. else if (dp[x - 1, y] > dp[x, y - 1])
  864. {
  865. x--;
  866. }
  867. else
  868. {
  869. y--;
  870. }
  871. }
  872. return new string(result);
  873. }
  874. /// <summary>
  875. /// 按顺序匹配结果
  876. /// </summary>
  877. private class OrderedMatchResult
  878. {
  879. public bool IsMatch { get; set; }
  880. public int MatchedCount { get; set; }
  881. public int ConsecutiveLength { get; set; }
  882. public int StartPosition { get; set; }
  883. public int AverageGap { get; set; }
  884. }
  885. /// <summary>
  886. /// 在应用层计算匹配度
  887. /// </summary>
  888. private List<SearchResultItem<T>> CalculateMatchScore(List<T> data, DynamicSearchRequest request)
  889. {
  890. if (string.IsNullOrWhiteSpace(request.Keyword))
  891. {
  892. // 无关键词时,所有记录匹配度为0
  893. return data.Select(item => new SearchResultItem<T>
  894. {
  895. Data = item,
  896. MatchScore = 0
  897. }).ToList();
  898. }
  899. var searchAnalysis = AnalyzeSearchPattern(request.Keyword);
  900. var searchFields = request.FieldWeights?.Keys.ToList() ?? GetDefaultSearchFields();
  901. var fieldWeights = request.FieldWeights ?? GetDefaultFieldWeights(searchFields);
  902. var scoredItems = data.Select(item =>
  903. {
  904. var matchResult = CalculateItemMatchScore(item, searchAnalysis, searchFields, fieldWeights, request.RequireAllSingleChars);
  905. return new SearchResultItem<T>
  906. {
  907. Data = item,
  908. MatchScore = matchResult.TotalScore,
  909. MatchFields = matchResult.MatchFields
  910. };
  911. })
  912. .Where(item => item.MatchScore > 0) // 只保留有匹配的记录
  913. .OrderByDescending(item => item.MatchScore)
  914. .ThenByDescending(item => GetCreateTime(item.Data))
  915. .ToList();
  916. return scoredItems;
  917. }
  918. /// <summary>
  919. /// 新增:检查所有单字是否在任意字段中出现
  920. /// </summary>
  921. private bool CheckAllSingleCharsExist(T item, List<char> singleChars, List<string> searchFields)
  922. {
  923. foreach (var singleChar in singleChars)
  924. {
  925. bool charExists = false;
  926. // 检查所有搜索字段中是否包含该单字
  927. foreach (var field in searchFields)
  928. {
  929. var fieldValue = GetFieldValue(item, field);
  930. if (!string.IsNullOrEmpty(fieldValue) && fieldValue.Contains(singleChar))
  931. {
  932. charExists = true;
  933. break;
  934. }
  935. }
  936. // 如果有一个单字不存在,直接返回false
  937. if (!charExists)
  938. {
  939. return false;
  940. }
  941. }
  942. return true;
  943. }
  944. /// <summary>
  945. /// 计算单字位置权重奖励
  946. /// </summary>
  947. private (int Bonus, int FirstPosition) CalculateSingleCharPositionBonus(string fieldValue, char singleChar, int baseWeight)
  948. {
  949. var firstPosition = fieldValue.IndexOf(singleChar);
  950. if (firstPosition == -1)
  951. return (0, -1);
  952. // 计算位置比例 (0-1),0表示开头,1表示结尾
  953. double positionRatio = (double)firstPosition / Math.Max(fieldValue.Length - 1, 1);
  954. // 位置权重系数:位置越靠前,奖励越高
  955. double positionFactor = 1.0 - positionRatio;
  956. // 计算奖励分数(基于基础权重)
  957. int positionBonus = (int)(baseWeight * positionFactor * 0.8); // 最大奖励为基础权重的80%
  958. // 确保奖励至少为1
  959. positionBonus = Math.Max(positionBonus, 1);
  960. return (positionBonus, firstPosition);
  961. }
  962. /// <summary>
  963. /// 获取显示字段值(截断过长的值)
  964. /// </summary>
  965. private string GetDisplayFieldValue(string fieldValue, int maxLength = 50)
  966. {
  967. if (string.IsNullOrEmpty(fieldValue) || fieldValue.Length <= maxLength)
  968. return fieldValue;
  969. return fieldValue.Substring(0, maxLength) + "...";
  970. }
  971. #endregion
  972. #region 私有方法 - 辅助功能
  973. /// <summary>
  974. /// 应用过滤条件
  975. /// </summary>
  976. private ISugarQueryable<T> ApplyFilters(ISugarQueryable<T> query, List<SearchFilter> filters)
  977. {
  978. if (filters == null || !filters.Any())
  979. return query;
  980. foreach (var filter in filters)
  981. {
  982. query = filter.Operator?.ToLower() switch
  983. {
  984. "eq" => query.Where($"{filter.Field} = @Value", new { filter.Value }),
  985. "neq" => query.Where($"{filter.Field} != @Value", new { filter.Value }),
  986. "contains" => query.Where($"{filter.Field} LIKE '%' + @Value + '%'", new { filter.Value }),
  987. "startswith" => query.Where($"{filter.Field} LIKE @Value + '%'", new { filter.Value }),
  988. "endswith" => query.Where($"{filter.Field} LIKE '%' + @Value", new { filter.Value }),
  989. "gt" => query.Where($"{filter.Field} > @Value", new { filter.Value }),
  990. "gte" => query.Where($"{filter.Field} >= @Value", new { filter.Value }),
  991. "lt" => query.Where($"{filter.Field} < @Value", new { filter.Value }),
  992. "lte" => query.Where($"{filter.Field} <= @Value", new { filter.Value }),
  993. "in" => ApplyInFilter(query, filter),
  994. _ => query
  995. };
  996. }
  997. return query;
  998. }
  999. /// <summary>
  1000. /// 使用SqlSugar条件构建器构建搜索条件
  1001. /// </summary>
  1002. private List<IConditionalModel> BuildSearchConditions(SearchAnalysis analysis, List<string> searchFields)
  1003. {
  1004. var conditionalModels = new List<IConditionalModel>();
  1005. // 获取有效的搜索字段
  1006. var validFields = ValidateSearchFields(searchFields);
  1007. if (!validFields.Any())
  1008. return conditionalModels;
  1009. // 1. 符号分割的关键字条件
  1010. foreach (var segment in analysis.SymbolSegments)
  1011. {
  1012. var cleanSegment = Regex.Replace(segment, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  1013. if (!string.IsNullOrEmpty(cleanSegment))
  1014. {
  1015. var segmentGroup = new List<IConditionalModel>();
  1016. foreach (var field in validFields)
  1017. {
  1018. segmentGroup.Add(new ConditionalModel
  1019. {
  1020. FieldName = field,
  1021. ConditionalType = ConditionalType.Like,
  1022. FieldValue = $"%{cleanSegment}%"
  1023. });
  1024. }
  1025. if (segmentGroup.Count > 1)
  1026. {
  1027. conditionalModels.Add(new ConditionalCollections
  1028. {
  1029. ConditionalList = new List<KeyValuePair<WhereType, ConditionalModel>>(
  1030. segmentGroup.Select((model, index) =>
  1031. new KeyValuePair<WhereType, ConditionalModel>(
  1032. index == 0 ? WhereType.And : WhereType.Or,
  1033. (ConditionalModel)model))
  1034. )
  1035. });
  1036. }
  1037. else if (segmentGroup.Count == 1)
  1038. {
  1039. conditionalModels.Add(segmentGroup[0]);
  1040. }
  1041. }
  1042. }
  1043. return conditionalModels;
  1044. }
  1045. /// <summary>
  1046. /// 应用IN过滤条件
  1047. /// </summary>
  1048. private ISugarQueryable<T> ApplyInFilter(ISugarQueryable<T> query, SearchFilter filter)
  1049. {
  1050. if (filter.Values == null || !filter.Values.Any())
  1051. return query;
  1052. var valueList = string.Join(",", filter.Values.Select(v => $"'{v}'"));
  1053. return query.Where($"{filter.Field} IN ({valueList})");
  1054. }
  1055. /// <summary>
  1056. /// 应用排序
  1057. /// </summary>
  1058. private ISugarQueryable<T> ApplyOrderBy(ISugarQueryable<T> query, string orderBy, bool isDescending)
  1059. {
  1060. if (string.IsNullOrWhiteSpace(orderBy))
  1061. {
  1062. // 默认按主键或创建时间排序
  1063. var entityType = typeof(T);
  1064. var idProperty = entityType.GetProperty("Id") ?? entityType.GetProperty("CreateTime");
  1065. if (idProperty != null)
  1066. {
  1067. orderBy = idProperty.Name;
  1068. }
  1069. }
  1070. if (!string.IsNullOrWhiteSpace(orderBy))
  1071. {
  1072. return isDescending
  1073. ? query.OrderBy($"{orderBy} DESC")
  1074. : query.OrderBy($"{orderBy} ASC");
  1075. }
  1076. return query;
  1077. }
  1078. /// <summary>
  1079. /// 为轻量级搜索应用排序
  1080. /// </summary>
  1081. private ISugarQueryable<TResult> ApplyOrderByForLightweight<TResult>(
  1082. ISugarQueryable<TResult> query,
  1083. string orderBy,
  1084. bool isDescending) where TResult : class, new()
  1085. {
  1086. if (string.IsNullOrWhiteSpace(orderBy))
  1087. {
  1088. // 检查结果类型是否有Id或CreateTime字段
  1089. var resultType = typeof(TResult);
  1090. var idProperty = resultType.GetProperty("Id") ?? resultType.GetProperty("CreateTime");
  1091. if (idProperty != null)
  1092. {
  1093. orderBy = idProperty.Name;
  1094. }
  1095. else
  1096. {
  1097. // 如果没有默认排序字段,返回原查询
  1098. return query;
  1099. }
  1100. }
  1101. if (!string.IsNullOrWhiteSpace(orderBy))
  1102. {
  1103. // 验证排序字段是否存在于结果类型中
  1104. var resultType = typeof(TResult);
  1105. var orderByProperty = resultType.GetProperty(orderBy);
  1106. if (orderByProperty != null)
  1107. {
  1108. return isDescending
  1109. ? query.OrderBy($"{orderBy} DESC")
  1110. : query.OrderBy($"{orderBy} ASC");
  1111. }
  1112. else
  1113. {
  1114. _logger.LogWarning("排序字段 {OrderBy} 在返回类型 {ResultType} 中不存在", orderBy, resultType.Name);
  1115. }
  1116. }
  1117. return query;
  1118. }
  1119. /// <summary>
  1120. /// 构建原生排序子句
  1121. /// </summary>
  1122. private string BuildNativeOrderByClause(string orderBy, bool isDescending)
  1123. {
  1124. if (string.IsNullOrWhiteSpace(orderBy))
  1125. //return "ORDER BY Id DESC";
  1126. return "";
  1127. return $"ORDER BY {orderBy} {(isDescending ? "DESC" : "ASC")}";
  1128. }
  1129. /// <summary>
  1130. /// 分析搜索模式
  1131. /// </summary>
  1132. private SearchAnalysis AnalyzeSearchPattern(string keyword)
  1133. {
  1134. var analysis = new SearchAnalysis { OriginalKeyword = keyword };
  1135. // 检查是否包含特殊符号
  1136. var specialSymbols = new[] { ' ', ',', ',', ';', ';', '、', '/', '\\', '|', '-', '_' };
  1137. if (keyword.Any(c => specialSymbols.Contains(c)))
  1138. {
  1139. analysis.HasSpecialSymbols = true;
  1140. analysis.SymbolSegments = keyword.Split(specialSymbols, StringSplitOptions.RemoveEmptyEntries)
  1141. .Select(s => s.Trim())
  1142. .Where(s => !string.IsNullOrEmpty(s))
  1143. .ToList();
  1144. }
  1145. // 清理关键词并提取单字
  1146. var cleanKeyword = Regex.Replace(keyword, @"[^\u4e00-\u9fa5a-zA-Z0-9]", "");
  1147. if (!string.IsNullOrEmpty(cleanKeyword))
  1148. {
  1149. foreach (char c in cleanKeyword)
  1150. {
  1151. analysis.SingleChars.Add(c);
  1152. }
  1153. analysis.IsSingleChar = true;
  1154. // 如果没有特殊符号但有关键词,也作为符号分割段
  1155. if (cleanKeyword.Length > 1 && !analysis.HasSpecialSymbols)
  1156. {
  1157. analysis.SymbolSegments.Add(cleanKeyword);
  1158. }
  1159. }
  1160. // 去重
  1161. analysis.SingleChars = analysis.SingleChars.Distinct().ToList();
  1162. analysis.SymbolSegments = analysis.SymbolSegments.Distinct().ToList();
  1163. return analysis;
  1164. }
  1165. /// <summary>
  1166. /// 获取创建时间(用于排序)
  1167. /// </summary>
  1168. private DateTime GetCreateTime(T item)
  1169. {
  1170. var createTimeProperty = typeof(T).GetProperty("CreateTime");
  1171. if (createTimeProperty != null)
  1172. {
  1173. return (DateTime)(createTimeProperty.GetValue(item) ?? DateTime.MinValue);
  1174. }
  1175. return DateTime.MinValue;
  1176. }
  1177. /// <summary>
  1178. /// 获取字段值
  1179. /// </summary>
  1180. private string GetFieldValue(T item, string fieldName)
  1181. {
  1182. var property = typeof(T).GetProperty(fieldName);
  1183. return property?.GetValue(item) as string;
  1184. }
  1185. /// <summary>
  1186. /// 获取默认搜索字段
  1187. /// </summary>
  1188. private List<string> GetDefaultSearchFields()
  1189. {
  1190. return typeof(T).GetProperties()
  1191. .Where(p => p.PropertyType == typeof(string))
  1192. .Select(p => p.Name)
  1193. .Take(5)
  1194. .ToList();
  1195. }
  1196. /// <summary>
  1197. /// 获取默认字段权重
  1198. /// </summary>
  1199. private Dictionary<string, int> GetDefaultFieldWeights(List<string> fields)
  1200. {
  1201. var weights = new Dictionary<string, int>();
  1202. foreach (var field in fields)
  1203. {
  1204. weights[field] = GetDefaultWeight(field);
  1205. }
  1206. return weights;
  1207. }
  1208. /// <summary>
  1209. /// 验证搜索字段
  1210. /// </summary>
  1211. private List<string> ValidateSearchFields(List<string> searchFields)
  1212. {
  1213. var validFields = typeof(T).GetProperties()
  1214. .Where(p => p.PropertyType == typeof(string))
  1215. .Select(p => p.Name)
  1216. .ToList();
  1217. return searchFields.Intersect(validFields).ToList();
  1218. }
  1219. /// <summary>
  1220. /// 获取默认权重
  1221. /// </summary>
  1222. private int GetDefaultWeight(string fieldName)
  1223. {
  1224. return fieldName.ToLower() switch
  1225. {
  1226. var name when name.Contains("name") => 10,
  1227. var name when name.Contains("title") => 10,
  1228. var name when name.Contains("code") => 8,
  1229. var name when name.Contains("no") => 8,
  1230. var name when name.Contains("desc") => 6,
  1231. var name when name.Contains("content") => 6,
  1232. var name when name.Contains("remark") => 5,
  1233. var name when name.Contains("phone") => 4,
  1234. var name when name.Contains("tel") => 4,
  1235. var name when name.Contains("address") => 3,
  1236. var name when name.Contains("email") => 3,
  1237. _ => 5
  1238. };
  1239. }
  1240. /// <summary>
  1241. /// 获取显示名称
  1242. /// </summary>
  1243. private string GetDisplayName(System.Reflection.PropertyInfo property)
  1244. {
  1245. var displayAttr = property.GetCustomAttribute<System.ComponentModel.DataAnnotations.DisplayAttribute>();
  1246. return displayAttr?.Name ?? property.Name;
  1247. }
  1248. /// <summary>
  1249. /// 获取字段描述
  1250. /// </summary>
  1251. private string GetFieldDescription(System.Reflection.PropertyInfo property)
  1252. {
  1253. var descriptionAttr = property.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
  1254. return descriptionAttr?.Description ?? string.Empty;
  1255. }
  1256. #endregion
  1257. }
  1258. }