Kaynağa Gözat

Merge branch 'dev'

xf 1 yıl önce
ebeveyn
işleme
9c59af1436
33 değiştirilmiş dosya ile 1417 ekleme ve 80 silme
  1. 77 52
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  2. 1 1
      src/Hotline.Api/Controllers/EnforcementOrderController.cs
  3. 87 0
      src/Hotline.Api/Controllers/ExportWordController.cs
  4. 65 10
      src/Hotline.Api/Controllers/OrderController.cs
  5. 6 0
      src/Hotline.Api/Hotline.Api.csproj
  6. BIN
      src/Hotline.Api/Template/宜宾交办单模版.doc
  7. 11 0
      src/Hotline.Application/ExportWord/IWordExportProvider.cs
  8. 15 0
      src/Hotline.Application/ExportWord/IWordExportService.cs
  9. 69 0
      src/Hotline.Application/ExportWord/IWordExportTemplateExtensions.cs
  10. 32 0
      src/Hotline.Application/ExportWord/IWordHelperService.cs
  11. 297 0
      src/Hotline.Application/ExportWord/WordExportProvider.cs
  12. 48 0
      src/Hotline.Application/ExportWord/WordExportService.cs
  13. 91 0
      src/Hotline.Application/ExportWord/WordHelper.cs
  14. 71 0
      src/Hotline.Application/ExportWord/WordHelperService.cs
  15. 6 7
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  16. 3 1
      src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs
  17. 1 0
      src/Hotline.Application/Hotline.Application.csproj
  18. 9 2
      src/Hotline.Application/Mappers/MapperConfigs.cs
  19. 66 0
      src/Hotline.Share/Dtos/ExportWord/ExportByteWord.cs
  20. 51 0
      src/Hotline.Share/Dtos/ExportWord/ExportPicture.cs
  21. 118 0
      src/Hotline.Share/Dtos/ExportWord/ExportTable.cs
  22. 10 0
      src/Hotline.Share/Dtos/ExportWord/IWordExportTemplate.cs
  23. 12 0
      src/Hotline.Share/Dtos/ExportWord/PlaceholderAttribute.cs
  24. 5 0
      src/Hotline.Share/Dtos/FlowEngine/Workflow/WorkflowTraceDto.cs
  25. 63 0
      src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs
  26. 147 0
      src/Hotline.Share/Dtos/OrderExportWord/OrderSubmissionForm.cs
  27. 2 5
      src/Hotline.Share/Dtos/Position.cs
  28. 11 0
      src/Hotline.Share/Enums/ExportWord/EPlaceholderType.cs
  29. 1 0
      src/Hotline.Share/Hotline.Share.csproj
  30. 12 1
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  31. 10 0
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  32. 6 0
      src/Hotline/FlowEngine/Workflows/WorkflowTrace.cs
  33. 14 1
      src/Hotline/Orders/Order.cs

+ 77 - 52
src/Hotline.Api/Controllers/Bi/BiOrderController.cs

@@ -92,36 +92,51 @@ namespace Hotline.Api.Controllers.Bi
             _workflowStepHandleRepository = workflowStepHandleRepository;
         }
 
-        /// <summary>
-        /// 部门超期统计明细
-        /// </summary>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        [HttpGet("org_data_list_detail")]
-        public async Task<PagedDto<OrderDto>> OrgDataListDetail([FromQuery] OrgDataListDetailRequest dto)
-        {
-            dto.EndTime = dto.EndTime.AddDays(1).AddSeconds(-1);
-
-            var (total, items) = await _orderRepository.Queryable()
-            .Where(x => x.CreationTime >= dto.StartTime && x.CreationTime <= dto.EndTime)
-            .WhereIF(dto.QueryType == 1, x => x.Status >= EOrderStatus.Filed && x.ExpiredTime < x.FiledTime) //业务已办超期
-            //.WhereIF(dto.QueryType== 2,) //会签已办超期
-            .WhereIF(dto.QueryType == 3, x => x.Status < EOrderStatus.Filed && x.ExpiredTime < SqlFunc.GetDate()) //业务待办超期
-            .WhereIF(string.IsNullOrEmpty(dto.OrgCode), x => x.ActualHandleOrgCode == dto.OrgCode)
-            //.WhereIF(dto.QueryType ==4,) //会签待办超期
-            //.MergeTable();
-            .ToPagedListAsync(dto.PageIndex, dto.PageSize);
-
-            return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
-        }
-
-
-        /// <summary>
-        /// 部门超期统计
-        /// </summary>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        [HttpGet("org_data_list")]
+		/// <summary>
+		/// 部门超期统计明细
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("org_data_list_detail")]
+		public async Task<PagedDto<OrderDto>> OrgDataListDetail([FromQuery] OrgDataListDetailRequest dto)
+		{
+			dto.EndTime = dto.EndTime.AddDays(1).AddSeconds(-1);
+
+			var quer = _orderRepository.Queryable()
+					.Where(x => x.CreationTime >= dto.StartTime && x.CreationTime <= dto.EndTime)
+					.WhereIF(dto.QueryType == 1, x => x.Status >= EOrderStatus.Filed && x.ExpiredTime < x.FiledTime) //业务已办超期
+					.WhereIF(dto.QueryType == 3, x => x.Status < EOrderStatus.Filed && x.ExpiredTime < SqlFunc.GetDate()) //业务待办超期
+					.WhereIF(dto.QueryType == 5, x =>
+						(x.Status >= EOrderStatus.Filed && x.ExpiredTime < x.FiledTime) || (x.Status < EOrderStatus.Filed && x.ExpiredTime < SqlFunc.GetDate()))
+					.WhereIF(!string.IsNullOrEmpty(dto.OrgCode) && dto.QueryType is  1 or 3, x => x.ActualHandleOrgCode == dto.OrgCode);
+
+			if (dto.QueryType is 2 or 4 or 5)
+			{
+                var queryCountersign = _workflowCountersignRepository.Queryable()
+                    .LeftJoin<WorkflowCountersignMember>((x, o) => x.Id == o.WorkflowCountersignId)
+                    .Where((x, o) => x.CreationTime >= dto.StartTime && x.CreationTime <= dto.EndTime)
+                    .WhereIF(dto.QueryType == 2, (x, o) => o.IsHandled == true) //会签已办超期
+                    .WhereIF(dto.QueryType == 4, (x, o) => o.IsHandled == false) //会签待办超期
+                    .WhereIF(!string.IsNullOrEmpty(dto.OrgCode) && dto.QueryType is 2 or 4 or 5, (x, o) => o.Key == dto.OrgCode)
+                    //.GroupBy((x,o)=>x.WorkflowId)
+                    .Select((x,o)=> new { Id= x.WorkflowId })
+                    .MergeTable();
+
+				quer = quer.InnerJoin(queryCountersign, (x, c) => x.WorkflowId == c.Id);
+                   
+			}
+			var (total, items) = await quer.ToPagedListAsync(dto.PageIndex, dto.PageSize);
+
+			return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
+		}
+
+
+		/// <summary>
+		/// 部门超期统计
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("org_data_list")]
         public async Task<PagedDto<OrderBiOrgDataListVo>> OrgDataList([FromQuery] ReportPagedRequest dto)
         {
             if (!dto.StartTime.HasValue || !dto.EndTime.HasValue) throw UserFriendlyException.SameMessage("请选择时间!");
@@ -145,22 +160,6 @@ namespace Hotline.Api.Controllers.Bi
                     NoHandlerExtendedNum = SqlFunc.AggregateSum(SqlFunc.IIF(o.Status < EOrderStatus.Filed && o.ExpiredTime < SqlFunc.GetDate(), 1, 0)),
                 }).MergeTable();
 
