|
|
@@ -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);
|