qinchaoyue 6 months ago
parent
commit
b7a645be78
28 changed files with 1051 additions and 285 deletions
  1. 7 7
      src/Hotline.Api/Controllers/Bi/BiCallController.cs
  2. 224 99
      src/Hotline.Api/Controllers/KnowledgeController.cs
  3. 34 0
      src/Hotline.Api/Middleware/HeaderMiddleware.cs
  4. 4 1
      src/Hotline.Api/StartupExtensions.cs
  5. 1 1
      src/Hotline.Application.Contracts/Validators/Knowledge/KnowledageCollectGroupValidator.cs
  6. 109 0
      src/Hotline.Application.Tests/Application/KnowApplicationTest.cs
  7. 2 2
      src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs
  8. 6 13
      src/Hotline.Application/ExportExcel/ExportApplication.cs
  9. 2 1
      src/Hotline.Application/ExportExcel/IExportApplication.cs
  10. 50 1
      src/Hotline.Application/ExportWord/WordHelper.cs
  11. 2 0
      src/Hotline.Application/Hotline.Application.csproj
  12. 43 1
      src/Hotline.Application/Knowledge/IKnowApplication.cs
  13. 229 153
      src/Hotline.Application/Knowledge/KnowApplication.cs
  14. 17 1
      src/Hotline.Application/Systems/BaseDataApplication.cs
  15. 33 0
      src/Hotline.Application/Tools/StreamExtensions.cs
  16. 23 0
      src/Hotline.Application/Tools/StringExtensions.cs
  17. 10 0
      src/Hotline.Share/Attributes/ContentTypeAttribute.cs
  18. 10 0
      src/Hotline.Share/Attributes/FileExtensionAttribute.cs
  19. 3 0
      src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs
  20. 1 1
      src/Hotline.Share/Dtos/Knowledge/KnowledgeCollectDto.cs
  21. 101 1
      src/Hotline.Share/Dtos/Knowledge/KnowledgeDto.cs
  22. 31 0
      src/Hotline.Share/Enums/Article/EFileType.cs
  23. 45 0
      src/Hotline.Share/Tools/EnumExtensions.cs
  24. 13 1
      src/Hotline.Share/Tools/StringExtensions.cs
  25. 2 2
      src/Hotline.Share/Tools/TupleExtensions.cs
  26. 13 0
      src/Hotline.Share/Tools/TypeExtensions.cs
  27. 34 0
      src/Hotline/KnowledgeBase/KnowledgeHotWord.cs
  28. 2 0
      src/Hotline/KnowledgeBase/KnowledgeWord.cs

+ 7 - 7
src/Hotline.Api/Controllers/Bi/BiCallController.cs

@@ -83,7 +83,7 @@ public class BiCallController : BaseController
     [AllowAnonymous]
     public async Task<FileStreamResult> ExportQueryCallsAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
         => ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
@@ -212,7 +212,7 @@ public class BiCallController : BaseController
     public async Task<FileStreamResult> QueryCallsDetailExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
     {
         return ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsDetailAsync(dto.QueryDto),
                 items =>
@@ -272,7 +272,7 @@ public class BiCallController : BaseController
 
 
         return ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
             dto,
             (await _callReportApplication.QueryCallsDetailInTotalAsync(dto.QueryDto, dto.IsExportAll)).Item2
             ),
@@ -319,7 +319,7 @@ public class BiCallController : BaseController
     [HttpPost("query_calls_hour_detail_list_export")]
     public async Task<FileStreamResult> QueryCallsHourDetailListExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
         => ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsHourDetailAsync(dto.QueryDto, HttpContext.RequestAborted),
                 items =>
@@ -354,7 +354,7 @@ public class BiCallController : BaseController
     public async Task<FileStreamResult> ExportSeatss([FromBody] ExportExcelDto<ReportRequiredPagedRequest> dto)
     {
         return ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QuerySeatCallAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
@@ -449,7 +449,7 @@ public class BiCallController : BaseController
     [AllowAnonymous]
     public async Task<FileStreamResult> ExportQueryHourCall([FromBody] ExportExcelDto<BiQueryHourCallDto> dto)
         => ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.GetCallHourListAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
@@ -506,7 +506,7 @@ public class BiCallController : BaseController
     [HttpPost("gateway-query/export")]
     public async Task<FileStreamResult> ExportQueryGatetWay(ExportExcelDto<BiQueryGateWayDto> dto)
         => ExcelStreamResult(
-            _exportApplication.GetExcelFile(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.GetCallHotLineListAsync(dto.QueryDto, HttpContext.RequestAborted)
                 )

+ 224 - 99
src/Hotline.Api/Controllers/KnowledgeController.cs

@@ -1,14 +1,20 @@
-using DotNetCore.CAP;
+using DocumentFormat.OpenXml.Wordprocessing;
+using DotNetCore.CAP;
 using Hotline.Api.Filter;
 using Hotline.Application.Bulletin;
+using Hotline.Application.ExportExcel;
+using Hotline.Application.ExportWord;
 using Hotline.Application.FlowEngine;
 using Hotline.Application.Knowledge;
+using Hotline.Application.Systems;
+using Hotline.Application.Tools;
 using Hotline.File;
 using Hotline.FlowEngine.WorkflowModules;
 using Hotline.KnowledgeBase;
 using Hotline.KnowledgeBase.Notifies;
 using Hotline.Permissions;
 using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Repository.SqlSugar.Knowledge;
 using Hotline.Repository.SqlSugar.Ts;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
@@ -16,6 +22,8 @@ using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
 using Hotline.Share.Mq;
 using Hotline.Share.Tools;
@@ -26,6 +34,7 @@ using MapsterMapper;
 using MediatR;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Org.BouncyCastle.Utilities.IO;
 using SqlSugar;
 using System.Threading;
 using XF.Domain.Authentications;
@@ -40,6 +49,11 @@ namespace Hotline.Api.Controllers
     {
 
         #region 注入
+        private readonly IExportApplication _exportApplication;
+        private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+        private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+        private readonly IWordHelperService _wordHelperService;
+        private readonly BaseDataApplication _baseDataApplication;
         private readonly IKnowledgeRepository _knowledgeRepository;
         private readonly ISessionContext _sessionContext;
         private readonly IKnowledgeDomainService _knowledgeDomainService;
@@ -64,6 +78,7 @@ namespace Hotline.Api.Controllers
         private readonly IRepository<KnowledgeRelationType> _knowledgeRelationTypeRepository;
         private readonly IBulletinApplication _bulletinApplication;
         private readonly IRepository<KnowledgeCollectGroup> _knowledgeCollectGroupRepository;
+        private readonly IRepository<KnowledgePv> _knowledgePvepository;
 
 
         public KnowledgeController(
@@ -89,9 +104,15 @@ namespace Hotline.Api.Controllers
            IFileRepository fileRepository,
            ICapPublisher capPublisher,
            IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository,
-            IBulletinApplication bulletinApplication
-,
-            IRepository<KnowledgeCollectGroup> knowledgeCollectGroupRepository)
+            IBulletinApplication bulletinApplication,
+            IRepository<KnowledgeCollectGroup> knowledgeCollectGroupRepository,
+            IExportApplication exportApplication,
+            BaseDataApplication baseDataApplication,
+            IWordHelperService wordHelperService,
+            IRepository<KnowledgePv> knowledgePvepository,
+            IRepository<KnowledgeWord> knowledgeWordRepository,
+            IRepository<KnowledgeHotWord> knowledgeWordHotRepository,
+            IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
         {
             _knowledgeRepository = knowledgeRepository;
             _sessionContext = sessionContext;
@@ -117,6 +138,12 @@ namespace Hotline.Api.Controllers
             _knowledgeRelationTypeRepository = knowledgeRelationTypeRepository;
             _bulletinApplication = bulletinApplication;
             _knowledgeCollectGroupRepository = knowledgeCollectGroupRepository;
+            _exportApplication = exportApplication;
+            _baseDataApplication = baseDataApplication;
+            _wordHelperService = wordHelperService;
+            _knowledgePvepository = knowledgePvepository;
+            _knowledgeWordRepository = knowledgeWordRepository;
+            _knowledgeHotWordRepository = knowledgeHotWordRepository;
         }
 
         #endregion
@@ -136,24 +163,26 @@ namespace Hotline.Api.Controllers
             //var addDto = _mapper.Map<AddKnowledgeDto>(dto.Data);
             var kn = _mapper.Map<Knowledge>(dto.Data);
             kn.SourceOrganizeId = _sessionContext.RequiredOrgId;
-			var any = await _knowledgeRepository.Queryable().Where(x => x.Status == EKnowledgeStatus.OnShelf && x.Title == kn.Title).AnyAsync();
+            var any = await _knowledgeRepository.Queryable().Where(x => x.Status == EKnowledgeStatus.OnShelf && x.Title == kn.Title).AnyAsync();
             if (any) throw UserFriendlyException.SameMessage("当前知识标题存在重复标题!");
 
             //Code为空,从新生成Code
             if (string.IsNullOrEmpty(kn.Code))
                 kn.Code = Convert.ToInt64((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds).ToString();
 
+            kn.Keywords = (await _knowApplication.TitleParticiple(kn.Keywords, kn.Title)).ToList();
             kn.Status = EKnowledgeStatus.Drafts;
             kn.InitId();
             if (dto.Data.Files.Any()) kn.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, kn.Id, "", HttpContext.RequestAborted);
             await _knowledgeRepository.AddAsync(kn, HttpContext.RequestAborted);
+
             if (dto.Data.KnowledgeType.Any())
             {
-				List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
-				types.ForEach(x => x.KnowledgeId = kn.Id);
-				await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
-			}
-			if (dto.Workflow != null && !string.IsNullOrEmpty(kn.Id))
+                List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
+                types.ForEach(x => x.KnowledgeId = kn.Id);
+                await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
+            }
+            if (dto.Workflow != null && !string.IsNullOrEmpty(kn.Id))
             {
                 var startDto = _mapper.Map<StartWorkflowDto>(dto.Workflow);
                 startDto.DefinitionModuleCode = WorkflowModuleConsts.KnowledgeAdd;
@@ -165,7 +194,7 @@ namespace Hotline.Api.Controllers
                 //knowledge.Status = EKnowledgeStatus.Auditing;
                 //await _knowledgeRepository.UpdateAsync(knowledge, HttpContext.RequestAborted);
             }
-         
+
             return kn.Id;
         }
 
@@ -259,22 +288,22 @@ namespace Hotline.Api.Controllers
             _mapper.Map(dto.Data, knowledge);
             //if (update.Tags.Any()) await _repositoryts.UpdateVectorAsync(update.Id, update.Tags, HttpContext.RequestAborted);
 
-            if (dto.Data.Files.Any()) 
+            if (dto.Data.Files.Any())
                 knowledge.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, knowledge.Id, "", HttpContext.RequestAborted);
             else