-
-
-            //var queryOrder = _orderRepository.Queryable(false, false, false)
-            //    .LeftJoin<SystemOrganize>((x, o) => x.ActualHandleOrgCode == o.Id)
-            //    .WhereIF(dto.StartTime.HasValue, (x, o) => x.CreationTime >= dto.StartTime)
-            //    .WhereIF(dto.EndTime.HasValue, (x, o) => x.CreationTime <= dto.EndTime)
-            //    .WhereIF(IsCenter == false, (x, o) => x.ActualHandleOrgCode == _sessionContext.RequiredOrgId)
-            //    .GroupBy((x, o) => new { x.ActualHandleOrgCode, o.Name })
-            //    .Select((x, o) => new OrderBiOrgDataListVo
-            //    {
-            //        OrgName = o.Name,
-            //        OrgId = x.ActualHandleOrgCode,
-            //        HandlerExtendedNum = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed && x.ExpiredTime < x.FiledTime, 1, 0)),
-            //        NoHandlerExtendedNum = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status < EOrderStatus.Filed && x.ExpiredTime < SqlFunc.GetDate(), 1, 0)),
-            //    }).MergeTable();
-
             var queryCountersign = _workflowCountersignRepository.Queryable()
                 .LeftJoin<WorkflowCountersignMember>((x, o) => x.Id == o.WorkflowCountersignId)
                 .WhereIF(dto.StartTime.HasValue, x => x.CreationTime >= dto.StartTime)
@@ -509,12 +508,38 @@ namespace Hotline.Api.Controllers.Bi
             return list;
         }
 
-        /// <summary>
-        /// 特提统计
-        /// </summary>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        [HttpGet("special_data_list")]
+		/// <summary>
+		/// 部门延期统计明细
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("order-delay-data-detail")]
+        public async Task<PagedDto<OrderDelayDto>> QueryOrderDelayDataDetail([FromQuery] QueryOrderDelayDataDetailRequest dto)
+        {
+	        if (!dto.StartTime.HasValue || !dto.EndTime.HasValue) throw UserFriendlyException.SameMessage("请选择时间!");
+
+	        dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+	        var (total, items) = await _orderDelayRepository.Queryable()
+                .Includes(x=>x.Order)
+		        .WhereIF(dto.StartTime.HasValue, x => x.CreationTime >= dto.StartTime)
+		        .WhereIF(dto.EndTime.HasValue, x => x.CreationTime <= dto.EndTime)
+		        .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), x => x.ApplyOrgCode == dto.OrgCode)
+                .WhereIF(dto.Type is 1, x=> x.DelayState == EDelayState.Pass)
+                .WhereIF(dto.Type is 2, x => x.DelayState == EDelayState.NoPass)
+                .WhereIF(dto.Type is 3, x => x.DelayState == EDelayState.Examining)
+                .WhereIF(dto.Type is 4, x => x.DelayState < EDelayState.Withdraw)
+                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
+			return new PagedDto<OrderDelayDto>(total, _mapper.Map<IReadOnlyList<OrderDelayDto>>(items));
+		}
+
+		/// <summary>
+		/// 特提统计
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("special_data_list")]
         public async Task<PagedDto<OrderBiSpecialListVo>> SpecialDataList([FromQuery] ReportPagedRequest dto)
         {
             if (!dto.StartTime.HasValue || !dto.EndTime.HasValue) throw UserFriendlyException.SameMessage("请选择时间!");

+ 1 - 1
src/Hotline.Api/Controllers/EnforcementOrderController.cs

@@ -37,7 +37,7 @@ namespace Hotline.Api.Controllers
         /// <param name="enforcementOrdersRepository"></param>
         /// <param name="judicialComplaintsEventTypeRepository"></param>
         /// <param name="enforcementHotspotRepository"></param>
-        /// <param name="mapper"></param>
+        /// <param name="mapper"></param> 
         /// <param name="workflowApplication"></param>
         /// <param name="sysDicDataCacheManager"></param>
         /// <param name="systemOrganizeRepository"></param>

+ 87 - 0
src/Hotline.Api/Controllers/ExportWordController.cs

@@ -0,0 +1,87 @@
+using Hotline.Application.ExportWord;
+using Hotline.Orders;
+using Hotline.Share.Dtos.OrderExportWord;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Repository;
+
+namespace Hotline.Api.Controllers
+{
+    public class ExportWordController : BaseController
+    {
+        private readonly IOrderRepository _orderRepository;
+        private readonly IWordHelperService _wordHelperService;
+        private readonly IMapper _mapper;
+        private readonly IRepository<OrderVisit> _orderVisitRepository;
+        private readonly IRepository<OrderVisitDetail> _orderVisitedDetailRepository;
+        private readonly ILogger<ExportWordController> _logger;
+
+        public ExportWordController(IOrderRepository orderRepository,
+            IWordHelperService wordHelperService,
+            IMapper mapper,
+           IRepository<OrderVisit> orderVisitRepository,
+           IRepository<OrderVisitDetail> orderVisitedDetailRepository,
+           ILogger<ExportWordController> logger)
+        {
+            _orderRepository = orderRepository;
+            _wordHelperService = wordHelperService;
+            _mapper = mapper;
+            _orderVisitRepository = orderVisitRepository;
+            _orderVisitedDetailRepository = orderVisitedDetailRepository;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// 工单交办单导出
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("order_submission_form")]
+        public async Task<IActionResult> OrderSubmissionForm([FromBody] List<string> Ids)
+        {
+            var streams = new Dictionary<string, Stream>();
+            var path = $"{Directory.GetCurrentDirectory()}/Template/宜宾交办单模版.doc";
+
+            foreach (var item in Ids)
+            {
+                var order = await _orderRepository.GetAsync(item, HttpContext.RequestAborted);
+                if (order == null)
+                    continue;
+
+                var exportTest = _mapper.Map<OrderSubmissionForm>(order);
+
+                //查询回访信息
+                var visitData = await _orderVisitRepository.GetAsync(p => p.OrderId == order.Id, HttpContext.RequestAborted);
+                if (visitData != null)
+                {
+                    //回访明细
+                    var visitDetail = await _orderVisitedDetailRepository.Queryable().Where(p => p.VisitId == visitData.Id && p.VisitTarget == Share.Enums.Order.EVisitTarget.Org).ToListAsync();
+                    string visit = "";
+                    foreach (var itemVisit in visitDetail)
+                    {
+                        visit += "回访部门:" + itemVisit.VisitOrgName;
+                        visit += " \n办件结果:" + itemVisit.OrgProcessingResults?.Value + "    办事态度:" + itemVisit.OrgHandledAttitude?.Value + "\n";
+
+                        if (itemVisit.VisitOrgCode == order.ActualHandleOrgCode)
+                            exportTest.VisitContent = "回访内容:" + itemVisit.VisitContent;
+                    }
+                    exportTest.VisitOrg = visit;
+                }
+                if (Ids.Count > 1)
+                    streams.Add(order.No + path.Substring(path.LastIndexOf(".")), _wordHelperService.WordStream(path, exportTest));
+                else
+                {
+                    var btyes = _wordHelperService.WordByte(path, exportTest);
+                    HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+                    return File(btyes, "application/vnd.ms-word", order.No + path.Substring(path.LastIndexOf(".")));
+                }
+            }
+
+            //调用压缩方法 进行压缩 (接收byte[] 数据)
+            byte[] fileBytes = _wordHelperService.ConvertZipStream(streams);
+            HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+            var name = DateTime.Now.ToString("yyyyMMddHHmmss");
+            return File(fileBytes, "application/octet-stream", $"{name}.zip");
+        }
+
+    }
+}

+ 65 - 10
src/Hotline.Api/Controllers/OrderController.cs

@@ -20,6 +20,7 @@ using Hotline.Push.FWMessage;
 using Hotline.Push.Notifies;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Repository.SqlSugar.Ts;
+using Hotline.SeedData;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
 using Hotline.Settings.TimeLimits;
@@ -43,6 +44,7 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using MiniExcelLibs;
 using SqlSugar;
+using System;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
 using XF.Domain.Constants;
@@ -2626,8 +2628,11 @@ public class OrderController : BaseController
         else if (dto.FlowDirection is EFlowDirection.CenterToOrg)
         {
             expiredTimeConfig = _timeLimitDomainService.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.AcceptTypeCode);
-            order.CenterToOrg(expiredTimeConfig.TimeText, expiredTimeConfig.Count,
-                expiredTimeConfig.TimeType, expiredTimeConfig.ExpiredTime, expiredTimeConfig.NearlyExpiredTime);
+            order.CenterToOrg(
+                expiredTimeConfig.TimeText, expiredTimeConfig.Count,
+                expiredTimeConfig.TimeType, expiredTimeConfig.ExpiredTime, 
+                expiredTimeConfig.NearlyExpiredTime, dto.Opinion, 
+                _sessionContext.RequiredUserId, _sessionContext.UserName);
             //写入质检
             await _qualityApplication.AddQualityAsync(EQualitySource.Send, order.Id, HttpContext.RequestAborted);
         }
