using System.Collections.Concurrent; using DS.Module.Core; using DS.Module.Core.Data; using DS.Module.PrintModule; using DS.WMS.Core.Code.Entity; using DS.WMS.Core.Info.Entity; using DS.WMS.Core.Op.Dtos; using DS.WMS.Core.Op.Entity; using DS.WMS.Core.Sys.Entity; using DS.WMS.Core.TaskInteraction.Dtos; using DS.WMS.Core.TaskInteraction.Entity; using DS.WMS.Core.TaskInteraction.Interface; using DS.WMS.Core.TaskInteraction.Method.ActionExecutor; using DS.WMS.Core.TaskInteraction.Method.DataProvider; using Fasterflect; using HtmlAgilityPack; using Masuit.Tools; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using RazorEngineCore; namespace DS.WMS.Core.TaskInteraction.Method { /// /// 邮件模板发送 /// public class MailGenerator : ServiceBase { static readonly ConcurrentDictionary TemplateCache = new(); readonly IConfiguration config; readonly ApiFox Api = DefaultActionExecutor.Api; /// /// 初始化 /// /// public MailGenerator(IServiceProvider provider) : base(provider) { config = provider.GetRequiredService(); TenantDb.QueryFilter.Clear(); Db.QueryFilter.Clear(); } //渲染模板 internal static async Task RenderTemplateAsync(string templateText, object model, IRazorEngine? razorEngine = null) { if (string.IsNullOrEmpty(templateText)) return templateText; string crc64 = templateText.Crc64(); IRazorEngineCompiledTemplate compiledTemplate = TemplateCache.GetOrAdd(crc64, i => { var engine = razorEngine ?? new RazorEngine(); return engine.Compile(templateText); }); return await compiledTemplate.RunAsync(model); } /// /// 根据业务ID和类型获取模板所需数据 /// ///邮件配置 /// 业务ID /// 业务类型 /// /// public async Task GetBusinessModelAsync(BusinessTaskMail taskMail, long bsId, BusinessType? bsType = null, IActionManagerService? actionManager = null) { ArgumentNullException.ThrowIfNull(taskMail, nameof(taskMail)); MailTemplateModel model = new() { BusinessId = bsId, BusinessType = bsType }; if (taskMail.Provider == null) { actionManager ??= ServiceProvider.GetRequiredService(); model.Primary = await actionManager.GetBusinessDataAsync(bsId, bsType.GetValueOrDefault()); } else { IDataProvider? dataProvider = null; switch (taskMail.Provider.Type) { case DataProviderType.Custom: if (string.IsNullOrEmpty(taskMail.Provider.TypeName)) return model; var type = Type.GetType(taskMail.Provider.TypeName, true); dataProvider = ConstructorExtensions.CreateInstance(type, [ServiceProvider]) as IDataProvider; dataProvider ??= ConstructorExtensions.CreateInstance(type) as IDataProvider; break; case DataProviderType.Fixed: model.Primary = JsonConvert.DeserializeObject(taskMail.Content!); break; case DataProviderType.Database: dataProvider = new DefaultDatabaseProvider(ServiceProvider); break; case DataProviderType.Network: break; } if (dataProvider != null) { DataFetchContext context = new() { ServiceProvider = ServiceProvider, BusinessId = bsId, BusinessType = bsType, Content = taskMail.Content, }; await dataProvider.FetchDataAsync(context); model.Primary = context.Data; } } if (model.Primary == null) return model; List> list = []; long? forwarderId = null, shipperCNId = null, customerId = null; if (model.Primary is SeaExportRes order) { list.Add(new Tuple(order.SaleId, "sale")); list.Add(new Tuple(order.OperatorId, "op")); list.Add(new Tuple(order.CustomerService, "cs")); list.Add(new Tuple(order.Doc, "doc")); forwarderId = order.ForwarderId; shipperCNId = order.ShipperCnId; customerId = order.CustomerId; } else { if (model.Primary is not IDictionary dic) return model; if (dic.TryGetValue(nameof(SeaExport.SaleId), out object? sale)) list.Add(new Tuple((long)sale, nameof(sale))); if (dic.TryGetValue(nameof(SeaExport.OperatorId), out object? op)) list.Add(new Tuple((long)op, nameof(op))); if (dic.TryGetValue(nameof(SeaExport.CustomerService), out object? cs)) list.Add(new Tuple((long)cs, nameof(cs))); if (dic.TryGetValue(nameof(SeaExport.Doc), out object? doc)) list.Add(new Tuple((long)doc, nameof(doc))); if (dic.TryGetValue(nameof(SeaExport.ForwarderId), out object? forwarder) && forwarder != null) forwarderId = (long)forwarder; if (dic.TryGetValue(nameof(SeaExport.ShipperCnId), out object? shipperCN) && shipperCN != null) shipperCNId = (long)shipperCN; if (dic.TryGetValue(nameof(SeaExport.CustomerId), out object? customer) && customer != null) customerId = (long)customer; } long?[] custIds = [forwarderId, shipperCNId, customerId]; var custList = await TenantDb.Queryable().Where(x => custIds.Contains(x.Id)) .Select(x => new { x.Id, x.Description }).ToListAsync(); model.ForwarderName = custList.Find(x => x.Id == forwarderId)?.Description; model.DomesticShipperName = custList.Find(x => x.Id == shipperCNId)?.Description; model.CustomerName = custList.Find(x => x.Id == customerId)?.Description; if (list.Count > 0) { var ids = list.Select(x => x.Item1).ToArray(); var userList = await Db.Queryable().Where(x => ids.Contains(x.Id)).Select(x => new Contact { Id = x.Id, Code = x.UserCode, Email = x.Email, EnName = x.UserEnName, Name = x.UserName, Mobile = x.Phone, Tel = x.OfficePhone, QQ = x.QQ }).ToListAsync(); long saleId = list.Find(x => x.Item2 == "sale")!.Item1; model.Sales = userList.Find(x => x.Id == saleId); long opId = list.Find(x => x.Item2 == "op")!.Item1; model.Operator = userList.Find(x => x.Id == opId); long csId = list.Find(x => x.Item2 == "cs")!.Item1; model.CustomerService = userList.Find(x => x.Id == csId); long docId = list.Find(x => x.Item2 == "doc")!.Item1; model.Document = userList.Find(x => x.Id == docId); } //获取箱型价格 model.CtnPriceList = await TenantDb.Queryable().Where(x => x.BusinessId == model.BusinessId) .Select(x => new BusinessCtnPrice { Ctn = x.Ctn, CtnAll = x.CtnAll, CtnNum = x.CtnNum, QuotePrice = x.QuotePrice, GuidePrice = x.GuidePrice, FloorPrice = x.FloorPrice }).ToListAsync(); return model; } /// /// 根据模板配置发送邮件 /// /// 邮件配置 /// 模板数据 /// public async Task SendAsync(BusinessTaskMail mailConfig, MailTemplateModel templateModel) { ArgumentNullException.ThrowIfNull(mailConfig, nameof(mailConfig)); ArgumentNullException.ThrowIfNull(templateModel, nameof(templateModel)); var order = await TenantDb.Queryable().Where(x => x.Id == templateModel.BusinessId) .Select(x => new { x.CustomerNo, // x.SaleId, x.OperatorId, x.CustomerService, x.Doc, // x.CarrierId, x.ForwarderId, x.YardId, x.TruckerId, x.CustomerId }).FirstAsync(); if (order == null) return DataResult.Failed($"未能获取订单({templateModel.BusinessId})数据"); //设置发件人 if (mailConfig.Sender == null) return DataResult.Failed("未设置发件人"); long senderId = 0; if (mailConfig.Sender.IsSale) senderId = order.SaleId; else if (mailConfig.Sender.IsOperator) senderId = order.OperatorId; else if (mailConfig.Sender.IsCustomerService) senderId = order.CustomerService; else if (mailConfig.Sender.IsVouchingClerk) senderId = order.Doc; templateModel.Sender = await Db.Queryable().Where(x => x.Id == senderId).Select(x => new MailSender { DisplayName = x.UserName, Phone = x.Phone, SignatureHtml = x.SignatureHtml }).FirstAsync(); if (templateModel.Sender == null) return DataResult.Failed("邮件模板未设置发件人"); var senderConfig = await TenantDb.Queryable().FirstAsync(x => x.CreateBy == senderId); if (senderConfig == null) return DataResult.Failed($"发件人用户:{templateModel.Sender.DisplayName} 未设置SMTP发件信息"); templateModel.Sender.MailAddress = senderConfig.MailAccount; List ccList = []; if (mailConfig.CC != null) { //设置抄送人 List ccIds = []; if (mailConfig.CC.IsSale) ccIds.Add(order.SaleId); else if (mailConfig.CC.IsOperator) ccIds.Add(order.OperatorId); else if (mailConfig.CC.IsCustomerService) ccIds.Add(order.CustomerService); else if (mailConfig.CC.IsVouchingClerk) ccIds.Add(order.Doc); if (ccIds.Count > 0) ccList = await Db.Queryable().Where(x => ccIds.Contains(x.Id)).Select(x => x.Email).ToListAsync(); } //设置收件人 if (mailConfig.Receiver == null) return DataResult.Failed("邮件模板未设置收件人"); List receiverTypes = []; if (mailConfig.Receiver.IsCarrier) receiverTypes.Add("carrier"); if (mailConfig.Receiver.IsBooking) receiverTypes.Add("booking"); if (mailConfig.Receiver.IsYard) receiverTypes.Add("yard"); if (mailConfig.Receiver.IsTruck) receiverTypes.Add("truck"); if (mailConfig.Receiver.IsController) receiverTypes.Add("controller"); if (mailConfig.Receiver.IsShipper) receiverTypes.Add("shipper"); if (mailConfig.Receiver.IsShipperCn) receiverTypes.Add("shippercn"); if (mailConfig.Receiver.IsConsignee) receiverTypes.Add("consignee"); templateModel.Receivers = await TenantDb.Queryable().Where(x => receiverTypes.Contains(x.CustomerType) && x.Email != null && x.Email != string.Empty && x.BusinessId == templateModel.BusinessId && x.BusinessType == templateModel.BusinessType) .Select(x => new MailReceiver { DisplayName = x.Name, MailAddress = x.Email }).ToListAsync(); string title, content = string.Empty; var razorEngine = new RazorEngine(); try { title = await RenderTemplateAsync(mailConfig.Title, templateModel, razorEngine); content = await RenderTemplateAsync(mailConfig.Content, templateModel, razorEngine); } catch (Exception ex) { await ex.LogAsync(Db); return DataResult.Failed($"渲染邮件模板({mailConfig.Id})时出错,请检查模板是否有语法错误"); } //插入发件人签名 var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(content); var node = htmlDoc.GetElementbyId("sign"); if (node != null) node.InnerHtml = templateModel.Sender.SignatureHtml; using (StringWriter writer = new()) { htmlDoc.Save(writer); content = writer.ToString(); writer.Close(); } var attachmentList = mailConfig.Attachments == null ? [] : new List>(mailConfig.Attachments.Count); try { //生成模板附件 if (mailConfig.Attachments?.Count > 0) { if (Api.DefaultHeaders.Contains("Authorization")) Api.DefaultHeaders.Remove("Authorization"); Api.DefaultHeaders.Add("Authorization", "Bearer " + User.GetToken()); long tenantId = long.Parse(User.TenantId); string requestUrl = config["TaskMail:FileBaseUrl"] + config["TaskMail:SQLPrint"]; foreach (var item in mailConfig.Attachments) { object? obj = templateModel.GetPropertyValue(nameof(MailTemplateModel.Primary), Flags.InstancePublic); var req = new OpenPrintReq { ParamJsonStr = JsonConvert.SerializeObject(obj), PrintType = ((int)FileFormat.PDF).ToString(), TemplateId = item.TemplateId, TenantId = tenantId }; var reqResult = await Api.PostAsync(requestUrl, req); if (!reqResult.Data.Succeeded) return DataResult.Failed($"未能获取打印API生成的文件,请求地址:{requestUrl}"); string url = config["TaskMail:FileBaseUrl"] + @"/PrintTempFile/" + reqResult.Data.Data; var response = await Api.SendRequestAsync(HttpMethod.Get, url); if (!response.IsSuccessStatusCode) return DataResult.Failed($"未能获取打印API生成的文件,附件地址:{url}"); string? fileName = Path.GetFileName(reqResult.Data.Data); if (!string.IsNullOrEmpty(item.FileName)) //设置了文件名 { if (item.FileName.Contains('@')) { fileName = await RenderTemplateAsync(item.FileName, templateModel, razorEngine); } else { fileName = item.FileName; } //文件名的最后一个字符不是“.”,则拼接扩展名 if (item.FileName.LastIndexOf('.') != item.FileName.Length - 1 && item.FileType.HasValue) fileName = fileName + "." + item.FileType.Value.ToString().ToLowerInvariant(); } var byteArray = await response.Content.ReadAsByteArrayAsync(); string base64Str = Convert.ToBase64String(byteArray); attachmentList.Add(new Tuple(fileName, base64Str)); } } //手工附件 if (templateModel.FileAttachments?.Count > 0) { foreach (var item in templateModel.FileAttachments) { FileInfo file = new(item.FilePath); if (!file.Exists) return DataResult.Failed($"参数设置的附件路径:{file.FullName} 不存在"); using (var stream = file.OpenRead()) { var byteArray = stream.ToArray(); string base64Str = Convert.ToBase64String(byteArray); attachmentList.Add(new Tuple(item.DisplayName ?? file.Name, base64Str)); } } } string sendTo = string.Join(",", templateModel.Receivers.Select(x => x.MailAddress)); dynamic[] mailParams = [new { SendTo = sendTo, Title = title, Body = content, CCTo = string.Join(";", ccList), ShowName = senderConfig.ShowName.IsNullOrEmpty() ? templateModel.Sender.DisplayName : senderConfig.ShowName, Account = senderConfig.MailAccount, senderConfig.Password, Server = senderConfig.SmtpServer, Port = senderConfig.SmtpPort.GetValueOrDefault(), UseSSL = senderConfig.SmtpSSL.GetValueOrDefault(), Attaches = attachmentList?.Select(x => new { AttachName = x.Item1, AttachContent = x.Item2 }).ToList() }]; var mailResult = await Api.SendRequestAsync(HttpMethod.Post, config["TaskMail:MailApiUrl"], mailParams); if (mailResult.IsSuccessStatusCode) { return DataResult.Successed($"已发邮件(委托单:{order.CustomerNo}),收件人:" + string.Join(",", sendTo) + "发件人:" + senderConfig.MailAccount); } return DataResult.Failed($"邮件发送失败,API返回状态为:{(int)mailResult.StatusCode} {mailResult.StatusCode}" + $"(委托单:{order.CustomerNo}),收件人:" + string.Join(",", sendTo) + "发件人:" + senderConfig.MailAccount); } catch (Exception ex) { await ex.LogAsync(Db); return DataResult.FailedWithDesc(nameof(MultiLanguageConst.HttpRequestFailed)); } } } }