-	            knowledge.FileJson = new List<Share.Dtos.File.FileJson>();
-			if (dto.Workflow != null) knowledge.Renewaln = knowledge.Status != EKnowledgeStatus.Drafts;
+                knowledge.FileJson = new List<Share.Dtos.File.FileJson>();
+            if (dto.Workflow != null) knowledge.Renewaln = knowledge.Status != EKnowledgeStatus.Drafts;
             await _knowledgeRepository.UpdateAsync(knowledge, HttpContext.RequestAborted);
             if (dto.Data.KnowledgeType.Any())
             {
-	            var anyRelationTypes = await _knowledgeRelationTypeRepository.Queryable().Where(x => x.KnowledgeId == knowledge.Id).ToListAsync();
-	            if (anyRelationTypes.Any())
-		            await _knowledgeRelationTypeRepository.RemoveRangeAsync(anyRelationTypes);
-	            List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
-	            types.ForEach(x => x.KnowledgeId = update.Id);
-	            await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
+                var anyRelationTypes = await _knowledgeRelationTypeRepository.Queryable().Where(x => x.KnowledgeId == knowledge.Id).ToListAsync();
+                if (anyRelationTypes.Any())
+                    await _knowledgeRelationTypeRepository.RemoveRangeAsync(anyRelationTypes);
+                List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
+                types.ForEach(x => x.KnowledgeId = update.Id);
+                await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
             }
-			if (dto.Workflow != null)
+            if (dto.Workflow != null)
             {
                 if (knowledge.Status == EKnowledgeStatus.Drafts)
                 {
@@ -293,8 +322,8 @@ namespace Hotline.Api.Controllers
                     await StartFlow(knowledge.Id, WorkflowModuleConsts.KnowledgeUpdate, EKnowledgeApplyType.Update, startDto);
                 }
             }
-            
-		}
+
+        }
 
         /// <summary>
         /// 删除知识
@@ -359,11 +388,11 @@ namespace Hotline.Api.Controllers
 
             if (_sessionContext.OrgIsCenter == false)
             {
-                query = query.Where(m => m.KnowledgeType.Any(a =>  a.IsDeleted == false && a.KnowledgeType.KnowledgeTypeOrgs
+                query = query.Where(m => m.KnowledgeType.Any(a => a.IsDeleted == false && a.KnowledgeType.KnowledgeTypeOrgs
                 .Any(k => k.IsDeleted == false && k.OrgId.StartsWith(_sessionContext.OrgId))));
                 query = query.Where(m => m.Attribution == "部门知识库");
             }
-            var (total, items) = await query .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+            var (total, items) = await query.ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
             return new PagedDto<KnowledgeDto>(total, _mapper.Map<IReadOnlyList<KnowledgeDto>>(items));
         }
 
@@ -377,7 +406,7 @@ namespace Hotline.Api.Controllers
         {
             //var know = await _knowledgeRepository.GetAsync(Id, HttpContext.RequestAborted);
             var know = await _knowledgeDomainService.KnowledgeInfo(Id, HttpContext.RequestAborted);
-			if (know is null)
+            if (know is null)
                 throw UserFriendlyException.SameMessage("知识查询失败!");
 
             var knowledgeInfoDto = _mapper.Map<KnowledgeInfoDto>(know);
@@ -412,7 +441,7 @@ namespace Hotline.Api.Controllers
             var knowledge = await _knowledgeDomainService.KnowledgeInfo(Id, HttpContext.RequestAborted);
             if (knowledge is null)
                 throw UserFriendlyException.SameMessage("知识查询失败!");
-            if (knowledge.Workflow != null) 
+            if (knowledge.Workflow != null)
                 knowledge.IsCanHandle = knowledge.Workflow.IsCanHandle(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId, _sessionContext.Roles);
             //转化
             var knowledgeShowInfoDto = _mapper.Map<KnowledgeInfoDto>(knowledge);
@@ -450,6 +479,26 @@ namespace Hotline.Api.Controllers
             return knowledgeShowInfoDto;
         }
 
+        /// <summary>
+        /// 知识详情--导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("info/export")]
+        public async Task<IActionResult> KnowledgeInfoExport([FromBody] KnowledgeInfoExportInDto dto)
+        {
+            if (dto.Ids.Length > 1)
+            {
+                var streams = await _knowApplication.KnowledgeInfoListExportAsync(dto, HttpContext.RequestAborted);
+                byte[] fileBytes = _wordHelperService.ConvertZipStream(streams);
+                var name = DateTime.Now.ToString("yyyyMMddHHmmss");
+                return File(fileBytes, "application/octet-stream", $"{name}.zip");
+            }
+
+            var info = await _knowledgeRepository.GetAsync(dto.Ids[0]) ?? throw UserFriendlyException.SameMessage("知识不存在");
+            return info.Content.HtmlToStream(dto.FileType).GetWordFile("知识详情");
+        }
+
         /// <summary>
         /// 知识申请-关联知识-获取知识列表
         /// </summary>
@@ -513,13 +562,18 @@ namespace Hotline.Api.Controllers
         [HttpGet("knowledge-status-data")]
         public async Task<object> KnowledgeStatus()
         {
-            return new List<KeyValuePair<int, string>>
+            var tabNames = new List<KeyValuePair<int, string>>
             {
                 new KeyValuePair<int, string>(3, "已上架"),
                 new KeyValuePair<int, string>(4, "已下架"),
                 new KeyValuePair<int, string>(1, "审核中"),
                 new KeyValuePair<int, string>(5, "审核不通过")
             };
+
+            return _baseDataApplication
+                .FileType(0)
+                .Add("tabNames", tabNames)
+                .Build();
         }
 
         /// <summary>
@@ -530,66 +584,40 @@ namespace Hotline.Api.Controllers
         [HttpGet]
         public async Task<PagedDto<KnowledgeDataDto>> GetKnowList([FromQuery] KnowPagedListDto pagedDto)
         {
-            var typeSpliceName = string.Empty;
-            var hotspotHotSpotFullName = string.Empty;
-			if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
-            {
-                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
-                typeSpliceName = type?.SpliceName;
-            }
-            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
-            {
-                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
-                hotspotHotSpotFullName = hotspot?.HotSpotFullName;
-            }
-            //var aa = _knowledgeRepository.Queryable().OrderByDescending(d => d.CreationTime).ToSql();
-            var (total, temp) = await _knowledgeRepository.Queryable(false, false, false)
-                .Includes(x => x.User)
-                .Includes(x => x.SystemOrganize)
-                .Includes(x => x.SourceOrganize)
-                .Includes(x => x.HotspotType)
-                .Includes(x => x.Workflow)
-                .Includes(x=>x.KnowledgeType)
-                //.Includes(x=>x.KnowledgeRelationTypes,t=> t.)
-                .Where(x => x.IsDeleted == false)
-                .Where(x=>x.KnowledgeType.Any(t=>t.KnowledgeType.KnowledgeTypeOrgs.Any(to=>to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
-                .Where(x => (x.Status == EKnowledgeStatus.Drafts && x.CreatorId == _sessionContext.UserId) || (x.Status != EKnowledgeStatus.Drafts))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title), x => x.Title.Contains(pagedDto.Title!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), x => x.Title.Contains(pagedDto.Keyword!) || x.CreatorName.Contains(pagedDto.Keyword!) || x.CreatorOrgName.Contains(pagedDto.Keyword!) || x.SourceOrganize.Name.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status != EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status && ((x.ExpiredTime != null && x.ExpiredTime > DateTime.Now) || x.ExpiredTime == null))
-                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status || (x.ExpiredTime != null && x.ExpiredTime < DateTime.Now && x.Status != EKnowledgeStatus.Drafts))
-                .WhereIF(pagedDto.IsPublic.HasValue, x => x.IsPublic == pagedDto.IsPublic)
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Summary), x => x.Summary != null && x.Summary.Contains(pagedDto.Summary!))
-				//.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
-				.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.SourceOrganizeId != null && x.SourceOrganizeId.EndsWith(pagedDto.CreateOrgId!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.ModuleCode), x => x.Workflow.ModuleCode == pagedDto.ModuleCode)
-                .OrderByDescending(d => d.CreationTime)
-                .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, HttpContext.RequestAborted);
-            //temp.ForEach(x => x.IsCanHandle = x.Workflow.CanHandle(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId));
-            //返回数据
-            return new PagedDto<KnowledgeDataDto>(total, _mapper.Map<IReadOnlyList<KnowledgeDataDto>>(temp));
+            return (await _knowApplication.GetKnowList(pagedDto, HttpContext.RequestAborted))
+                .ToPaged();
+        }
+
+        /// <summary>
+        /// 知识查询-导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("export")]
+        public async Task<IActionResult> GetKnowListExportAsync([FromBody] ExportExcelDto<KnowPagedListDto> dto)
+        {
+            var items = (await _knowApplication.GetKnowList(dto.QueryDto, HttpContext.RequestAborted)).Item2;
+            return _exportApplication.GetExcelFile(dto, items, "知识明细导出");
         }
 
         /// <summary>
         /// 知识检索
         /// </summary>