@@ -2869,14 +2874,64 @@ public class OrderController : BaseController
             .ToPagedListAsync(dto, HttpContext.RequestAborted);
 
         return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
-    }
+	}
 
-    /// <summary>
-    /// 首页查询
-    /// </summary>
-    /// <param name="dto"></param>
-    /// <returns></returns>
-    [HttpGet("waited/home")]
+
+	/// <summary>
+	/// 查询中心待办
+	/// </summary>
+	[HttpGet("waited/center")]
+	public async Task<PagedDto<OrderDto>> QueryWaitedForCenter([FromQuery] QueryOrderWaitedCenterDto dto) 
+    {
+        if (dto.EndCreationTime.HasValue)
+            dto.EndCreationTime = dto.EndCreationTime.Value.AddDays(1).AddSeconds(-1);
+        if (dto.StartTimeEnd.HasValue)
+	        dto.StartTimeEnd = dto.StartTimeEnd.Value.AddDays(1).AddSeconds(-1);
+
+		var (total, items) = await _orderRepository.Queryable(viewFilter: false)
+            .Where(x => x.Workflow.Steps.Any(s => s.Status < EWorkflowStepStatus.Handled && s.StepHandlers.Any(d => d.OrgId == OrgSeedData.CenterId) ))
+            .Where(x => x.Source < ESource.MLSQ || x.Source > ESource.WZSC)
+			.Where(x => x.Status != EOrderStatus.BackToProvince)
+            .WhereIF(!string.IsNullOrEmpty(dto.No), x => x.No!.Contains(dto.No!))
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title!.Contains(dto.Title!))
+            .WhereIF(dto is { StCreationTime: not null, EndCreationTime: not null }, x => x.CreationTime >= dto.StCreationTime && x.CreationTime <= dto.EndCreationTime)
+            .WhereIF(dto is { StartTimeSt: not null, StartTimeEnd: not null }, x => x.CreationTime >= dto.StartTimeSt && x.CreationTime <= dto.StartTimeEnd)
+			.WhereIF(!string.IsNullOrEmpty(dto.StepName), x => x.Workflow.Steps.Any(s=>s.Name == dto.StepName))
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName), x => x.ActualHandleOrgName!.Contains(dto.ActualHandleOrgName!))
+            .WhereIF(dto.Status.HasValue, x => x.Status == dto.Status)
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptorName), x => x.AcceptorName!.Contains(dto.AcceptorName!))
+            .WhereIF(dto.ExpiredStatus is EExpiredStatus.Normal, x => DateTime.Now < x.NearlyExpiredTime)
+            .WhereIF(dto.ExpiredStatus is EExpiredStatus.GoingToExpired, x => DateTime.Now > x.NearlyExpiredTime && DateTime.Now < x.ExpiredTime)
+            .WhereIF(dto.ExpiredStatus is EExpiredStatus.Expired, x => DateTime.Now >= x.ExpiredTime)
+			.OrderBy(x => x.Status)
+			.OrderBy(x => x.CreationTime, OrderByType.Desc)
+			.ToPagedListAsync(dto, HttpContext.RequestAborted);
+
+		return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
+	}
+
+	/// <summary>
+	/// 查询中心待办基础数据
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("waited/center/base")]
+	public async Task<object> WaitedForCenterBaseData()
+	{
+		var rsp = new
+		{
+			OrderStatus = EnumExts.GetDescriptions<EOrderStatus>(),
+			ExpiredStatus = EnumExts.GetDescriptions<EExpiredStatus>(),
+            StepNames = new  string[] { "话务部", "派单组" }
+		};
+		return rsp;
+	}
+
+	/// <summary>
+	/// 首页查询
+	/// </summary>
+	/// <param name="dto"></param>
+	/// <returns></returns>
+	[HttpGet("waited/home")]
 
     public async Task<Object> QueryWaitedHome([FromQuery] QueryOrderWaitedDto dto) {
 
@@ -3038,7 +3093,7 @@ public class OrderController : BaseController
              .WhereIF(dto.AuditState == 1, d => d.State == ESendBackAuditState.Apply)
              .WhereIF(dto.AuditState == 2 && !dto.State.HasValue, d => d.State > ESendBackAuditState.Apply)
              .WhereIF(dto.AuditState == 2 && dto.State.HasValue, d => d.State == dto.State)
-             .WhereIF(_sessionContext.Roles.Contains("123") == false, x => x.SendBackOrgId == _sessionContext.OrgId);// 123 系统管理员
+             .WhereIF(_sessionContext.Roles.Contains("role_sysadmin") == false, x => x.SendBackOrgId == _sessionContext.OrgId);// 123 系统管理员
 
         var (total, items) = await query.OrderByDescending(x => x.CreationTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);

+ 6 - 0
src/Hotline.Api/Hotline.Api.csproj

@@ -24,4 +24,10 @@
     <ProjectReference Include="..\Hotline.Application\Hotline.Application.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <None Update="Template\宜宾交办单模版.doc">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
 </Project>

BIN
src/Hotline.Api/Template/宜宾交办单模版.doc


+ 11 - 0
src/Hotline.Application/ExportWord/IWordExportProvider.cs

@@ -0,0 +1,11 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Application.ExportWord
+{
+    public interface IWordExportProvider
+    {
+        ExportByteWord ExportFromTemplate<T>(string templatePath, T data) where T : IWordExportTemplate;
+
+        Task<ExportByteWord> ExportFromTemplateAsync<T>(string templatePath, T data) where T : IWordExportTemplate;
+    }
+}

+ 15 - 0
src/Hotline.Application/ExportWord/IWordExportService.cs

@@ -0,0 +1,15 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Application.ExportWord
+{
+    public interface IWordExportService
+    {
+        Task<ExportByteWord> TemplateCreateWordAsync<T>(string templatePath, T data) where T : IWordExportTemplate;
+
+        ExportByteWord TemplateCreateWord<T>(string templatePath, T data) where T : IWordExportTemplate;
+
+        Task<IEnumerable<ExportByteWord>> TemplateCreateWordAsync<T>(string templatePath, IEnumerable<T> data) where T : IWordExportTemplate;
+
+        IEnumerable<ExportByteWord> TemplateCreateWord<T>(string templatePath, IEnumerable<T> data) where T : IWordExportTemplate;
+    }
+}

+ 69 - 0
src/Hotline.Application/ExportWord/IWordExportTemplateExtensions.cs

@@ -0,0 +1,69 @@
+using Hotline.Share.Dtos.ExportWord;
+using Hotline.Share.Enums.ExportWord;
+using System.Reflection;
+
+namespace Hotline.Application.ExportWord
+{
+    public static class IWordExportTemplateExtensions
+    {
+        public static IEnumerable<PlaceholderEntity> GetReplacements<T>(this T wordData) where T : IWordExportTemplate
+        {
+            List<PlaceholderEntity> list = new List<PlaceholderEntity>();
+            PropertyInfo[] properties = typeof(T).GetProperties();
+            foreach (PropertyInfo propertyInfo in properties)
+            {
+                PlaceholderEntity placeholderEntity = new PlaceholderEntity();
+                string placeholder = (propertyInfo.IsDefined(typeof(PlaceholderAttribute)) ? propertyInfo.GetCustomAttribute<PlaceholderAttribute>().Placeholder.ToString() : ("{" + propertyInfo.Name + "}"));
+                if (propertyInfo.PropertyType == typeof(string))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Text;
+                    placeholderEntity.Data = new ExportText
+                    {
+                        Data = propertyInfo.GetValue(wordData)?.ToString()
+                    };
+                    list.Add(placeholderEntity);
+                }
+                else if (propertyInfo.PropertyType == typeof(ExportTable))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Table;
+                    placeholderEntity.Data = (IWordElement)propertyInfo.GetValue(wordData);
+                    list.Add(placeholderEntity);
+                }
+                else if (propertyInfo.PropertyType == typeof(ExportPicture))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Picture;
+                    ExportPicture item = (ExportPicture)propertyInfo.GetValue(wordData);
+                    placeholderEntity.Pictures = new List<ExportPicture> { item };
+                    list.Add(placeholderEntity);
+                }
+                else if (typeof(IEnumerable<ExportPicture>).IsAssignableFrom(propertyInfo.PropertyType))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Picture;
+                    IEnumerable<ExportPicture> pictures = (IEnumerable<ExportPicture>)propertyInfo.GetValue(wordData);
+                    placeholderEntity.Pictures = pictures;
+                    list.Add(placeholderEntity);
+                }
+                else if (propertyInfo.PropertyType == typeof(ExportParagraph))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Paragraph;
+                    placeholderEntity.Data = (IWordElement)propertyInfo.GetValue(wordData);
+                    list.Add(placeholderEntity);
+                }
+                else if (propertyInfo.PropertyType == typeof(ExportComplex))
+                {
+                    placeholderEntity.Placeholder = placeholder;
+                    placeholderEntity.PlaceholderType = EPlaceholderType.Complex;
+                    placeholderEntity.Data = (IWordElement)propertyInfo.GetValue(wordData);
+                    list.Add(placeholderEntity);
+                }
+            }
+
+            return list;
+        }
+    }
+}

