Преглед изворни кода

hotmail发送邮件新增附件

Lyyyi пре 2 дана
родитељ
комит
f43eebafd6

+ 232 - 10
OASystem/OASystem.Api/Controllers/ResourceController.cs

@@ -1,9 +1,11 @@
 using Aspose.Cells;
+using Aspose.Cells.Drawing;
 using Aspose.Words;
 using Aspose.Words.Tables;
 using Azure;
 using EyeSoft.Extensions;
 using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Graph.Models;
 using Newtonsoft.Json.Serialization;
 using NodaTime;
 using NPOI.SS.Formula.Functions;
@@ -30,6 +32,7 @@ using static OASystem.API.OAMethodLib.GeneralMethod;
 using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
 using static OASystem.API.OAMethodLib.JWTHelper;
 using static OpenAI.GPT3.ObjectModels.Models;
+using Workbook = Aspose.Cells.Workbook;
 
 namespace OASystem.API.Controllers
 {
@@ -3596,7 +3599,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             // A4页面设置
             double usableWidth = 495;
 
-            foreach (Section sec in doc.Sections)
+            foreach (Aspose.Words.Section sec in doc.Sections)
             {
                 sec.PageSetup.PaperSize = Aspose.Words.PaperSize.A4;
                 sec.PageSetup.LeftMargin = 50;
@@ -4259,7 +4262,227 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
         }
 
         /// <summary>
-        /// 商邀资料AI 发送邮件(默认当前用户企业微信邮件)
+        /// 商邀资料AI 邮箱附件上传
+        /// </summary>
+        [HttpPost]
+        [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
+        public async Task<IActionResult> InvitationAIFileSave([FromForm] InvitationAIFileSaveDto dto)
+        {
+            // 1. 炼金前置:严格参数校验
+            if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的团组"));
+            if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
+            if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
+            if (dto.Attachments == null || !dto.Attachments.Any()) return Ok(JsonView(false, "请传入附件"));
+
+            // 2. 数据获取:利用 SqlSugar 异步查询
+            var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
+                .InSingleAsync(dto.Id);
+
+            if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "邀请方信息不存在"));
+
+            // 3. 业务定位:定位到具体需要编辑的 Guid 记录
+            var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
+            if (editInfo == null) return Ok(JsonView(false, "未找到匹配的 Guid 记录"));
+
+            // 4. 追踪信息更新
+            var opUserName = await _sqlSugar.Queryable<Sys_Users>()
+                .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
+                .Select(x => x.CnName)
+                .FirstAsync() ?? "-";
+
+            // 更新操作人与时间 (利用引用类型特性)
+            editInfo.Operator = opUserName;
+            editInfo.OperatedAt = DateTime.Now;
+
+            // 初始化 EmailInfo 确保不为 null
+            editInfo.EmailInfo ??= new EmailInfo();
+            editInfo.EmailInfo.Operator = opUserName;
+            editInfo.EmailInfo.OperatedAt = DateTime.Now;
+            editInfo.EmailInfo.AttachmentPaths ??= new List<string>();
+
+            // 5. 构建绝对路径与相对路径
+            string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
+            string baseDir = AppSettingsHelper.Get("InvitationAIAssistBasePath");
+            string ftpBase = AppSettingsHelper.Get("InvitationAIAssistFtpPath");
+
+            string absolutePath = Path.Combine(baseDir, dirName);
+            string relativePathPrefix = $"{ftpBase}{dirName}/";
+
+            // 核心修复:确保父级目录递归创建,防止 DirectoryNotFoundException
+            if (!Directory.Exists(absolutePath))
+            {
+                Directory.CreateDirectory(absolutePath);
+            }
+
+            var newSavedRelativePaths = new List<string>();
+
+            // 6. 持续文件存储与实时验证
+            foreach (var file in dto.Attachments)
+            {
+                // 文件名安全过滤
+                string safeName = string.Join("_", file.FileName.Split(Path.GetInvalidFileNameChars()));
+                string fullPath = Path.Combine(absolutePath, safeName);
+
+                // 重名冲突处理:文件名(n).ext
+                if (System.IO.File.Exists(fullPath))
+                {
+                    string fileNameOnly = Path.GetFileNameWithoutExtension(safeName);
+                    string extension = Path.GetExtension(safeName);
+                    int count = 1;
+                    while (System.IO.File.Exists(fullPath))
+                    {
+                        safeName = $"{fileNameOnly}({count++}){extension}";
+                        fullPath = Path.Combine(absolutePath, safeName);
+                    }
+                }
+
+                try
+                {
+                    // 写入流:使用作用域隔离,确保退出 using 时立即释放句柄
+                    using (var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
+                    {
+                        await file.CopyToAsync(stream);
+                        await stream.FlushAsync(); // 强制落盘
+                    }
+
+                    // 实时验证:物理验证 + 逻辑校验
+                    var fileInfo = new FileInfo(fullPath);
+                    if (fileInfo.Exists && fileInfo.Length == file.Length)
+                    {
+                        newSavedRelativePaths.Add($"{relativePathPrefix}{safeName}");
+                    }
+                }
+                catch (Exception ex)
+                {
+                    // 发生 IO 异常时清理残余文件并抛出,触发外层处理
+                    if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
+                    // 此处记录日志:
+                    _logger.LogError(ex, "File Save Error");
+                    continue; // 跳过失败的文件,继续下一个
+                }
+            }
+
+            if (!newSavedRelativePaths.Any()) return Ok(JsonView(false, "所有文件上传均失败"));
+
+            // 7. 数据合并与持久化:
+            // 将新路径与旧路径合并,并去重
+            var updatedPaths = editInfo.EmailInfo.AttachmentPaths;
+            updatedPaths.AddRange(newSavedRelativePaths);
+            editInfo.EmailInfo.AttachmentPaths = updatedPaths.Distinct().ToList();
+
+            // 重新排序整体列表
+            invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
+                .OrderByDescending(x => x.OperatedAt)
+                .ToList();
+
+            // SqlSugar 高效部分列更新
+            var isSuccess = await _sqlSugar.Updateable(invAiInfo)
+                .UpdateColumns(x => x.AiCrawledDetails)
+                .ExecuteCommandAsync() > 0;
+
+            if (!isSuccess) return Ok(JsonView(false, "数据库更新失败"));
+
+            // 8. 返回前端数据 (支持 lowerCamelCase)
+            var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
+            var finalResultUrls = editInfo.EmailInfo.AttachmentPaths
+                .Select(path => $"{officeBaseUrl}{path}")
+                .ToList();
+
+            return Ok(JsonView(true, "邮件附件保存成功", finalResultUrls));
+        }
+
+        /// <summary>
+        /// 商邀资料AI 邮箱附件删除
+        /// </summary>
+        [HttpPost]
+        [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
+        public async Task<IActionResult> InvitationAIFileDel([FromBody] InvitationAIFileDelDto dto)
+        {
+            // 1. 基础校验
+            if (dto.Id < 1) return Ok(JsonView(false, "请选择团组"));
+            if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
+            if (dto.AttachmentNames == null || !dto.AttachmentNames.Any()) return Ok(JsonView(false, "请传入待删除附件名称"));
+
+            // 2. 数据获取
+            var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().InSingleAsync(dto.Id);
+            if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "业务数据不存在"));
+
+            var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
+            if (editInfo?.EmailInfo?.AttachmentPaths == null) return Ok(JsonView(false, "未找到对应的附件记录"));
+
+            // 3. 操作人追踪
+            var opUserName = await _sqlSugar.Queryable<Sys_Users>()
+                .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
+                .Select(x => x.CnName).FirstAsync() ?? "System";
+
+            // 更新追踪状态
+            editInfo.Operator = editInfo.EmailInfo.Operator = opUserName;
+            editInfo.OperatedAt = editInfo.EmailInfo.OperatedAt = DateTime.Now;
+
+            // 4. 路径
+            string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
+            string absoluteDir = Path.Combine(AppSettingsHelper.Get("InvitationAIAssistBasePath"), dirName);
+
+            // 获取当前数据库中的相对路径列表
+            var currentPaths = editInfo.EmailInfo.AttachmentPaths;
+            bool isChanged = false;
+
+            // 5. 核心删除逻辑
+            foreach (var fileName in dto.AttachmentNames)
+            {
+                // 查找数据库中是否存在包含该文件名的路径 (忽略大小写比较)
+                var targetRelativePath = currentPaths.FirstOrDefault(p => p.EndsWith("/" + fileName) || p.Equals(fileName));
+
+                if (targetRelativePath != null)
+                {
+                    // A. 从数据库记录中移除
+                    currentPaths.Remove(targetRelativePath);
+                    isChanged = true;
+
+                    // B. 物理文件删除逻辑
+                    // 注意:fullPath 必须是 [基础路径] + [目录名] + [纯文件名]
+                    string fullPath = Path.Combine(absoluteDir, fileName);
+
+                    try
+                    {
+                        if (System.IO.File.Exists(fullPath))
+                        {
+                            System.IO.File.Delete(fullPath);
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        // 记录 IO 异常但不中断流程
+                        _logger.LogWarning($"物理文件删除失败: {fullPath}, {ex.Message}");
+                    }
+                }
+            }
+
+            if (!isChanged) return Ok(JsonView(false, "未找到匹配的可删除附件"));
+
+            // 6. 数据同步与持久化
+            editInfo.EmailInfo.AttachmentPaths = currentPaths;
+
+            // 排序 (利用引用类型,无需手动 Add/Remove)
+            invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
+
+            var isUpd = await _sqlSugar.Updateable(invAiInfo)
+                .UpdateColumns(x => x.AiCrawledDetails)
+                .ExecuteCommandAsync() > 0;
+
+            if (!isUpd) return Ok(JsonView(false, "数据库记录更新失败"));
+
+            // 7. 返回剩余附件的完整访问地址
+            var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
+            var remainingUrls = editInfo.EmailInfo.AttachmentPaths
+                .Select(path => $"{officeBaseUrl}{path}")
+                .ToList();
+
+            return Ok(JsonView(true, "附件删除成功", remainingUrls));
+        }
+
+        /// <summary>
+        /// 商邀资料AI 发送邮件(Hotmail)
         /// </summary>
         /// <returns></returns>
         [HttpPost]
@@ -4301,7 +4524,6 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             {
                 int seedStatus = 5;
 
-
                 if (string.IsNullOrEmpty(item.EmailInfo?.EmailTitle) || string.IsNullOrEmpty(item.EmailInfo?.EmailContent))
                 {
                     msgSb.Append($"跳过:{item.NameCn} (邮件标题或内容缺失)");
@@ -4314,12 +4536,12 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     {
                         Subject = item.EmailInfo.EmailTitle,
                         Content = item.EmailInfo.EmailContent,
-                        To = item.Email
+                        To = item.Email,
+                        AttachmentPaths = item.EmailInfo.AttachmentPaths,
                     };
 
                     var res = await _hotmailService.SendMailAsync(hotmailConfig.UserName, req);
 
-
                     if (res.IsSuccess)
                     {
                         seedStatus = 4;
@@ -4337,8 +4559,8 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "商邀AI调用企业微信邮件API失败。");
-                    msgSb.AppendLine($"{item.NameCn}({item.Email}):邮件发送失败(msg:商邀AI调用企业微信邮件API失败。)");
+                    _logger.LogError(ex, "商邀AI调用Hotmail邮件API失败。");
+                    msgSb.AppendLine($"{item.NameCn}({item.Email}):邮件发送失败(msg:商邀AI调用Hotmail邮件API失败。)");
                 }
             }
 
@@ -4354,7 +4576,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
             var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
             if (update < 1)
             {
-                return Ok(JsonView(false, $"邮件AI发送,数据保存失败!"));
+                return Ok(JsonView(false, $"Hotmail邮件发送,数据保存失败!"));
             }
 
             return Ok(JsonView(true, msgSb.ToString()));
@@ -4397,7 +4619,6 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                     .Select(x => new { x.Email, x.CnName })
                     .FirstAsync();
 
-
                 if (invAiInfo?.AiCrawledDetails == null)
                 {
                     await HttpContext.SendSseStepAsync(-1, "未找到有效的邀请方数据。");
@@ -4442,7 +4663,8 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                         var req = new MailDto() {
                             Subject = item.EmailInfo.EmailTitle,
                             Content = item.EmailInfo.EmailContent,
-                            To = item.Email
+                            To = item.Email,
+                            AttachmentPaths = item.EmailInfo.AttachmentPaths
                         };
 
                         var res = await _hotmailService.SendMailAsync(hotmailConfig.UserName, req);

+ 41 - 3
OASystem/OASystem.Api/OAMethodLib/Hotmail/HotmailService.cs

@@ -3,6 +3,7 @@ using Microsoft.Graph;
 using Microsoft.Graph.Models;
 using Microsoft.Graph.Models.ODataErrors;
 using Microsoft.Kiota.Abstractions.Authentication;
+using MimeKit;
 using System.Collections.Concurrent;
 using System.Text.Json;
 using System.Text.Json.Serialization;
@@ -185,6 +186,36 @@ namespace OASystem.API.OAMethodLib.Hotmail
             {
                 var client = await GetClientAsync(fromEmail, _sqlSugar);
 
+                // 1. 构建附件列表
+                var graphAttachments = new List<Attachment>();
+                if (mail.AttachmentPaths?.Any() == true)
+                {
+                    // 获取物理根路径
+                    string baseDir = AppSettingsHelper.Get("InvitationAIAssistBasePath");
+
+                    foreach (var path in mail.AttachmentPaths)
+                    {
+                        // 相对路径,需要提取出文件名并拼接物理全路径
+                        string fileName = Path.GetFileName(path);
+
+                        string directoryPath = Path.GetDirectoryName(path);
+                        string dirName = Path.GetFileName(directoryPath);
+
+                        string fullPath = Path.Combine(baseDir, dirName, fileName);
+
+                        if (System.IO.File.Exists(fullPath))
+                        {
+                            byte[] contentBytes = await System.IO.File.ReadAllBytesAsync(fullPath);
+                            graphAttachments.Add(new FileAttachment
+                            {
+                                Name = fileName,
+                                ContentBytes = contentBytes,
+                                ContentType = MimeTypes.GetMimeType(fileName) // 需要安装 MimeTypes 库或手动判断
+                            });
+                        }
+                    }
+                }
+
                 var requestBody = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody
                 {
                     Message = new Message
@@ -196,9 +227,10 @@ namespace OASystem.API.OAMethodLib.Hotmail
                             ContentType = BodyType.Html
                         },
                         ToRecipients = new List<Recipient>
-                {
-                    new Recipient { EmailAddress = new EmailAddress { Address = mail.To } }
-                }
+                        {
+                            new Recipient { EmailAddress = new EmailAddress { Address = mail.To } }
+                        },
+                        Attachments = graphAttachments
                     }
                 };
 
@@ -518,6 +550,12 @@ namespace OASystem.API.OAMethodLib.Hotmail
             [JsonPropertyName("content")]
             public string? Content { get; set; }
 
+            /// <summary>
+            /// 附件地址
+            /// </summary>
+            [JsonPropertyName("attachments")]
+            public List<string> AttachmentPaths { get; set; } = new List<string>();
+
             /// <summary>
             /// 接收时间 - 使用 DateTimeOffset 以确保跨时区准确性
             /// </summary>

+ 28 - 1
OASystem/OASystem.Domain/Dtos/Resource/InvitationAI.cs

@@ -1,4 +1,5 @@
-using OASystem.Domain.Entities.Resource;
+using Microsoft.AspNetCore.Http;
+using OASystem.Domain.Entities.Resource;
 
 namespace OASystem.Domain.Dtos.Resource
 {
@@ -89,6 +90,32 @@ namespace OASystem.Domain.Dtos.Resource
         public string EmailContent { get; set; }
     }
 
+    public class InvitationAIFileSaveDto : InvitationAISearchDto
+    {
+        /// <summary>
+        /// Guid
+        /// </summary>
+        public string Guid { get; set; }
+
+        /// <summary>
+        /// 附件
+        /// </summary>
+        public List<IFormFile> Attachments { get; set; }
+    }
+
+
+    public class InvitationAIFileDelDto : InvitationAISearchDto
+    {
+        /// <summary>
+        /// Guid
+        /// </summary>
+        public string Guid { get; set; }
+
+        /// <summary>
+        /// 附件Names
+        /// </summary>
+        public List<string> AttachmentNames { get; set; }
+    }
 
     public class InvitationAIGenerateEmailDto: InvitationAISearchDto
     {

+ 5 - 0
OASystem/OASystem.Domain/Entities/Resource/Res_InvitationAI.cs

@@ -210,6 +210,11 @@ namespace OASystem.Domain.Entities.Resource
         /// </summary>
         public string EmailContent { get; set; }
 
+        /// <summary>
+        /// 附件地址
+        /// </summary>
+        public List<string> AttachmentPaths { get; set; }
+
         /// <summary>
         /// 发送状态
         /// 1.未开始