-        /// <param name="pagedDto"></param>
+        /// <param name="dto"></param>
         /// <returns></returns>
         [HttpGet("knowretrieval")]
-        public async Task<PagedDto<KnowledgeRetrievalDataDto>> KnowRetrieval([FromQuery] KnowledgeRetrievalPagedListDto pagedDto)
+        public async Task<PagedDto<KnowledgeRetrievalDataDto>> KnowRetrieval([FromQuery] KnowledgeRetrievalPagedListDto dto)
         {
             var typeSpliceName = string.Empty;
             var hotspotHotSpotFullName = string.Empty;
-            if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
+            if (!string.IsNullOrEmpty(dto.KnowledgeTypeId))
             {
-                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
+                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == dto.KnowledgeTypeId);
                 typeSpliceName = type?.SpliceName;
             }
-            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
+            if (!string.IsNullOrEmpty(dto.HotspotId))
             {
-                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
+                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == dto.HotspotId);
                 hotspotHotSpotFullName = hotspot?.HotSpotFullName;
             }
             var sugar = _knowledgeRepository
@@ -600,17 +628,35 @@ namespace Hotline.Api.Controllers
                 .Where(x => x.IsDeleted == false)
                 .Where(x => x.Status == EKnowledgeStatus.OnShelf)
                 .Where(x => x.KnowledgeType.Any(t => t.KnowledgeType.KnowledgeTypeOrgs.Any(to => to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
-				.WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.All && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Title.Contains(pagedDto.Keyword!) || d.Content.Contains(pagedDto.Keyword!))// || d.Additions.Contains(pagedDto.Keyword)
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Title && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Title.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Content && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Content.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Summary && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Summary != null && d.Summary.Contains(pagedDto.Keyword!))
-				//.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
-				.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.HotspotName), x => x.HotspotType.HotSpotFullName.EndsWith(pagedDto.HotspotName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.CreatorOrgId != null && x.CreatorOrgId.EndsWith(pagedDto.CreateOrgId!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Attribution), x => x.Attribution == pagedDto.Attribution!);
-            switch (pagedDto.Sort)
+                // .WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.All && !string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.Content.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Title && !string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Content && !string.IsNullOrEmpty(dto.Keyword), d => d.Content.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Summary && !string.IsNullOrEmpty(dto.Keyword), d => d.Summary != null && d.Summary.Contains(dto.Keyword!))
+                //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
+                .WhereIF(!string.IsNullOrEmpty(dto.HotspotName), x => x.HotspotType.HotSpotFullName.EndsWith(dto.HotspotName!))
+                .WhereIF(!string.IsNullOrEmpty(dto.CreateOrgId), x => x.CreatorOrgId != null && x.CreatorOrgId.EndsWith(dto.CreateOrgId!))
+                .WhereIF(!string.IsNullOrEmpty(dto.Attribution), x => x.Attribution == dto.Attribution!);
+            if (dto.Keyword.NotNullOrEmpty())
+            {
+                var keywords = dto.Keyword!.SplitKeywords();
+                var exp = Expressionable.Create<Knowledge>();
+                foreach (var keyword in keywords)
+                {
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.All)
+                        exp.Or(m => m.Title.Contains(keyword) || m.Content.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Title)
+                        exp.Or(m => m.Title.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Content)
+                        exp.Or(m => m.Content.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Summary)
+                        exp.Or(m => m.Summary != null && m.Summary.Contains(keyword));
+                }
+                sugar.Where(exp.ToExpression());
+            }
+
+            switch (dto.Sort)
             {
                 case "2":
                     sugar = sugar.OrderByDescending(p => p.Score);
@@ -622,7 +668,7 @@ namespace Hotline.Api.Controllers
                     sugar = sugar.OrderByDescending(p => p.PageView);
                     break;
             }
-            var (total, temp) = await sugar.ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize);
+            var (total, temp) = await sugar.ToPagedListAsync(dto.PageIndex, dto.PageSize);
             return new PagedDto<KnowledgeRetrievalDataDto>(total, _mapper.Map<IReadOnlyList<KnowledgeRetrievalDataDto>>(temp));
         }
 
@@ -641,7 +687,7 @@ namespace Hotline.Api.Controllers
                 .Includes(x => x.Workflow)
                 .Where(x => x.KnowledgeId == pagedDto.id)
                 .Where(x => x.IsDeleted == false)
-                .OrderBy(x=>x.CreationTime)
+                .OrderBy(x => x.CreationTime)
                 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize);
             return new PagedDto<KnowledgeWorkFlowDto>(total, _mapper.Map<IReadOnlyList<KnowledgeWorkFlowDto>>(temp));
         }
@@ -658,7 +704,7 @@ namespace Hotline.Api.Controllers
                 .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title.Equals(dto.Title))
                 .WhereIF(!string.IsNullOrEmpty(dto.Summary), x => x.Summary.Equals(dto.Summary))
                 .WhereIF(!string.IsNullOrEmpty(dto.Content), x => x.Content.Equals(dto.Content))
-                .WhereIF(!string.IsNullOrEmpty(dto.Id),x=> x.Id !=  dto.Id)
+                .WhereIF(!string.IsNullOrEmpty(dto.Id), x => x.Id != dto.Id)
                 .AnyAsync();
             return any;
         }
@@ -733,7 +779,7 @@ namespace Hotline.Api.Controllers
                 .Includes(it => it.Knowledge)
                 .Includes(it => it.User)
                 .Includes(it => it.SystemOrganize)
-                .Includes(it => it.Workflow, d=>d.Steps)
+                .Includes(it => it.Workflow, d => d.Steps)
                 .Where(it => it.WorkflowId != null)
                 .WhereIF(pagedDto.EKnowledgeApplyType.HasValue, d => d.WorkflowModuleStatus == pagedDto.EKnowledgeApplyType)
                 .WhereIF(pagedDto.EKnowledgeWorkFlowStatus.HasValue, d => d.WorkFlowApplyStatus == pagedDto.EKnowledgeWorkFlowStatus)
@@ -872,7 +918,7 @@ namespace Hotline.Api.Controllers
             dto.DefinitionModuleCode = moduleCode;
             dto.Title = knowledge.Title;
             return await _workflowApplication.StartWorkflowAsync(dto, _sessionContext, id, cancellationToken: HttpContext.RequestAborted);
-		}
+        }
         #endregion
 
         #region 知识库词库
@@ -1047,9 +1093,9 @@ namespace Hotline.Api.Controllers
                 .Includes(x => x.Knowledge)
                 //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
                 .WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
-				 //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
-				 .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
+                 //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
+                 .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
                 .OrderByDescending(x => x.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
             return new PagedDto<KnowledgeCorrectionDto>(total, _mapper.Map<IReadOnlyList<KnowledgeCorrectionDto>>(items));
@@ -1150,10 +1196,10 @@ namespace Hotline.Api.Controllers
             }
             var (total, items) = await _knowledgeQuestionsRepository.Queryable()
                 .Includes(x => x.Knowledge)