+ 32 - 0
src/Hotline.Application/ExportWord/IWordHelperService.cs

@@ -0,0 +1,32 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Application.ExportWord
+{
+    public interface IWordHelperService
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="templatePath"></param>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        byte[] WordByte<T>(string templatePath, T data) where T : IWordExportTemplate;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="templatePath"></param>
+        /// <param name="data"></param>
+        /// <returns></returns>
+        Stream WordStream<T>(string templatePath, T data) where T : IWordExportTemplate;
+
+        /// <summary>
+        /// ZipStream 压缩
+        /// </summary>
+        /// <param name="streams">Dictionary(string, Stream) 文件名和Stream</param>
+        /// <returns></returns>
+        byte[] ConvertZipStream(Dictionary<string, Stream> streams);
+    }
+}

+ 297 - 0
src/Hotline.Application/ExportWord/WordExportProvider.cs

@@ -0,0 +1,297 @@
+using Hotline.Share.Dtos.ExportWord;
+using Hotline.Share.Enums.ExportWord;
+using Novacode;
+using System.Drawing;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Application.ExportWord
+{
+    public class WordExportProvider : IWordExportProvider
+    {
+        public ExportByteWord ExportFromTemplate<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            DocX docX = GetDocX(templatePath);
+            ReplacePlaceholders(docX, data);
+            using MemoryStream memoryStream = new();
+            docX.SaveAs(memoryStream);
+            return new ExportByteWord
+            {
+                WordBytes = memoryStream.ToArray()
+            };
+        }
+
+        public async Task<ExportByteWord> ExportFromTemplateAsync<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            return await Task.Run(() => ExportFromTemplate(templatePath, data));
+        }
+
+        private void ReplacePlaceholders<T>(DocX word, T wordData) where T : IWordExportTemplate
+        {
+            if (word == null)
+            {
+                throw new ArgumentNullException("word");
+            }
+
+            IEnumerable<PlaceholderEntity> replacements = wordData.GetReplacements();
+            if (replacements.Count() == 0)
+            {
+                throw UserFriendlyException.SameMessage("实体中没有可替换的属性");
+            }
+
+            // ReplacePlaceholdersInWord(word, replacements);
+            if (word == null)
+                throw new ArgumentNullException("word");
+
+            if (replacements == null)
+                throw new ArgumentNullException("placeholderEntities");
+
+            foreach (PlaceholderEntity placeholderEntity in replacements)
+            {
+                switch (placeholderEntity.PlaceholderType)
+                {
+                    case EPlaceholderType.Table:
+                        ReplacePlaceholdersInTable(word, placeholderEntity.Placeholder, (ExportTable)placeholderEntity.Data);
+                        break;
+                    case EPlaceholderType.Text:
+                        ReplacePlaceholdersInText(word, placeholderEntity.Placeholder, (ExportText)placeholderEntity.Data);
+                        break;
+                    case EPlaceholderType.Picture:
+                        ReplacePlaceholdersInImage(word, placeholderEntity.Placeholder, placeholderEntity.Pictures);
+                        break;
+                    case EPlaceholderType.Paragraph:
+                        ReplacePlaceholdersInParagraph(word, placeholderEntity.Placeholder, (ExportParagraph)placeholderEntity.Data);
+                        break;
+                    case EPlaceholderType.Complex:
+                        ReplacePlaceholdersInComplex(word, placeholderEntity.Placeholder, (ExportComplex)placeholderEntity.Data);
+                        break;
+                }
+            }
+        }
+
+        private DocX GetDocX(string fileUrl)
+        {
+            if (!System.IO.File.Exists(fileUrl))
+                throw UserFriendlyException.SameMessage("找不到模板文件");
+
+            try
+            {
+                using FileStream stream = System.IO.File.OpenRead(fileUrl);
+                return DocX.Load(stream);
+            }
+            catch (Exception ex)
+            {
+                throw UserFriendlyException.SameMessage("打开模板文件失败,异常原因:" + ex.Message);
+            }
+        }
+
+        private void ReplacePlaceholdersInComplex(DocX word, string oldText, ExportComplex newComplex)
+        {
+            foreach (Paragraph paragraph2 in word.Paragraphs)
+            {
+                if (!paragraph2.Text.Contains(oldText))
+                    continue;
+
+                if (newComplex != null)
+                {
+                    newComplex.Elements.Reverse();
+                    foreach (IWordElement element in newComplex.Elements)
+                    {
+                        if (element.GetType() == typeof(ExportParagraph))
+                        {
+                            ExportParagraph fWParagraph = (ExportParagraph)element;
+                            Paragraph paragraph = word.InsertParagraph();
+                            paragraph.Append(fWParagraph.Run.Text);
+                            if (fWParagraph.Run.IsBold)
+                            {
+                                paragraph.Bold();
+                            }
+
+                            paragraph.FontSize(fWParagraph.Run.FontSize);
+                            paragraph.Font(fWParagraph.Run.FontFamily);
+                            paragraph.Color(fWParagraph.Run.Color);
+                            paragraph.Alignment = fWParagraph.Alignment;
+                            paragraph2.InsertParagraphAfterSelf(paragraph);
+                            paragraph.Remove(trackChanges: false);
+                        }
+                        else if (element.GetType() == typeof(ExportTable))
+                        {
+                            ExportTable fWTable = (ExportTable)element;
+                            Table t = FWTableToTable(word, fWTable);
+                            paragraph2.InsertTableAfterSelf(t);
+                        }
+                        else if (element.GetType() == typeof(ExportPicture))
+                        {
+                            try
+                            {
+                                ExportPicture fWPicture = (ExportPicture)element;
+                                Stream stream = ((fWPicture.PictureData != null) ? fWPicture.PictureData : System.IO.File.OpenRead(fWPicture.PictureUrl));
+                                Novacode.Image image = word.AddImage(stream);
+                                paragraph2.AppendPicture(image.CreatePicture(fWPicture.Height, fWPicture.Width));
+                            }
+                            catch
+                            {
+                            }
+                        }
+                    }
+                }
+
+                paragraph2.ReplaceText(oldText, "");
+            }
+        }
+
+        private void ReplacePlaceholdersInParagraph(DocX word, string oldText, ExportParagraph newParagraph)
+        {
+            foreach (Paragraph paragraph2 in word.Paragraphs)
+            {
+                if (paragraph2.Text.Contains(oldText))
+                {
+                    Paragraph paragraph = word.InsertParagraph();
+                    paragraph.Append(newParagraph.Run.Text);
+                    if (newParagraph.Run.IsBold)
+                        paragraph.Bold();
+
+                    paragraph.FontSize(newParagraph.Run.FontSize);
+                    paragraph.Font(newParagraph.Run.FontFamily);
+                    paragraph.Color(newParagraph.Run.Color);
+                    paragraph.Alignment = newParagraph.Alignment;
+                    paragraph2.InsertParagraphAfterSelf(paragraph);
+                    paragraph2.Remove(trackChanges: false);
+                }
+            }
+        }
+
+        private void ReplacePlaceholdersInText(DocX word, string oldText, ExportText newText)
+        {
+            foreach (Paragraph paragraph in word.Paragraphs)
+            {
+                if (paragraph.Text.Contains(oldText))
+                    paragraph.ReplaceText(oldText, newText.Data);
+            }
+        }
+
+        private void ReplacePlaceholdersInTable(DocX word, string oldText, ExportTable newTable)
+        {
+            foreach (Paragraph paragraph in word.Paragraphs)
+            {
+                if (paragraph.Text.Contains(oldText))
+                {
+                    if (newTable != null)
+                    {
+                        Table t = FWTableToTable(word, newTable);
+                        paragraph.InsertTableAfterSelf(t);
+                        paragraph.Remove(trackChanges: false);
+                    }
+                    else
+                        paragraph.ReplaceText(oldText, "");
+                }
+            }
+        }
+
+        private void ReplacePlaceholdersInImage(DocX word, string oldText, IEnumerable<ExportPicture> newPic)
+        {
+            IEnumerable<ExportPicture> enumerable;
+            if (newPic != null)
+                enumerable = newPic;
+            else
+            {
+                IEnumerable<ExportPicture> enumerable2 = new List<ExportPicture>();
+                enumerable = enumerable2;
+            }
+
+            newPic = enumerable;
+            foreach (Paragraph paragraph2 in word.Paragraphs)
+            {
+                Paragraph paragraph = paragraph2;
+                if (!paragraph.Text.Contains(oldText))
+                    continue;
+
+                if (newPic.Count() > 0)
+                {
+                    newPic.ToList().ForEach(delegate (ExportPicture pic)
+                    {
+                        try
+                        {
+                            paragraph.AppendPicture(word.AddImage(pic.PictureData ?? System.IO.File.OpenRead(pic.PictureUrl)).CreatePicture(pic.Height, pic.Width));
+                        }
+                        catch
+                        {
+                        }
+                    });
+                }
+
+                paragraph.ReplaceText(oldText, "");
+            }
+        }
+
+        private Table FWTableToTable(DocX word, ExportTable FWTable)
+        {
+            Table table = word.AddTable(FWTable.RowCount, FWTable.ColumnCount);
+            table.Alignment = Alignment.center;
+            table.SetBorder(TableBorderType.InsideH, new Border());
+            table.SetBorder(TableBorderType.Top, new Border());
+            table.SetBorder(TableBorderType.Bottom, new Border());
+            table.SetBorder(TableBorderType.Left, new Border());
+            table.SetBorder(TableBorderType.Right, new Border());
+            table.SetBorder(TableBorderType.InsideV, new Border());
+            for (int i = 0; i < FWTable.Rows.Count; i++)
+            {
+                if (FWTable.Rows[i].Height > 0.0)
+                    table.Rows[i].Height = FWTable.Rows[i].Height;
+
+                for (int j = 0; j < FWTable.Rows[i].Cells.Count; j++)
+                {
+                    ExportTableCell fWTableCell = FWTable.Rows[i].Cells[j];
+                    if (table.Rows[i].Cells[j].Width > 0.0)
+                        table.Rows[i].Cells[j].Width = fWTableCell.Width;
+
+                    if (fWTableCell.FillColor != Color.Empty)
+                        table.Rows[i].Cells[j].FillColor = fWTableCell.FillColor;
+
+                    table.Rows[i].Cells[j].VerticalAlignment = fWTableCell.VerticalAlignment;
+                    table.Rows[i].Cells[j].RemoveParagraphAt(0);
+                    foreach (ExportParagraph paragraph in fWTableCell.Paragraphs)
+                    {
+                        Paragraph p = table.Rows[i].Cells[j].InsertParagraph();
+                        if (!string.IsNullOrEmpty(paragraph.Run.Text))
+                            p.Append(paragraph.Run.Text);
+
+                        if (paragraph.Run.IsBold)
+                            p.Bold();
+
+                        p.FontSize(paragraph.Run.FontSize);
+                        p.Font(paragraph.Run.FontFamily);
+                        p.Color(paragraph.Run.Color);
+                        p.Alignment = paragraph.Alignment;
+                        if (paragraph.Run.Pictures.Count > 0)
+                        {
+                            paragraph.Run.Pictures.ForEach(delegate (ExportPicture t)
+                            {
+                                p.AppendPicture(word.AddImage(t.PictureData ?? System.IO.File.OpenRead(t.PictureUrl)).CreatePicture(t.Height, t.Width));
+                            });
+                        }
+                    }
+                }
+            }
+
+            foreach (var mergeCellsInColumn in FWTable.MergeCellsInColumns)
+            {
+                table.MergeCellsInColumn(mergeCellsInColumn.Item1, mergeCellsInColumn.Item2, mergeCellsInColumn.Item3);
+            }
+
+            foreach (var mergeCellsInRow in FWTable.MergeCellsInRows)
+            {
+                table.Rows[mergeCellsInRow.Item1].MergeCells(mergeCellsInRow.Item2, mergeCellsInRow.Item3);
+                if (!(table.Rows[mergeCellsInRow.Item1].Cells[mergeCellsInRow.Item2].Paragraphs[0].Text == "备注"))
+                {
+                }
+
+                while (table.Rows[mergeCellsInRow.Item1].Cells[mergeCellsInRow.Item2].Paragraphs.Count > 1)
+                {
+                    table.Rows[mergeCellsInRow.Item1].Cells[mergeCellsInRow.Item2].RemoveParagraphAt(1);
+                }
+            }
+
+            return table;
+        }
+    }
+}