-			   //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
-			   //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
-			   .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
+               //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
+               //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
+               .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
                 .Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
                 .OrderByDescending(x => x.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
@@ -1336,7 +1382,7 @@ namespace Hotline.Api.Controllers
         /// 增加收藏分组
         /// </summary>
         [HttpPost("group")]
-        public async Task<KnowledgeCollectGroupOutDto> SaveKnowledgeCoolectGroupAsync([FromBody]KnowledgeCollectGroupInDto dto)
+        public async Task<KnowledgeCollectGroupOutDto> SaveKnowledgeCoolectGroupAsync([FromBody] KnowledgeCollectGroupInDto dto)
         {
             var entity = dto.Adapt<KnowledgeCollectGroup>();
             if (await _knowledgeCollectGroupRepository
@@ -1374,7 +1420,7 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <returns></returns>
         [HttpGet("group")]
-        public async Task<PagedDto<KnowledgeCollectGroupOutDto>> GetKnowledgeCollectGroupListAsync([FromQuery]KnowledgeCollectGroupListInDto dto)
+        public async Task<PagedDto<KnowledgeCollectGroupOutDto>> GetKnowledgeCollectGroupListAsync([FromQuery] KnowledgeCollectGroupListInDto dto)
         {
             return (await _knowledgeCollectGroupRepository.Queryable()
                 .WhereIF(dto.Keyword.NotNullOrEmpty(), m => m.Name.Contains(dto.Keyword))
@@ -1384,5 +1430,84 @@ namespace Hotline.Api.Controllers
                 ).ToPaged();
         }
         #endregion
+
+        #region PageView 浏览记录
+        /// <summary>
+        /// 浏览记录集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("pageview")]
+        public async Task<PagedDto<PageViewOutDto>> GetPageViewListAsync([FromQuery] PageViewInDto dto)
+        {
+            return (await _knowApplication.GetPageViewListAsync(dto, HttpContext.RequestAborted))
+                .ToPaged();
+
+        }
+
+        /// <summary>
+        /// 浏览记录集合-导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("pageview/export")]
+        public async Task<FileStreamResult> GetPageViewListAsync([FromBody] ExportExcelDto<PageViewInDto> dto)
+        {
+            var items = (await _knowApplication.GetPageViewListAsync(dto.QueryDto, HttpContext.RequestAborted))
+                .Item2;
+            return _exportApplication.GetExcelFile(dto, items, "浏览记录");
+        }
+
+        #endregion
+
+        #region 热词
+
+        /// <summary>
+        /// 新增热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("hotword")]
+        public async Task AddKnowledgeHotWordAsync([FromBody] AddKnowledgeHotWordInDto dto)
+        {
+            await _knowApplication.AddKnowledgeHotWordAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 热词集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("hotword")]
+        public async Task<PagedDto<KnowledgeWordOutDto>> GetKnowledgeHotWordListAsync([FromQuery] KnowledgeHotWordInDto dto)
+        {
+            return (await _knowApplication.GetKnowledgeHotWordListAsync(dto, HttpContext.RequestAborted))
+                .ToPaged();
+        }
+
+        /// <summary>
+        /// 修改热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("hotword")]
+        public async Task UpdateKnowledgeHotWordAsync([FromQuery] UpdateKnowledgeHotWordInDto dto)
+        {
+            await _knowApplication.UpdateKnowledgeHotWordAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 删除热词
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        [HttpDelete("hotword")]
+        public async Task DeleteKnowledgeHotWordAsync([FromBody] string id)
+        {
+            await _knowledgeHotWordRepository
+                .RemoveAsync(id);
+        }
+
+        #endregion
     }
 }

+ 34 - 0
src/Hotline.Api/Middleware/HeaderMiddleware.cs

@@ -0,0 +1,34 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Api.Middleware;
+
+/// <summary>
+/// 为特定的返回结果添加 头部信息 的中间件
+/// </summary>
+public class HeaderMiddleware
+{
+    private readonly RequestDelegate _next;
+
+    public HeaderMiddleware(RequestDelegate next)
+    {
+        _next = next;
+    }
+
+    public async Task InvokeAsync(HttpContext context)
+    {
+        var originalBodyStream = context.Response.Body;
+        using var responseBody = new MemoryStream();
+        context.Response.Body = responseBody;
+
+        await _next(context);
+
+        // 为返回类型是 FileStreamResult 的请求添加 头部信息
+        if (context.Response.StatusCode == 200 && context.Response is FileStreamResult)
+        {
+            context.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+        }
+
+        context.Response.Body.Seek(0, SeekOrigin.Begin);
+        await responseBody.CopyToAsync(originalBodyStream);
+    }
+}

+ 4 - 1
src/Hotline.Api/StartupExtensions.cs

@@ -29,6 +29,7 @@ using Hotline.Configurations;
 using Hotline.DI;
 using Hotline.Share.Tools;
 using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+using Hotline.Api.Middleware;
 
 namespace Hotline.Api;
 
@@ -219,7 +220,9 @@ internal static class StartupExtensions
         app.MapControllers()
             .RequireAuthorization();
         //app.MapSubscribeHandler();
-
+        
+        // 为特定返回结果添加 头部信息 的中间件
+        app.UseMiddleware<HeaderMiddleware>(); 
         return app;
     }
 }

+ 1 - 1
src/Hotline.Application.Contracts/Validators/Knowledge/KnowledageCollectGroupValidator.cs

@@ -15,7 +15,7 @@ namespace Hotline.Application.Contracts.Validators.Knowledge
     {
         public KnowledgeCollectAddDtoValidator()
         {
-            //RuleFor(d => d.KnowledgeCollectGroupId).NotEmpty();
+            // RuleFor(d => d.KnowledgeCollectGroupId).NotEmpty();
         }
     }
 }

+ 109 - 0
src/Hotline.Application.Tests/Application/KnowApplicationTest.cs

@@ -0,0 +1,109 @@
+using Hotline.Application.Knowledge;
+using Hotline.KnowledgeBase;
+using Hotline.KnowledgeBase.Notifies;
+using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Tools;
+using Mapster;
+using MediatR;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class KnowApplicationTest
+{
+    private readonly IKnowApplication _knowApplication;
+    private readonly IRepository<KnowledgeRelationType> _knowledgeRelationTypeRepository;
+    private readonly IMediator _mediator;
+    private readonly IRepository<KnowledgeBase.Knowledge> _knowledgeRepository;
+    private readonly IKnowledgeDomainService _knowledgeDomainService;
+    private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+    private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+
+    public KnowApplicationTest(IKnowApplication knowApplication, IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository, IMediator mediator, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IKnowledgeDomainService knowledgeDomainService, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
+    {
+        _knowApplication = knowApplication;
+        _knowledgeRelationTypeRepository = knowledgeRelationTypeRepository;
+        _mediator = mediator;
+        _knowledgeRepository = knowledgeRepository;
+        _knowledgeDomainService = knowledgeDomainService;
+        _knowledgeWordRepository = knowledgeWordRepository;
+        _knowledgeHotWordRepository = knowledgeHotWordRepository;
+    }
+
+    [Fact]
+    public async Task TitleParticiple_Test()
+    {
+        var result = await _knowApplication.TitleParticiple(null, "短信回访的相关解释口径、规范用语");
+        result.ShouldNotBeNull();
+    }
+
+    [Fact]
+    public void SplitKeywords_Test()
+    {
+        var keywords = "短信 筷子, 天才, 脑洞, 大开";
+        var items = keywords.SplitKeywords();
+        items.Count.ShouldBe(5);
+        items[0] = "短信";
+        items[1] = "筷子";
+        items[2] = "天才";
+        items[3] = "脑洞";
+        items[4] = "大开";
+    }
+
+    [Fact]
+    public async Task GetPageViewList_Test()
+    {
+        var r = await _knowledgeRelationTypeRepository.Queryable()
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+        var knowledge = await _knowledgeRepository.GetAsync(r.KnowledgeId);
+        await _knowledgeDomainService.KnowledgePvIncreaseAsync(knowledge, new CancellationToken());
+
+        var inDto = new PageViewInDto
+        {
+            KnowledgeTypeId = r.KnowledgeTypeId
+        };
+        var (total, items) = await _knowApplication.GetPageViewListAsync(inDto);
+        total.ShouldNotBe(0);
+    }
+
+    [Fact]
+    public async Task UpdateKnowledgeWord_Test()
+    {
+
+        var entity = await _knowledgeHotWordRepository.Queryable()
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+        entity.KeyWord = "单元测试修改";
+        var inDto = entity.Adapt<UpdateKnowledgeHotWordInDto>();
+        await _knowApplication.UpdateKnowledgeHotWordAsync(inDto);
+        var updateEntity = await _knowledgeWordRepository.GetAsync(entity.Id);
+        updateEntity.Tag.ShouldBe(entity.KeyWord);
+    }
+
+    [Fact]
+    public async Task AddKnowledgeHotWord_Test()
+    {
+        var addDto = new AddKnowledgeHotWordInDto
+        {
+            KeyWord = "单元测试" + new Random().Next(1, 100),
+        };
+        await _knowApplication.AddKnowledgeHotWordAsync(addDto);
+
+        try
+        {
+            await _knowApplication.AddKnowledgeHotWordAsync(addDto);
+
+        }
+        catch (Exception e)
+        {
+            e.Message.ShouldBe("热词已存在");
+        }
+    }
+}

+ 2 - 2
src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs

@@ -48,12 +48,12 @@ public class DefaultHttpContextAccessor : ISessionContext, IScopeDependency
     /// Roles
     /// </summary>
     public string[] Roles { get; set; }
-    public string? OrgId { get; set; }
+    public string? OrgId { get; set; } = "001";
     public string RequiredOrgId { get; }
     public string? OrgName { get; set; }
     public int OrgLevel { get; set; }
     public string? OrgAreaCode { get; set; }
-    public bool OrgIsCenter { get; set; }
+    public bool OrgIsCenter { get; set; } = true;
 
     /// <summary>
     /// 部门行政区划名称

+ 6 - 13
src/Hotline.Application/ExportExcel/ExportApplication.cs

@@ -1,6 +1,8 @@
 
+using Hotline.Application.Tools;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Tools;
 using Hotline.Tools;
 using Mapster;
@@ -11,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
 using MiniExcelLibs;
 using System.Reflection;
 using XF.Domain.Dependency;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Application.ExportExcel
 {
@@ -43,7 +46,7 @@ namespace Hotline.Application.ExportExcel
             };
         }
 