+ 48 - 0
src/Hotline.Application/ExportWord/WordExportService.cs

@@ -0,0 +1,48 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Application.ExportWord
+{
+    public class WordExportService : IWordExportService
+    {
+        private readonly IWordExportProvider exportProvider;
+
+        public WordExportService(IWordExportProvider _exportProvider)
+        {
+            exportProvider = _exportProvider;
+        }
+
+        public ExportByteWord TemplateCreateWord<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            return exportProvider.ExportFromTemplate(templatePath, data);
+        }
+
+        public IEnumerable<ExportByteWord> TemplateCreateWord<T>(string templatePath, IEnumerable<T> data) where T : IWordExportTemplate
+        {
+            List<ExportByteWord> list = new();
+            foreach (T datum in data)
+            {
+                list.Add(exportProvider.ExportFromTemplate(templatePath, datum));
+            }
+
+            return list;
+        }
+
+        public async Task<ExportByteWord> TemplateCreateWordAsync<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            return await exportProvider.ExportFromTemplateAsync(templatePath, data);
+        }
+
+        public async Task<IEnumerable<ExportByteWord>> TemplateCreateWordAsync<T>(string templatePath, IEnumerable<T> data) where T : IWordExportTemplate
+        {
+            List<ExportByteWord> words = new();
+            foreach (T obj in data)
+            {
+                T item = obj;
+                List<ExportByteWord> fwWordList = words;
+                fwWordList.Add(await exportProvider.ExportFromTemplateAsync(templatePath, item));
+            }
+
+            return words;
+        }
+    }
+}

+ 91 - 0
src/Hotline.Application/ExportWord/WordHelper.cs