-        public Stream GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null)
+        public Stream GetExcelStream<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null)
         {
             if (items != null && items.Count > 0 && func != null)
             {
@@ -62,17 +65,7 @@ namespace Hotline.Application.ExportExcel
 
         public FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, Func<IList<T>, T>? func = null)
         {
-            var tail = DateTime.Now.ToString("yyyyMMddhhmmss") + ".xlsx";
-            fileName = string.IsNullOrEmpty(fileName)
-                ? tail
-                : $"{fileName}_{tail}";
-            _httpContextAccessor.HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
-            var stream = GetExcelFile(dto, items, func);
-            return new FileStreamResult(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
-            {
-                FileDownloadName = fileName
-            };
-
+            return GetExcelStream(dto, items, func).GetExcelFile(fileName);
         }
 
         public FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, string totalName) where T : new()
@@ -118,4 +111,4 @@ namespace Hotline.Application.ExportExcel
                    type == typeof(ushort) || type == typeof(sbyte);
         }
     }
-  }
+}

+ 2 - 1
src/Hotline.Application/ExportExcel/IExportApplication.cs

@@ -1,5 +1,6 @@
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using System;
@@ -21,7 +22,7 @@ namespace Hotline.Application.ExportExcel
         /// <returns></returns>
         FileStreamResult ExportData<T>(IList<T> list, string? name);
 
-        Stream GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null);
+        Stream GetExcelStream<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null);
 
         FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items,string fileName, Func<IList<T>, T>? func = null);
 

+ 50 - 1
src/Hotline.Application/ExportWord/WordHelper.cs