@@ -0,0 +1,91 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Application.ExportWord
+{
+    public static class WordHelper
+    {
+        public static byte[] WordByte<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            return new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;
+        }
+
+        public static Stream WordStream<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            var bytes = new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;
+            Stream stream = new MemoryStream(bytes);
+            return stream;
+        }
+
+        public static bool WordDocX<T>(string templatePath, T data, string savePath) where T : IWordExportTemplate
+        {
+            System.IO.File.WriteAllBytes(ExistDir(savePath), WordByte(templatePath, data));
+            return true;
+        }
+
+        private static string ExistDir(string path)
+        {
+            string text = path.Replace("\\", "/");
+            if (text.IndexOf("/") > 0)
+            {
+                string path2 = text.Substring(0, text.LastIndexOf("/"));
+                if (!Directory.Exists(path2))
+                {
+                    Directory.CreateDirectory(path2);
+                }
+            }
+
+            return text;
+        }
+
+        /// <summary>
+        /// ZipStream 压缩
+        /// </summary>
+        /// <param name="streams">Dictionary(string, Stream) 文件名和Stream</param>
+        /// <returns></returns>
+        public static byte[] ConvertZipStream(Dictionary<string, Stream> streams)
+        {
+            byte[] buffer = new byte[6500];
+            MemoryStream returnStream = new();
+            var zipMs = new MemoryStream();
+            using (ICSharpCode.SharpZipLib.Zip.ZipOutputStream zipStream = new ICSharpCode.SharpZipLib.Zip.ZipOutputStream(zipMs))
+            {
+                zipStream.SetLevel(9);//设置 压缩等级 (9级 500KB 压缩成了96KB)
+                foreach (var kv in streams)
+                {
+                    string fileName = kv.Key;
+                    using (var streamInput = kv.Value)
+                    {
+                        ICSharpCode.SharpZipLib.Zip.ZipEntry zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(fileName);
+                        zipEntry.IsUnicodeText = true;
+                        zipStream.PutNextEntry(zipEntry);
+
+                        while (true)
+                        {
+                            var readCount = streamInput.Read(buffer, 0, buffer.Length);
+                            if (readCount > 0)
+                            {
+                                zipStream.Write(buffer, 0, readCount);
+                            }
+                            else
+                            {
+                                break;
+                            }
+                        }
+                        zipStream.Flush();
+                    }
+                }
+                zipStream.Finish();
+                zipMs.Position = 0;
+                zipMs.CopyTo(returnStream, 5600);
+            }
+            returnStream.Position = 0;
+
+            //Stream转Byte[]
+            byte[] returnBytes = new byte[returnStream.Length];
+            returnStream.Read(returnBytes, 0, returnBytes.Length);
+            returnStream.Seek(0, SeekOrigin.Begin);
+
+            return returnBytes;
+        }
+    }
+}

+ 71 - 0
src/Hotline.Application/ExportWord/WordHelperService.cs

@@ -0,0 +1,71 @@
+using Hotline.Share.Dtos.ExportWord;
+using XF.Domain.Dependency;
+
+namespace Hotline.Application.ExportWord
+{
+    public class WordHelperService : IWordHelperService, IScopeDependency
+    {
+        public byte[] WordByte<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            return new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;
+        }
+
+        public Stream WordStream<T>(string templatePath, T data) where T : IWordExportTemplate
+        {
+            var bytes = new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;
+            Stream stream = new MemoryStream(bytes);
+            return stream;
+        }
+
+        /// <summary>
+        /// ZipStream 压缩
+        /// </summary>
+        /// <param name="streams">Dictionary(string, Stream) 文件名和Stream</param>
+        /// <returns></returns>
+        public byte[] ConvertZipStream(Dictionary<string, Stream> streams)
+        {
+            byte[] buffer = new byte[6500];
+            MemoryStream returnStream = new();
+            var zipMs = new MemoryStream();
+            using (ICSharpCode.SharpZipLib.Zip.ZipOutputStream zipStream = new ICSharpCode.SharpZipLib.Zip.ZipOutputStream(zipMs))
+            {
+                zipStream.SetLevel(9);//设置 压缩等级 (9级 500KB 压缩成了96KB)
+                foreach (var kv in streams)
+                {
+                    string fileName = kv.Key;
+                    using (var streamInput = kv.Value)
+                    {
+                        ICSharpCode.SharpZipLib.Zip.ZipEntry zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(fileName);
+                        zipEntry.IsUnicodeText = true;
+                        zipStream.PutNextEntry(zipEntry);
+
+                        while (true)
+                        {
+                            var readCount = streamInput.Read(buffer, 0, buffer.Length);
+                            if (readCount > 0)
+                            {
+                                zipStream.Write(buffer, 0, readCount);
+                            }
+                            else
+                            {
+                                break;
+                            }
+                        }
+                        zipStream.Flush();
+                    }
+                }
+                zipStream.Finish();
+                zipMs.Position = 0;
+                zipMs.CopyTo(returnStream, 5600);
+            }
+            returnStream.Position = 0;
+
+            //Stream转Byte[]
+            byte[] returnBytes = new byte[returnStream.Length];
+            returnStream.Read(returnBytes, 0, returnBytes.Length);
+            returnStream.Seek(0, SeekOrigin.Begin);
+
+            return returnBytes;
+        }
+    }
+}

+ 6 - 7
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -207,8 +207,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         CancellationToken cancellationToken = default)
     {
         var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withDefine: true, withSteps: true,
-            withCountersigns: true,
-            cancellationToken: cancellationToken);
+            withTraces: true, withCountersigns: true, cancellationToken: cancellationToken);
         //await NextAsync(workflow, dto, dto.ExpiredTime, dto.IsStartCountersign, cancellationToken);
 
         ////未超期工单,节点超期时间不能小于当前时间,不能大于流程整体超期时间
@@ -276,8 +275,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// </summary>
     public async Task PreviousAsync(PreviousWorkflowDto dto, CancellationToken cancellationToken)
     {
-        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withSteps: true,
-            withCountersigns: true, cancellationToken: cancellationToken);
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withSteps: true, 
+            withTraces: true, withCountersigns: true, cancellationToken: cancellationToken);
         var user = await _userRepository.Queryable()
             .Includes(x => x.Organization)
             .FirstAsync(x => x.Id == _sessionContext.RequiredUserId, cancellationToken);
@@ -289,8 +288,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// </summary>
     public async Task PreviousAsync(PreviousWorkflowDto dto, string userId, CancellationToken cancellationToken)
     {
-        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withSteps: true,
-            withCountersigns: true, cancellationToken: cancellationToken);
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withSteps: true, 
+            withTraces: true, withCountersigns: true, cancellationToken: cancellationToken);
         var user = await _userRepository.Queryable()
             .Includes(x => x.Organization)
             .FirstAsync(x => x.Id == userId, cancellationToken);
@@ -308,7 +307,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             throw new UserFriendlyException(string.Join(',', validationResult.Errors));
 
         var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, withDefine: true, withSteps: true,
-            cancellationToken: cancellationToken);
+           withTraces: true, cancellationToken: cancellationToken);
 
         //await _orderDomainService.ReadyToRecallAsync(workflow.ExternalId, cancellationToken);
 

+ 3 - 1
src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs

@@ -157,7 +157,7 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
                     order.ProcessType is EProcessType.Zhiban)
                     : 0;
                 order.File(now, handleDuration, fileDuration, allDuration);
-                order.FileUserName = notification.Trace.HandlerId;
+                order.FileUserId = notification.Trace.HandlerId;
                 order.FileUserName = notification.Trace.HandlerName;
                 order.FileUserOrgId = notification.Trace.HandlerOrgId;
                 order.FileUserOrgName = notification.Trace.HandlerOrgName;
@@ -166,6 +166,8 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
                 {
                     order.FileUserRole = EFileUserType.Org;
                 }
+                //TODO
+
                
                 await _orderRepository.UpdateAsync(order, cancellationToken);
                 var callRecord = await _trCallRecordRepository.GetAsync(p => p.CallAccept == order.CallId, cancellationToken);

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

@@ -7,6 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="NPOI" Version="2.7.0" />
     <PackageReference Include="XC.RSAUtil" Version="1.3.6" />
   </ItemGroup>
 

+ 9 - 2
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -1,11 +1,14 @@
 using Hotline.CallCenter.BlackLists;
 using Hotline.JudicialManagement;
+using Hotline.Orders;
 using Hotline.Push.FWMessage;
 using Hotline.Settings;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.JudicialManagement;
+using Hotline.Share.Dtos.OrderExportWord;
 using Hotline.Share.Dtos.Org;
 using Hotline.Share.Dtos.Push.FWMessage;
+using Hotline.Share.Enums.Order;
 using Mapster;
 using XF.Domain.Entities;
 
@@ -74,8 +77,12 @@ namespace Hotline.Application.Mappers
                 .Map(d => d.EventTypeSpliceName, x => x.EventTypeSpliceName)
                  ;
 