@@ -1,9 +1,58 @@
-using Hotline.Share.Dtos.ExportWord;
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
+using Hotline.Share.Dtos.ExportWord;
+using HtmlToOpenXml;
+using System.Drawing.Imaging;
+using System.Drawing.Printing;
+using System;
+using DinkToPdf;
+using ColorMode = DinkToPdf.ColorMode;
+using PaperKind = DinkToPdf.PaperKind;
+using DinkToPdf.Contracts;
 
 namespace Hotline.Application.ExportWord
 {
     public static class WordHelper
     {
+        public static Stream ConvertHtmlToPdf(string htmlContent)
+        {
+            var doc = new HtmlToPdfDocument()
+            {
+                GlobalSettings = { ColorMode = ColorMode.Color, Orientation = Orientation.Portrait, PaperSize = PaperKind.A4 },
+                Objects = { new ObjectSettings() { PagesCount = true, HtmlContent = htmlContent } }
+            };
+
+            IConverter _converter = new SynchronizedConverter(new PdfTools());
+            byte[] pdfBytes = _converter.Convert(doc);
+
+            var memoryStream = new MemoryStream(pdfBytes);
+            memoryStream.Position = 0;
+            return memoryStream;
+        }
+
+        public static Stream ConvertHtmlToWord(string htmlContent)
+        {
+            var memoryStream = new MemoryStream();
+            using (var wordDoc = WordprocessingDocument.Create(memoryStream, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
+            {
+                MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();
+                mainPart.Document = new Document(new Body());
+
+                HtmlConverter converter = new HtmlConverter(mainPart);
+                var paragraphs = converter.Parse(htmlContent);
+
+                foreach (var paragraph in paragraphs)
+                {
+                    mainPart.Document.Body.Append(paragraph);
+                }
+
+                mainPart.Document.Save();
+            }
+
+            memoryStream.Position = 0;
+            return memoryStream;
+        }
+
         public static byte[] WordByte<T>(string templatePath, T data) where T : IWordExportTemplate
         {
             return new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;

+ 2 - 0
src/Hotline.Application/Hotline.Application.csproj

@@ -7,6 +7,8 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="DinkToPdf" Version="1.0.8" />
+    <PackageReference Include="HtmlToOpenXml.dll" Version="3.2.0" />
     <PackageReference Include="JiebaAspNetCore.Segmenter" Version="1.0.1" />
     <PackageReference Include="JV.PanGu.Core" Version="1.0.1" />
     <PackageReference Include="NPOI" Version="2.7.0" />

+ 43 - 1
src/Hotline.Application/Knowledge/IKnowApplication.cs

@@ -1,6 +1,7 @@
 using Hotline.KnowledgeBase;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Knowledge;
+using Microsoft.AspNetCore.Mvc;
 
 namespace Hotline.Application.Knowledge
 {
@@ -11,7 +12,7 @@ namespace Hotline.Application.Knowledge
         /// </summary>
         /// <param name="pagedDto"></param>
         /// <returns></returns>
-        Task<PagedDto<KnowledgeDataDto>> GetKnowList(KnowPagedListDto pagedDto);
+        Task<(int, IList<KnowledgeDataDto>)> GetKnowList(KnowPagedListDto pagedDto, CancellationToken cancellationToken);
 
         /// <summary>
         /// 知识申请查询
@@ -28,5 +29,46 @@ namespace Hotline.Application.Knowledge
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 批量导出知识详情
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<Dictionary<string, Stream>> KnowledgeInfoListExportAsync(KnowledgeInfoExportInDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 知识标题分词
+        /// </summary>
+        /// <param name="keywords"></param>
+        /// <param name="title"></param>
+        /// <returns></returns>
+        Task<IList<string>> TitleParticiple(IList<string> keywords, string title);
+
+        /// <summary>
+        /// 知识库浏览历史
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task<(int, IList<PageViewOutDto>)> GetPageViewListAsync(PageViewInDto dto, CancellationToken requestAborted = default);
+
+        /// <summary>
+        /// 热词集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task<(int total, IList<KnowledgeWordOutDto> items)> GetKnowledgeHotWordListAsync(KnowledgeHotWordInDto dto, CancellationToken requestAborted);
+
+        /// <summary>
+        /// 修改热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task UpdateKnowledgeHotWordAsync(UpdateKnowledgeHotWordInDto dto, CancellationToken requestAborted = default);
+        Task AddKnowledgeHotWordAsync(AddKnowledgeHotWordInDto dto, CancellationToken requestAborted = default);
     }
 }

+ 229 - 153
src/Hotline.Application/Knowledge/KnowApplication.cs

@@ -1,169 +1,245 @@
-using Hotline.KnowledgeBase;
+using Hotline.Application.Tools;
+using Hotline.KnowledgeBase;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
+using Hotline.Share.Tools;
 using Hotline.Users;
+using Mapster;
 using MapsterMapper;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Org.BouncyCastle.Utilities.IO;
+using PanGu;
 using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
 namespace Hotline.Application.Knowledge
 {
-	public class KnowApplication : IKnowApplication, IScopeDependency
-	{
-		private readonly IKnowledgeRepository _knowledgeRepository;
-		private readonly IRepository<KnowledgeApply> _knowledgeApplyRepository;
-		private readonly ISessionContext _sessionContext;
-		private readonly IMapper _mapper;
-
-		/// <summary>
-		/// 
-		/// </summary>
-		/// <param name="knowledgeRepository"></param>
-		/// <param name="knowledgeApplyRepository"></param>
-		/// <param name="sessionContext"></param>
-		/// <param name="knowledgeDomainService"></param>
-		/// <param name="mapper"></param>
-		public KnowApplication(IKnowledgeRepository knowledgeRepository, IRepository<KnowledgeApply> knowledgeApplyRepository, ISessionContext sessionContext, IMapper mapper)
-		{
-			_knowledgeRepository = knowledgeRepository;
-			_knowledgeApplyRepository = knowledgeApplyRepository;
-			_sessionContext = sessionContext;
-			_mapper = mapper;
-		}
-
-		/// <summary>
-		/// 知识库查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeDataDto>> GetKnowList(KnowPagedListDto pagedDto)
-		{
-			RefAsync<int> total = 0;
-			var temp = await _knowledgeRepository
-			   .Queryable()
-			   .InnerJoin<User>((o, cus) => o.CreatorId == cus.Id)
-			   .InnerJoin<SystemOrganize>((o, cus, sys) => o.CreatorOrgId == sys.Id)
-			   .LeftJoin<Hotspot>((o, cus, sys, hst) => o.HotspotId == hst.Id)
-			   .Where((o, cus, sys, hst) => o.IsDeleted == false && o.Status != EKnowledgeStatus.Revert && o.Status != EKnowledgeStatus.Drafts)
-			   //关键词查询标题、创建人、创建部门
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), (o, cus, sys,  hst) => o.Title.Contains(pagedDto.Keyword!) || cus.Name.Contains(pagedDto.Keyword!) || sys.Name.Contains(pagedDto.Keyword!))
-			   //分类
-			   //.WhereIF(!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId), (o, cus, sys,  hst) => SqlFunc.JsonListObjectAny(o.KnowledgeType,"Key", pagedDto.KnowledgeTypeId))
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId), (o, cus, sys, hst) => o.KnowledgeType.Any(t=>t.KnowledgeTypeId == pagedDto.KnowledgeTypeId))
-			   //热点
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.HotspotId), (o, cus, sys,  hst) => o.HotspotId == pagedDto.HotspotId)
-			   //部门
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), (o, cus, sys,  hst) => o.CreatorOrgId == pagedDto.CreateOrgId)
-			  //状态
-			  // .WhereIF(pagedDto.Status.HasValue, (o, cus, sys, kn, hst) => o.Status == pagedDto.Status)
-			  //创建时间
-			  //.WhereIF(pagedDto.CreationStartTime.HasValue, (o, cus, sys, kn, hst) => o.CreationTime >= pagedDto.CreationStartTime)
-			  //.WhereIF(pagedDto.CreationEndTime.HasValue, (o, cus, sys, kn, hst) => o.CreationTime <= pagedDto.CreationEndTime)
-			  // //上架时间
-			  // .WhereIF(pagedDto.StartOnShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OnShelfTime >= pagedDto.StartOnShelfTime)
-			  //.WhereIF(pagedDto.EndOnShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OnShelfTime <= pagedDto.EndOnShelfTime)
-			  // //下架时间
-			  // .WhereIF(pagedDto.StartOffShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OffShelfTime >= pagedDto.StartOffShelfTime)
-			  //.WhereIF(pagedDto.EndOffShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OffShelfTime <= pagedDto.EndOffShelfTime)
-			   //重新构建数据
-			   .Select((o, cus, sys,  hst) => new
-			   {
-				   index = SqlFunc.RowNumber($"{o.Version} desc ", $"{o.Code}"),
-				   CreationName = cus.Name,
-				   CreationBMName = sys.Name,
-				   HotspotName = hst.HotSpotFullName,
-				   o.Id,
-				   o.Code,
-				   o.Title,
-				   o.Version,
-				   //o.KnowledgeType,
-				   o.KnowledgeType,
-				   o.IsDeleted,
-				   o.HotspotId,
-				   o.OnShelfTime,
-				   o.CreationTime,
-				   o.PageView,
-				   o.Status,
-				   o.OffShelfTime,
-				   o.LastModificationTime,
-				   o.WorkflowId,
-				   o.ExpiredTime,
-				   CreatorOrgId = o.CreatorOrgId,
-
-			   })
-		   //将结果合并成一个表
-		   .MergeTable()
-		   //取第一条数据
-		   .Where(d => d.index == 1)
-			.WhereIF(pagedDto.Status.HasValue, o => o.Status == pagedDto.Status)
-		   .OrderByDescending(d => d.CreationTime)
-		  //转分页数据
-		  .ToPageListAsync(pagedDto.PageIndex, pagedDto.PageSize, total);
-			//返回数据
-			return new PagedDto<KnowledgeDataDto>(total.Value, _mapper.Map<IReadOnlyList<KnowledgeDataDto>>(temp));
-		}
-
-		/// <summary>
-		/// 知识申请查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <param name="cancellationToken"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyList(KnowledgeApplyPagedDto pagedDto, CancellationToken cancellationToken)
-		{
-			var (total, items) = await _knowledgeApplyRepository
-			   .Queryable()
-			   .Includes(it => it.User)
-			   .Includes(it => it.SystemOrganize)
-			   .Where(d => d.CreatorId == _sessionContext.RequiredUserId)
-			   .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
-			   .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
-			   .WhereIF(pagedDto.StartTime.HasValue, d => d.CreationTime >= pagedDto.StartTime)
-			   .WhereIF(pagedDto.EndTime.HasValue, d => d.CreationTime <= pagedDto.EndTime)
-			   .OrderByDescending(p => p.CreationTime)
-			   .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
-
-			return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
-		}
-
-		/// <summary>
-		/// 申请处理查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <param name="cancellationToken"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken)
-		{
-			var (total, items) = await _knowledgeApplyRepository
-				 .Queryable()
-				 .Includes(it => it.User)
-				 .Includes(it => it.SystemOrganize)
-				 .Where(p => p.DepartmentId == _sessionContext.RequiredOrgId)
-				 .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), d => d.User.Name.Contains(pagedDto.Keyword!) || d.SystemOrganize.Name.Contains(pagedDto.Keyword!))
-				 .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
-
-				 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime <= pagedDto.EndTime)
-				 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime <= pagedDto.EndTime)
-				  .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime <= pagedDto.EndTime)
-
-				 .OrderByDescending(p => p.CreationTime)
-				 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
-
-			return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
-		}
-	}
+    public class KnowApplication : IKnowApplication, IScopeDependency
+    {
+        private readonly IKnowledgeRepository _knowledgeRepository;
+        private readonly IRepository<KnowledgePv> _knowledgePvepository;
+        private readonly IRepository<Hotspot> _hotspotTypeRepository;
+        private readonly IRepository<KnowledgeType> _knowledgeTypeRepository;
+        private readonly IRepository<KnowledgeApply> _knowledgeApplyRepository;
+        private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+        private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+        private readonly ISessionContext _sessionContext;
+        private readonly IMapper _mapper;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="knowledgeRepository"></param>
+        /// <param name="knowledgeApplyRepository"></param>
+        /// <param name="sessionContext"></param>
+        /// <param name="knowledgeDomainService"></param>
+        /// <param name="mapper"></param>
+        public KnowApplication(IKnowledgeRepository knowledgeRepository, IRepository<KnowledgeApply> knowledgeApplyRepository, ISessionContext sessionContext, IMapper mapper, IRepository<KnowledgeType> knowledgeTypeRepository, IRepository<Hotspot> hotspotTypeRepository, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgePv> knowledgePvepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
+        {
+            _knowledgeRepository = knowledgeRepository;
+            _knowledgeApplyRepository = knowledgeApplyRepository;
+            _sessionContext = sessionContext;
+            _mapper = mapper;
+            _knowledgeTypeRepository = knowledgeTypeRepository;
+            _hotspotTypeRepository = hotspotTypeRepository;
+            _knowledgeWordRepository = knowledgeWordRepository;
+            _knowledgePvepository = knowledgePvepository;
+            _knowledgeHotWordRepository = knowledgeHotWordRepository;
+        }
+
+        /// <summary>
+        /// 知识库查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        public async Task<(int, IList<KnowledgeDataDto>)> GetKnowList(KnowPagedListDto pagedDto, CancellationToken cancellationToken)
+        {
+            var typeSpliceName = string.Empty;
+            var hotspotHotSpotFullName = string.Empty;
+            if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
+            {
+                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
+                typeSpliceName = type?.SpliceName;
+            }
+            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
+            {
+                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
+                hotspotHotSpotFullName = hotspot?.HotSpotFullName;
+            }
+            var (total, temp) = await _knowledgeRepository.Queryable(false, false, false)
+                .Includes(x => x.User)
+                .Includes(x => x.SystemOrganize)
+                .Includes(x => x.SourceOrganize)
+                .Includes(x => x.HotspotType)
+                .Includes(x => x.Workflow)
+                .Includes(x => x.KnowledgeType)
+                .Where(x => x.IsDeleted == false)
+                .Where(x => x.KnowledgeType.Any(t => t.KnowledgeType.KnowledgeTypeOrgs.Any(to => to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
+                .Where(x => (x.Status == EKnowledgeStatus.Drafts && x.CreatorId == _sessionContext.UserId) || (x.Status != EKnowledgeStatus.Drafts))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title), x => x.Title.Contains(pagedDto.Title!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), x => x.Title.Contains(pagedDto.Keyword!) || x.CreatorName.Contains(pagedDto.Keyword!) || x.CreatorOrgName.Contains(pagedDto.Keyword!) || x.SourceOrganize.Name.Contains(pagedDto.Keyword!))
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status != EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status && ((x.ExpiredTime != null && x.ExpiredTime > DateTime.Now) || x.ExpiredTime == null))
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status || (x.ExpiredTime != null && x.ExpiredTime < DateTime.Now && x.Status != EKnowledgeStatus.Drafts))
+                .WhereIF(pagedDto.IsPublic.HasValue, x => x.IsPublic == pagedDto.IsPublic)
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Summary), x => x.Summary != null && x.Summary.Contains(pagedDto.Summary!))
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.SourceOrganizeId != null && x.SourceOrganizeId.EndsWith(pagedDto.CreateOrgId!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.ModuleCode), x => x.Workflow.ModuleCode == pagedDto.ModuleCode)
+                .OrderByDescending(d => d.CreationTime)
+                .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+            //返回数据
+            return (total, _mapper.Map<IList<KnowledgeDataDto>>(temp));
+        }
+
+        /// <summary>
+        /// 知识申请查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyList(KnowledgeApplyPagedDto pagedDto, CancellationToken cancellationToken)
+        {
+            var (total, items) = await _knowledgeApplyRepository
+               .Queryable()
+               .Includes(it => it.User)
+               .Includes(it => it.SystemOrganize)
+               .Where(d => d.CreatorId == _sessionContext.RequiredUserId)
+               .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
+               .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
+               .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
+               .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
+               .WhereIF(pagedDto.StartTime.HasValue, d => d.CreationTime >= pagedDto.StartTime)
+               .WhereIF(pagedDto.EndTime.HasValue, d => d.CreationTime <= pagedDto.EndTime)
+               .OrderByDescending(p => p.CreationTime)
+               .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+
+            return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
+        }
+
+        /// <summary>
+        /// 申请处理查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken)
+        {
+            var (total, items) = await _knowledgeApplyRepository
+                 .Queryable()
+                 .Includes(it => it.User)
+                 .Includes(it => it.SystemOrganize)
+                 .Where(p => p.DepartmentId == _sessionContext.RequiredOrgId)
+                 .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), d => d.User.Name.Contains(pagedDto.Keyword!) || d.SystemOrganize.Name.Contains(pagedDto.Keyword!))
+                 .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
+
+                 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime <= pagedDto.EndTime)
+                 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime <= pagedDto.EndTime)
+                  .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime <= pagedDto.EndTime)
+
+                 .OrderByDescending(p => p.CreationTime)
+                 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+
+            return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
+        }
+
+        public async Task<Dictionary<string, Stream>> KnowledgeInfoListExportAsync(KnowledgeInfoExportInDto dto, CancellationToken cancellationToken)
+        {
+            var streamList = new Dictionary<string, Stream>();
+            var knowList = await _knowledgeRepository.Queryable()
+                .Where(m => dto.Ids.Contains(m.Id))
+                .Select(m => new { m.Title, m.Content })
+                .ToListAsync(cancellationToken);
+            foreach (var item in knowList)
+            {
+                Stream stream = item.Content.HtmlToStream(dto.FileType);
+                streamList.Add(item.Title + dto.FileType.GetFileExtension(), stream);
+            }
+            return streamList;
+        }
+
+        public async Task<IList<string>> TitleParticiple(IList<string> keywords, string title)
+        {
+            keywords ??= new List<string>();
+            var splitWords = new Segment().DoSegment(title);
+            for (int i = 0;i < splitWords.Count;i++)
+            {
+                var word = splitWords.ElementAt(i);
+                if (word is not { WordType: WordType.SimplifiedChinese, Word.Length: > 1 }) continue;
+
+                var tag = splitWords.ElementAt(i).Word;
+                var isExist = await _knowledgeWordRepository.Queryable().Where(m => m.Tag == tag).AnyAsync();
+                if (isExist) continue;
+
+                var wordEntity = new KnowledgeWord
+                {
+                    Tag = tag,
+                    Classify = "普通标签",
+                    Remark = "系统自动从知识标题分词",
+                    Synonym = "",
+                    IsEnable = 1
+                };
+                var keyId = await _knowledgeWordRepository.AddAsync(wordEntity);
+                keywords.Add(keyId);
+            }
+            return keywords;
+        }
+
+        public async Task<(int, IList<PageViewOutDto>)> GetPageViewListAsync(PageViewInDto dto, CancellationToken requestAborted = default)
+        {
+            var query = _knowledgePvepository.Queryable()
+                .LeftJoin<KnowledgeBase.Knowledge>((p, k) => p.KnowledgeCode == k.Code)
+                .LeftJoin<KnowledgeRelationType>((p, k, r) => r.KnowledgeId == k.Id)
+                .WhereIF(dto.Title.NotNullOrEmpty(), (p, k, r) => k.Title.Contains(dto.Title))
+                .WhereIF(dto.CreatorName.NotNullOrEmpty(), (p, k, r) => p.CreatorName.Contains(dto.CreatorName))
+                .WhereIF(dto.KnowledgeTypeId.NotNullOrEmpty(), (p, k, r) => r.KnowledgeTypeId == dto.KnowledgeTypeId)
+                .OrderByDescending((p, k, r) => p.CreationTime);
+
+            if (_sessionContext.OrgIsCenter == false)
+                query = query.Where((p, k, r) => p.CreatorOrgId.StartsWith(_sessionContext.OrgId));
+
+            return await query.Select<PageViewOutDto>().ToPagedListAsync(dto.PageIndex, dto.PageSize, requestAborted);
+        }
+
+        public async Task<(int total, IList<KnowledgeWordOutDto> items)> GetKnowledgeHotWordListAsync(KnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var query = _knowledgeHotWordRepository.Queryable()
+                .WhereIF(dto.IsEnable != null, m => m.IsEnable == dto.IsEnable)
+                .OrderByDescending(m => m.Sort)
+                .OrderByDescending(m => m.CreationTime);
+            return await query.Select<KnowledgeWordOutDto>().ToPagedListAsync(dto.PageIndex, dto.PageSize, requestAborted);
+        }
+
+        public async Task UpdateKnowledgeHotWordAsync(UpdateKnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var entity = await _knowledgeHotWordRepository.GetAsync(dto.Id)
+                ?? throw UserFriendlyException.SameMessage("数据不存在");
+            dto.Adapt(entity);
+            await _knowledgeHotWordRepository.UpdateAsync(entity);
+        }
+
+        public async Task AddKnowledgeHotWordAsync(AddKnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var isExist = await _knowledgeHotWordRepository.Queryable()
+                .Where(m => m.KeyWord == dto.KeyWord).AnyAsync();
+            if (isExist) throw UserFriendlyException.SameMessage("热词已存在");
+            var entity = dto.Adapt<KnowledgeHotWord>();
+            await _knowledgeHotWordRepository.AddAsync(entity, requestAborted);
+        }
+    }
 }