-
-
+            config.ForType<Order, OrderSubmissionForm>()
+             .Map(d => d.FromGender, x => x.FromGender == EGender.Male ? "男" : x.FromGender == EGender.Female ? "女" : "未知")
+             .Map(d => d.CreationTime, x => x.CreationTime.ToString("yyyy-MM-dd HH:mm:ss"))
+             .Map(d => d.CenterToOrgTime, x => x.CenterToOrgTime == null ? "" : x.CenterToOrgTime.Value.ToString("yyyy-MM-dd HH:mm:ss"))
+             .Map(d => d.ExpiredTime, x => x.ExpiredTime == null ? "" : x.ExpiredTime.Value.ToString("yyyy-MM-dd HH:mm:ss"))
+             ;
 
         }
     }

+ 66 - 0
src/Hotline.Share/Dtos/ExportWord/ExportByteWord.cs

@@ -0,0 +1,66 @@
+using Hotline.Share.Enums.ExportWord;
+using System.Drawing;
+
+namespace Hotline.Share.Dtos.ExportWord
+{
+    public class ExportByteWord
+    {
+        public byte[] WordBytes { get; set; }
+    }
+
+    public class ExportRun
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Text { get; set; } = "";
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public Color Color { get; set; } = Color.Black;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public int FontSize { get; set; } = 12;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public string FontFamily { get; set; } = "等线 (中文正文)";
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public bool IsBold { get; set; } = false;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<ExportPicture> Pictures { get; set; } = new List<ExportPicture>();
+    }
+
+    public class PlaceholderEntity
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Placeholder { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public EPlaceholderType PlaceholderType { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public IWordElement Data { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public IEnumerable<ExportPicture> Pictures { get; set; }
+    }
+}

+ 51 - 0
src/Hotline.Share/Dtos/ExportWord/ExportPicture.cs

@@ -0,0 +1,51 @@
+using Novacode;
+
+namespace Hotline.Share.Dtos.ExportWord
+{
+    public class ExportPicture : IWordElement
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public Stream PictureData { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public string PictureUrl { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public int Width { get; set; } = 300;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public int Height { get; set; } = 200;
+
+    }
+
+    public class ExportParagraph : IWordElement
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public ExportRun Run { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public Alignment Alignment { get; set; } = Alignment.center;
+
+    }
+
+    public class ExportComplex : IWordElement
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<IWordElement> Elements { get; set; } = new List<IWordElement>();
+
+    }
+}

+ 118 - 0
src/Hotline.Share/Dtos/ExportWord/ExportTable.cs

@@ -0,0 +1,118 @@
+using Novacode;
+using System.Drawing;
+
+namespace Hotline.Share.Dtos.ExportWord
+{
+    public class ExportTable : IWordElement
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<ExportTableRow> Rows { get; set; } = new List<ExportTableRow>();
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public int RowCount => Rows.Count;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public int ColumnCount { get; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<(int, int, int)> MergeCellsInColumns { get; } = new List<(int, int, int)>();
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<(int, int, int)> MergeCellsInRows { get; } = new List<(int, int, int)>();
+
+
+        public ExportTable()
+        {
+        }
+
+        public ExportTable(int rowCount, int columnCount)
+        {
+            ColumnCount = columnCount;
+            for (int i = 0; i < rowCount; i++)
+            {
+                ExportTableRow fWTableRow = new ExportTableRow();
+                for (int j = 0; j < columnCount; j++)
+                {
+                    ExportTableCell item = new ExportTableCell
+                    {
+                        Paragraphs = new List<ExportParagraph>
+                    {
+                        new ExportParagraph
+                        {
+                            Run = new ExportRun()
+                        }
+                    }
+                    };
+                    fWTableRow.Cells.Add(item);
+                }
+
+                Rows.Add(fWTableRow);
+            }
+        }
+
+        public void MergeCellsInColumn(int columnIndex, int startRow, int endRow)
+        {
+            MergeCellsInColumns.Add((columnIndex, startRow, endRow));
+        }
+
+        public void MergeCellsInRow(int rowIndex, int startColumn, int endColumn)
+        {
+            MergeCellsInRows.Add((rowIndex, startColumn, endColumn));
+        }
+    }
+
+    public class ExportTableCell
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<ExportParagraph> Paragraphs { get; set; }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public double Width { get; set; } = 200.0;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public Color FillColor { get; set; } = Color.Empty;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Center;
+
+    }
+
+    public class ExportTableRow
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public List<ExportTableCell> Cells { get; set; } = new List<ExportTableCell>();
+
+        /// <summary>
+        /// 
+        /// </summary>
+        public double Height { get; set; }
+    }
+
+    public class ExportText : IWordElement
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        public string Data { get; set; }
+    }
+}

+ 10 - 0
src/Hotline.Share/Dtos/ExportWord/IWordExportTemplate.cs

@@ -0,0 +1,10 @@
+namespace Hotline.Share.Dtos.ExportWord
+{
+    public interface IWordExportTemplate
+    {
+    }
+
+    public interface IWordElement
+    {
+    }
+}

+ 12 - 0
src/Hotline.Share/Dtos/ExportWord/PlaceholderAttribute.cs

@@ -0,0 +1,12 @@
+namespace Hotline.Share.Dtos.ExportWord
+{
+    public class PlaceholderAttribute : System.Attribute
+    {
+        public string Placeholder { get; set; }
+
+        public PlaceholderAttribute(string placeHolder)
+        {
+            Placeholder = placeHolder;
+        }
+    }
+}

+ 5 - 0
src/Hotline.Share/Dtos/FlowEngine/Workflow/WorkflowTraceDto.cs

@@ -31,6 +31,11 @@ public class WorkflowTraceDto : StepBasicDto
     /// </summary>
     public string? ParentId { get; set; }
 
+    /// <summary>
+    /// 派单组办理次数
+    /// </summary>
+    public int SendHandleTimes { get; set; }
+
     /// <summary>
     /// 会签流转记录
     /// </summary>

+ 63 - 0
src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Hotline.Share.Enums.Order;
 using Hotline.Share.Requests;
 
 namespace Hotline.Share.Dtos.Order
@@ -24,4 +25,66 @@ namespace Hotline.Share.Dtos.Order
     }
 
     public record QueryOrderWaitedDto(bool? IsProvince, bool? IsHandled,bool? IsCounterSign,bool? ExpiredOrAlmostOverdue) : PagedKeywordRequest;
+
+    /// <summary>
+    /// 中心待办
+    /// </summary>
+    public record QueryOrderWaitedCenterDto : PagedKeywordRequest
+	{
+		/// <summary>
+		/// 编号
+		/// </summary>
+		public string? No { get; set; }
+
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string? Title { get; set; }
+        
+        /// <summary>
+        /// 开始生成时间
+        /// </summary>
+        public DateTime? StCreationTime { get; set; }
+
+        /// <summary>
+        /// 结束生成时间
+        /// </summary>
+        public DateTime? EndCreationTime { get; set; }
+
+        /// <summary>
+        /// 开始受理时间
+        /// </summary>
+        public DateTime? StartTimeEnd { get; set; }
+
+        /// <summary>
+        /// 结束受理时间
+        /// </summary>
+        public DateTime? StartTimeSt { get; set; }
+        
+        /// <summary>
+        /// 当前节点名称
+        /// </summary>
+        public string? StepName { get; set; }
+
+        /// <summary>
+        /// 接办部门
+        /// </summary>
+        public string? ActualHandleOrgName { get; set; }
+
+        /// <summary>
+        /// 状态
+        /// </summary>
+        public EOrderStatus? Status { get; set; }
+
+        /// <summary>
+        /// 受理人
+        /// </summary>
+        public string? AcceptorName { get; set; }
+
+        /// <summary>
+        /// 过期状态
+        /// </summary>
+        public EExpiredStatus? ExpiredStatus { get; set; }
+
+    }
 }

+ 147 - 0
src/Hotline.Share/Dtos/OrderExportWord/OrderSubmissionForm.cs