+ 17 - 1
src/Hotline.Application/Systems/BaseDataApplication.cs

@@ -1,7 +1,9 @@
-using Hotline.Caching.Interfaces;
+using DocumentFormat.OpenXml.Wordprocessing;
+using Hotline.Caching.Interfaces;
 using Hotline.Repository.SqlSugar.System;
 using Hotline.Settings;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.Order;
 using J2N.Collections.ObjectModel;
@@ -16,6 +18,7 @@ using System.Threading.Tasks;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using XF.Utility.EnumExtensions;
+using static Lucene.Net.Util.Fst.Util;
 
 namespace Hotline.Application.Systems;
 public class BaseDataApplication : IScopeDependency
@@ -141,4 +144,17 @@ public class BaseDataApplication : IScopeDependency
         Add(typeof(EAttitudeType));
         return this;
     }
+
+    public BaseDataApplication FileType(int ignoreKey)
+    {
+        var items = EnumExts.GetDescriptions<EFileType>();
+        _baseData.TryAdd("FileType", items.Where(m => m.Key != ignoreKey).ToList());
+        return this;
+    }
+
+    public BaseDataApplication Add(string name, List<KeyValuePair<int, string>> items)
+    {
+        _baseData.TryAdd(name, items);
+        return this;
+    }
 }

+ 33 - 0
src/Hotline.Application/Tools/StreamExtensions.cs

@@ -0,0 +1,33 @@
+using Hotline.Share.Enums.Article;
+using Hotline.Share.Tools;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Application.Tools;
+public static class StreamExtensions
+{
+    public static FileStreamResult GetPDFFile(this Stream stream, string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.pdf, fileName);
+    }
+
+    public static FileStreamResult GetExcelFile(this Stream stream, string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.excel, fileName);
+    }
+
+    public static FileStreamResult GetWordFile(this Stream stream,  string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.word, fileName);
+    }
+
+    public static FileStreamResult GetFileStreamResult(this Stream stream, EFileType fileType, string fileName = "")
+    {
+        var tail = DateTime.Now.ToString("yyyyMMddhhmmss") + fileType.GetFileExtension();
+        fileName = fileName.IsNullOrEmpty() ? tail : $"{fileName}_{tail}";
+        return new FileStreamResult(stream, fileType.GetContentType())
+        {
+            FileDownloadName = fileName
+        };
+    }
+
+}

+ 23 - 0
src/Hotline.Application/Tools/StringExtensions.cs

@@ -0,0 +1,23 @@
+using Hotline.Application.ExportWord;
+using Hotline.Share.Enums.Article;
+
+namespace Hotline.Application.Tools;
+public static class StringExtensions
+{
+    public static Stream HtmlToWord(this string value)
+    {
+        return WordHelper.ConvertHtmlToWord(value);
+    }
+
+    public static Stream HtmlToPDF(this string value)
+    {
+        return WordHelper.ConvertHtmlToPdf(value);
+    }
+
+    public static Stream HtmlToStream(this string value, EFileType fileType)
+    {
+        if (fileType == EFileType.pdf) value.HtmlToPDF();
+        if (fileType == EFileType.word) value.HtmlToWord();
+        throw new NotImplementedException($"无效的 fileType 入参: {fileType}");
+    }
+}

+ 10 - 0
src/Hotline.Share/Attributes/ContentTypeAttribute.cs

@@ -0,0 +1,10 @@
+namespace Hotline.Share.Attributes;
+
+internal class ContentTypeAttribute : Attribute
+{
+    public string ContentType { get; }
+    public ContentTypeAttribute(string name)
+    {
+        ContentType = name;
+    }
+}

+ 10 - 0
src/Hotline.Share/Attributes/FileExtensionAttribute.cs

@@ -0,0 +1,10 @@
+namespace Hotline.Share.Attributes;
+
+internal class FileExtensionAttribute : Attribute
+{
+    public string FileExtension { get; }
+    public FileExtensionAttribute(string name)
+    {
+        FileExtension = name;
+    }
+}