@@ -0,0 +1,147 @@
+using Hotline.Share.Dtos.ExportWord;
+
+namespace Hotline.Share.Dtos.OrderExportWord
+{
+    public class OrderSubmissionForm : IWordExportTemplate
+    {
+        /// <summary>
+        /// 编号
+        /// </summary>
+        public string No { get; set; }
+
+        /// <summary>
+        /// 来信时间
+        /// </summary>
+        public string? CreationTime { get; set; }
+
+        /// <summary>
+        /// 来电/信人姓名
+        /// </summary>
+        public string? FromName { get; set; }
+
+        /// <summary>
+        /// 来电/信人性别
+        /// </summary>
+        public string? FromGender { get; set; }
+
+        /// <summary>
+        /// 来电号码
+        /// </summary>
+        public string? FromPhone { get; set; }
+
+        /// <summary>
+        /// 联系电话
+        /// </summary>
+        public string? Contact { get; set; }
+
+        /// <summary>
+        /// 来电地址
+        /// </summary>
+        public string? FullAddress { get; set; }
+
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string Title { get; set; }
+
+        /// <summary>
+        /// 内容
+        /// </summary>
+        public string Content { get; set; }
+
+        /// <summary>
+        /// 受理类型
+        /// </summary>
+        public string? AcceptType { get; set; }
+
+        /// <summary>
+        /// 热点
+        /// </summary>
+        public string? HotspotSpliceName { get; set; }
+
+        /// <summary>
+        /// 实际办理部门名称
+        /// </summary>
+        public string? ActualHandleOrgName { get; set; }
+
+        /// <summary>
+        /// 办理时间限制(如:24小时、7个工作日)
+        /// </summary>
+        public string? TimeLimit { get; set; }
+
+        /// <summary>
+        /// 交办意见
+        /// </summary>
+        public string? CenterToOrgOpinion { get; set; }
+
+        /// <summary>
+        /// 领导批示
+        /// </summary>
+        public string? LeaderInstructions { get; set; }
+
+        /// <summary>
+        /// 承办部门核实
+        /// </summary>
+        public string? OrgVerify { get; set; }
+
+        /// <summary>
+        /// 实际办理意见
+        /// </summary>
+        public string? ActualOpinion { get; set; }
+
+        /// <summary>
+        /// 回访部门信息
+        /// </summary>
+        public string? VisitOrg { get; set; }
+
+        /// <summary>
+        /// 回访内容
+        /// </summary>
+        public string? VisitContent { get; set; }
+
+        /// <summary>
+        /// 办理单位负责人
+        /// </summary>
+        public string? ResponsiblePerson { get; set; }
+
+        /// <summary>
+        /// 办理单位负责人电话
+        /// </summary>
+        public string? ResponsiblePersonPhone { get; set; }
+
+        /// <summary>
+        /// 上报时间
+        /// </summary>
+        public string? ReportingTime { get; set; }
+
+        /// <summary>
+        /// 承办人
+        /// </summary>
+        public string? Undertaker { get; set; }
+
+        /// <summary>
+        /// 承办人电话
+        /// </summary>
+        public string? UndertakerPhone { get; set; }
+
+        /// <summary>
+        /// 来电人意见(是否满意)
+        /// </summary>
+        public string? FromView { get; set; }
+
+        /// <summary>
+        /// 交办时间(中心交部门办理时间)
+        /// </summary>
+        public string? CenterToOrgTime { get; set; }
+
+        /// <summary>
+        /// 超期时间(期满时间)
+        /// </summary>
+        public string? ExpiredTime { get; set; }
+
+        /// <summary>
+        ///  交办人
+        /// </summary>
+        public string? CenterToOrgHandlerName { get; set; }
+    }
+}

+ 2 - 5
src/Hotline.Share/Dtos/Position.cs

@@ -54,10 +54,7 @@ public class Position
 
     public void InitAddress()
     {
-        if (string.IsNullOrEmpty(Address))
-        {
-            Address = $"{Province}{City}{County}{Town}";
-            FullAddress = $"{Address}{Street}";
-        }
+        Address = $"{Province}{City}{County}{Town}";
+        FullAddress = $"{Address}{Street}";
     }
 }

+ 11 - 0
src/Hotline.Share/Enums/ExportWord/EPlaceholderType.cs

@@ -0,0 +1,11 @@
+namespace Hotline.Share.Enums.ExportWord
+{
+    public enum EPlaceholderType
+    {
+        Table,
+        Text,
+        Picture,
+        Paragraph,
+        Complex
+    }
+}

+ 1 - 0
src/Hotline.Share/Hotline.Share.csproj

@@ -11,6 +11,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="DocXCore" Version="1.0.10" />
     <PackageReference Include="MediatR.Contracts" Version="1.0.1" />
     <PackageReference Include="XF.Utility.EnumExtensions" Version="1.0.4" />
   </ItemGroup>

+ 12 - 1
src/Hotline.Share/Requests/PagedKeywordRequest.cs

@@ -51,7 +51,7 @@ public record OrgDataListDetailRequest:PagedRequest
 	public string? No { get; set; }
 
 	/// <summary>
-	/// 查询状态 1:业务已办超期  2:会签已办超期  3:业务待办超期  4:会签待办超期
+	/// 查询状态 1:业务已办超期  2:会签已办超期  3:业务待办超期  4:会签待办超期  5:小计
 	/// </summary>
 	public int QueryType { get; set; }
 
@@ -136,6 +136,17 @@ public record QueryOrderDelayDataListRequest:ReportPagedRequest
 	public string? OrgName { get; set; }
 }
 
+public record QueryOrderDelayDataDetailRequest : ReportPagedRequest
+{
+	public string? OrgCode { get; set; }
+
+	/// <summary>
+	/// 查询类型  1已同意次数 2 未同意次数 3  审批中次数 4 小计
+	/// </summary>
+	public int? Type { get; set; }
+
+}
+
 public record QueryOrderReTransactRequest : ReportPagedRequest
 {
 	public string? OrgName { get; set; }

+ 10 - 0
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -1786,8 +1786,18 @@ namespace Hotline.FlowEngine.Workflows
             EWorkflowTraceType traceType = EWorkflowTraceType.Normal,
             CancellationToken cancellationToken = default)
         {
+            var sendHandleTimes = 0;
+            if (step.BusinessType == EBusinessType.Send)
+            {
+                var sendHandleCount = workflow.Traces.Count(d => d.StepType == EStepType.Normal &&
+                                                                 d.BusinessType == EBusinessType.Send);
+                sendHandleTimes = sendHandleCount + 1;
+            }
+
+
             var trace = _mapper.Map<WorkflowTrace>(step);
             trace.TraceType = traceType;
+            trace.SendHandleTimes = sendHandleTimes;
 
             if (step.IsInCountersign())
             {

+ 6 - 0
src/Hotline/FlowEngine/Workflows/WorkflowTrace.cs

@@ -24,6 +24,12 @@ public class WorkflowTrace : StepBasicEntity
     /// </summary>
     public EWorkflowTraceType? TraceType { get; set; }
 
+    /// <summary>
+    /// 派单组办理次数
+    /// </summary>
+    [SugarColumn(DefaultValue = "0")]
+    public int SendHandleTimes { get; set; }
+
     /// <summary>
     /// 会签流转记录
     /// </summary>

+ 14 - 1
src/Hotline/Orders/Order.cs

@@ -314,6 +314,17 @@ namespace Hotline.Orders
         /// </summary>
         public DateTime? CenterToOrgTime { get; set; }
 
+        /// <summary>
+        /// 交办意见
+        /// </summary>
+        public string? CenterToOrgOpinion{ get; set; }
+
+        /// <summary>
+        /// 交班人
+        /// </summary>
+        public string? CenterToOrgHandlerId { get; set; }
+        public string? CenterToOrgHandlerName { get; set; }
+
         /// <summary>
         /// 归档时间(暂为流程结束时间,因流程结束自动归档)
         /// </summary>
@@ -877,7 +888,9 @@ namespace Hotline.Orders
             Status = EOrderStatus.Visited;
         }
 
-        public void CenterToOrg(string timelimit, int timelimitCount, ETimeType timilimitUnit, DateTime expiredTime, DateTime nearlyExpiredTime)
+        public void CenterToOrg(string timelimit, int timelimitCount, ETimeType timilimitUnit, 
+            DateTime expiredTime, DateTime nearlyExpiredTime,
+            string opinion, string handlerId, string handlerName)
         {
             ProcessType = EProcessType.Jiaoban;
             TimeLimit = timelimit;