+ 3 - 0
src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs

@@ -49,7 +49,10 @@ public class StartEndTimeDto
 
 public record PagedStartEndTimeDto : PagedRequest 
 {
+    [Required]
     public DateTime StartTime { get; set; }
+
+    [Required]
     public DateTime EndTime { get; set; }
 }
 

+ 1 - 1
src/Hotline.Share/Dtos/Knowledge/KnowledgeCollectDto.cs

@@ -57,7 +57,7 @@ namespace Hotline.Share.Dtos.Knowledge
 		/// <summary>
 		/// 分组Id
 		/// </summary>
-		public string KnowledgeCollectGroupId { get; set; }
+		public string? KnowledgeCollectGroupId { get; set; }
 	}
 
 	public class KnowledgeCollectDeleteDto

+ 101 - 1
src/Hotline.Share/Dtos/Knowledge/KnowledgeDto.cs

@@ -1,8 +1,11 @@
-using Hotline.Share.Dtos.File;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Org;
 using Hotline.Share.Dtos.Quality;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
+using Hotline.Share.Notifications.NewRockCallCenter;
 using Hotline.Share.Requests;
 using System.ComponentModel.DataAnnotations;
 using XF.Utility.EnumExtensions;
@@ -360,4 +363,101 @@ namespace Hotline.Share.Dtos.Knowledge
 
     public record KnowledgeCollectGroupListInDto : PagedKeywordRequest
     { }
+
+    public class KnowledgeInfoExportInDto
+    {
+        /// <summary>
+        /// 导入的知识Id集合
+        /// </summary>
+        public string[] Ids { get; set; }
+
+        /// <summary>
+        /// 导出格式
+        /// </summary>
+        public EFileType FileType { get; set; }
+    }
+
+    public record PageViewInDto : PagedStartEndTimeDto
+    {
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string? Title { get; set; }
+
+        /// <summary>
+        /// 浏览人
+        /// </summary>
+        public string? CreatorName { get; set; }
+
+        /// <summary>
+        /// 知识分类Id
+        /// </summary>
+        public string? KnowledgeTypeId { get; set; }
+    }
+
+    public class PageViewOutDto
+    {
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string KnowledgeTitle { get; set; }
+
+        /// <summary>
+        /// 浏览人
+        /// </summary>
+        public string CreatorName { get; set; }
+
+        /// <summary>
+        /// 浏览时间
+        /// </summary>
+        public DateTime CreationTime { get; set; }
+    }
+
+    public class KnowledgeWordOutDto
+    {
+        /// <summary>
+        /// 标签
+        /// </summary>
+        public string Tag { get; set; }
+    }
+
+    public record KnowledgeHotWordInDto : PagedStartEndTimeDto
+    {
+        /// <summary>
+        /// 是否启用
+        /// </summary>
+        public bool? IsEnable { get; set; }
+    }
+
+    public class UpdateKnowledgeHotWordInDto : AddKnowledgeHotWordInDto
+    { 
+        /// <summary>
+        /// Id
+        /// </summary>
+        [Required]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 点击次数
+        /// </summary>
+        public int? ClickCount { get; set; }
+
+        /// <summary>
+        /// 是否启用
+        /// </summary>
+        public bool? IsEnable { get; set; }
+    }
+
+    public class AddKnowledgeHotWordInDto
+    {
+        /// <summary>
+        /// 热词
+        /// </summary>
+        public string? KeyWord { get; set; }
+
+        /// <summary>
+        /// 排序
+        /// </summary>
+        public int? Sort { get; set; }
+    }
 }

+ 31 - 0
src/Hotline.Share/Enums/Article/EFileType.cs

@@ -0,0 +1,31 @@
+using Hotline.Share.Attributes;
+using System.ComponentModel;
+
+namespace Hotline.Share.Enums.Article;
+public enum EFileType
+{
+    /// <summary>
+    /// Excel文件
+    /// </summary>
+    [Description("Excel文件")]
+    [FileExtension(".xlsx")]
+    [ContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
+    excel,
+
+    /// <summary>
+    /// Word文件
+    /// </summary>
+    [Description("Word文件")]
+    [FileExtension(".docx")]
+    [ContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")]
+    word,
+
+
+    /// <summary>
+    /// PDF文件
+    /// </summary>
+    [Description("PDF文件")]
+    [FileExtension(".pdf")]
+    [ContentType("application/pdf")]
+    pdf,
+}

+ 45 - 0
src/Hotline.Share/Tools/EnumExtensions.cs

@@ -0,0 +1,45 @@
+using Hotline.Share.Attributes;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Tools;
+public static class EnumExtensions
+{
+    /// <summary>
+    /// 获取枚举项上的<see cref="FileExtensionAttribute"/>特性的文字描述
+    /// </summary>
+    /// <param name="value">枚举</param>
+    /// <returns>属性值</returns>
+    public static string GetFileExtension(this Enum value)
+    {
+        var type = value.GetType();
+        var member = type.GetMember(value.ToString()).FirstOrDefault();
+        var desc = member?.GetAttribute<FileExtensionAttribute>();
+        if (desc != null)
+        {
+            return desc.FileExtension;
+        }
+        return string.Empty;
+    }
+
+    /// <summary>
+    /// 获取枚举项上的<see cref="ContentTypeAttribute"/>特性的文字描述
+    /// </summary>
+    /// <param name="value">枚举</param>
+    /// <returns>属性值</returns>
+    public static string GetContentType(this Enum value)
+    {
+        var type = value.GetType();
+        var member = type.GetMember(value.ToString()).FirstOrDefault();
+        var desc = member?.GetAttribute<ContentTypeAttribute>();
+        if (desc != null)
+        {
+            return desc.ContentType;
+        }
+        return string.Empty;
+    }
+}

+ 13 - 1
src/Hotline.Share/Tools/StringExtensions.cs

@@ -1,4 +1,6 @@
-namespace Hotline.Share.Tools;
+using System.Text.RegularExpressions;
+
+namespace Hotline.Share.Tools;
 public static class StringExtensions
 {
     public static bool IsNullOrEmpty(this string? str)
@@ -27,4 +29,14 @@ public static class StringExtensions
         // 尝试解析字符串为枚举
         return (TEnum)Enum.Parse(typeof(TEnum), value);
     }
+
+    public static IList<string> SplitKeywords(this string value)
+    {
+        var regex = new Regex(@"[ ,,]+");
+
+        return regex.Split(value)
+                    .Where(s => !string.IsNullOrWhiteSpace(s))
+                    .Select(s => s.Trim())
+                    .ToList();
+    }
 }

+ 2 - 2
src/Hotline.Share/Tools/TupleExtensions.cs

@@ -8,8 +8,8 @@ using System.Threading.Tasks;
 namespace Hotline.Share.Tools;
 public static class TupleExtensions
 {
-    public static PagedDto<T> ToPaged<T>(this (int, List<T>) tuple)
+    public static PagedDto<T> ToPaged<T>(this (int, IList<T>) tuple)
     {
-        return new PagedDto<T>(tuple.Item1, tuple.Item2);
+        return new PagedDto<T>(tuple.Item1, tuple.Item2.ToList());
     }
 }

+ 13 - 0
src/Hotline.Share/Tools/TypeExtensions.cs

@@ -14,4 +14,17 @@ public static class TypeExtensions
     {
         return memberInfo.IsDefined(typeof(T), inherit);
     }
+
+    /// <summary>
+    /// 从类型成员获取指定Attribute特性
+    /// </summary>
+    /// <typeparam name="T">Attribute特性类型</typeparam>
+    /// <param name="memberInfo">类型类型成员</param>
+    /// <param name="inherit">是否从继承中查找</param>
+    /// <returns>存在返回第一个,不存在返回null</returns>
+    public static T GetAttribute<T>(this MemberInfo memberInfo, bool inherit = false) where T : Attribute
+    {
+        var descripts = memberInfo.GetCustomAttributes(typeof(T), inherit);
+        return descripts.FirstOrDefault() as T;
+    }
 }

+ 34 - 0
src/Hotline/KnowledgeBase/KnowledgeHotWord.cs

@@ -0,0 +1,34 @@
+using SqlSugar;
+using System.ComponentModel;
+using XF.Domain.Repository;
+
+namespace Hotline.KnowledgeBase;
+
+[Description("知识热词")]
+public class KnowledgeHotWord : CreationEntity
+{
+    /// <summary>
+    /// 关键字
+    /// </summary>
+    [SugarColumn(ColumnDescription = "关键字")]
+    public string KeyWord { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序")]
+    public int Sort { get; set; }
+
+    /// <summary>
+    /// 搜索次数
+    /// </summary>
+    [SugarColumn(ColumnDescription = "搜索次数")]
+    public int SearchCount { get; set; }
+
+    /// <summary>
+    /// 是否启用
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否启用")]
+    public bool IsEnable { get; set; }
+}
+

+ 2 - 0
src/Hotline/KnowledgeBase/KnowledgeWord.cs

@@ -47,5 +47,7 @@ namespace Hotline.KnowledgeBase
 		/// </summary>
 		[SugarColumn(ColumnDescription = "启禁用  0  启用  1 禁用")]
 		public int IsEnable { get; set; }
+
+ 
 	}
 }