Browse Source

考勤部分接口完善

yuanrf 13 hours ago
parent
commit
696c2f8ede

+ 808 - 2
OASystem/OASystem.Api/Controllers/PersonnelModuleController.cs

@@ -1,4 +1,5 @@
 using Aspose.Cells;
+using Aspose.Words;
 using FluentValidation;
 using Microsoft.AspNetCore.SignalR;
 using OASystem.API.OAMethodLib;
@@ -21,6 +22,8 @@ using System.Data;
 using System.Diagnostics;
 using System.Globalization;
 using static OASystem.API.OAMethodLib.JWTHelper;
+using Markdig;
+using Dm.util;
 
 namespace OASystem.API.Controllers
 {
@@ -3119,7 +3122,8 @@ OPTION (MAXRECURSION 0); -- 允许无限递归      ";
         #region Ai绩效分析
 
         [HttpGet]
-        public async Task<IActionResult> AiPerformanceAnalysis_JobMarketingAsync(int userId, DateTime start, DateTime end)
+        public async Task<IActionResult> AiPerformanceAnalysis_JobMarketingAsync
+        (int userId, DateTime start, DateTime end, int createUserId)
         {
             var jw = JsonView(false);
 
@@ -3254,6 +3258,21 @@ OPTION (MAXRECURSION 0); -- 允许无限递归      ";
                     Answer = resp.Answer,
                     kaoqinAnswer = kaoqinResp.Answer
                 };
+
+                //保存至数据库中
+                Pm_PerformanceAnalysis insertData = new Pm_PerformanceAnalysis
+                {
+                    CreateTime = DateTime.Now,
+                    CreateUserId = createUserId,
+                    IsDel = 0,
+                    Year = start.Year,
+                    Month = start.Month,
+                    JsonResult = JsonConvert.SerializeObject(jw.Data),
+                    UserId = userId,
+                };
+
+                await _sqlSugar.Insertable(insertData).ExecuteCommandAsync();
+
             }
             catch (Exception ex)
             {
@@ -3267,6 +3286,781 @@ OPTION (MAXRECURSION 0); -- 允许无限递归      ";
             return Ok(jw);
         }
 
+        class wordTable
+        {
+            public int 序号 { get; set; }
+            public string 团组名 { get; set; }
+            public decimal 营业颔 { get; set; }
+
+            public string 出访日期 { get; set; }
+
+            public int 人数 { get; set; }
+
+            public string 收款日期 { get; set; }
+        };
+
+        [HttpPost]
+        public async Task<IActionResult> AiPerformanceAnalysis_JobMarketingFileDownAsync(int year, int month, int userId)
+        {
+            var jw = JsonView(false);
+
+            var user_entity = _sqlSugar.Queryable<Sys_Users>()
+                            .First(e => e.Id == userId && e.IsDel == 0);
+
+            if (user_entity == null)
+            {
+                jw.Msg = "用户不存在!";
+                return Ok(jw);
+            }
+
+            var data = new Pm_PerformanceAnalysis();
+
+            if (year < 1 && month < 1)
+            {
+                data = await _sqlSugar.Queryable<Pm_PerformanceAnalysis>()
+                .Where(x => x.UserId == userId && x.IsDel == 0)
+                .OrderByDescending(x => x.CreateTime)
+                .FirstAsync();
+            }
+            else
+            {
+                data = await _sqlSugar.Queryable<Pm_PerformanceAnalysis>()
+                .Where(x => x.UserId == userId && x.Year == year && x.Month == month && x.IsDel == 0)
+                .FirstAsync();
+            }
+
+            if (data == null)
+            {
+                jw.Msg = "数据不存在!";
+                return Ok(jw);
+            }
+
+            var jsonResult = JObject.Parse(data.JsonResult);
+
+            var answer = jsonResult["Answer"]?.toString();
+            var kaoqinAnswer = jsonResult["kaoqinAnswer"]?.toString();
+            var tableapi = await this.AiPerformanceAnalysis_GroupStatisticsAsync(
+              userId,
+                new DateTime(data.Year, data.Month, 1),
+                new DateTime(data.Year, data.Month, 1).AddMonths(1)
+                );
+
+            var jwValue = (((tableapi as OkObjectResult).Value) as OASystem.Domain.ViewModels.JsonView);
+            if (jwValue.Code != 200)
+            {
+                jw.Msg = "获取团组统计数据失败!" + jwValue.Msg;
+                return Ok(jw);
+            }
+
+            var tableList = jwValue.Data as List<AiPerformanceAnalysis_GroupStatisticsView>;
+            if (tableList == null)
+            {
+                jw.Msg = "获取团组统计数据失败!";
+                return Ok(jw);
+            }
+
+            var tableListValue = tableList.Select(x => new wordTable
+            {
+                序号 = x.RowNumber,
+                团组名 = x.TeamName,
+                营业颔 = x.GroupSales,
+                出访日期 = x.VisitDate.ToString("yyyy-MM-dd"),
+                人数 = x.VisitPNumber,
+                收款日期 = x.CollectionDays.ToString("yyyy-MM-dd")
+            }).ToList();
+
+            var url = this.MarkdownToWord(new
+             List<WordContentItem>{
+                 new
+                  WordContentItem{
+                     Type = WordContentType.Markdown,
+                    MarkdownContent = answer
+                  },
+                    WordContentItem.FromObjectList(tableListValue, "团组统计")
+                  ,
+                  new
+                  WordContentItem{
+                     Type = WordContentType.Markdown,
+                    MarkdownContent = kaoqinAnswer
+                  }
+             }, $"{user_entity.CnName}_{data.Year}年{data.Month.ToString("00")}月绩效分析");
+
+            return Ok(new
+            {
+                url
+            });
+        }
+
+        /// <summary>
+        /// Word文档内容项类型
+        /// </summary>
+        public enum WordContentType
+        {
+            Markdown,  // Markdown内容
+            Table      // 表格内容
+        }
+
+        /// <summary>
+        /// Word文档内容项
+        /// </summary>
+        public class WordContentItem
+        {
+            /// <summary>
+            /// 内容类型
+            /// </summary>
+            public WordContentType Type { get; set; }
+
+            /// <summary>
+            /// Markdown内容(当Type为Markdown时使用)
+            /// </summary>
+            public string MarkdownContent { get; set; }
+
+            /// <summary>
+            /// 表格数据(当Type为Table时使用)- 第一行为表头
+            /// </summary>
+            public List<List<string>> TableData { get; set; }
+
+            /// <summary>
+            /// 表格标题(可选)
+            /// </summary>
+            public string TableTitle { get; set; }
+
+            /// <summary>
+            /// 从DataTable创建表格数据(便捷方法)
+            /// </summary>
+            /// <param name="dataTable">数据表</param>
+            /// <param name="tableTitle">表格标题(可选)</param>
+            /// <param name="includeHeader">是否包含表头(默认true)</param>
+            /// <returns>WordContentItem实例</returns>
+            public static WordContentItem FromDataTable(DataTable dataTable, string? tableTitle = null, bool includeHeader = true)
+            {
+                if (dataTable == null || dataTable.Rows.Count == 0)
+                {
+                    return new WordContentItem
+                    {
+                        Type = WordContentType.Table,
+                        TableTitle = tableTitle,
+                        TableData = new List<List<string>>()
+                    };
+                }
+
+                var tableData = new List<List<string>>();
+
+                // 添加表头
+                if (includeHeader)
+                {
+                    var headerRow = new List<string>();
+                    foreach (DataColumn column in dataTable.Columns)
+                    {
+                        headerRow.Add(column.ColumnName);
+                    }
+                    tableData.Add(headerRow);
+                }
+
+                // 添加数据行
+                foreach (DataRow row in dataTable.Rows)
+                {
+                    var dataRow = new List<string>();
+                    foreach (DataColumn column in dataTable.Columns)
+                    {
+                        dataRow.Add(row[column]?.ToString() ?? "");
+                    }
+                    tableData.Add(dataRow);
+                }
+
+                return new WordContentItem
+                {
+                    Type = WordContentType.Table,
+                    TableTitle = tableTitle,
+                    TableData = tableData
+                };
+            }
+
+            /// <summary>
+            /// 从对象列表创建表格数据(便捷方法)
+            /// </summary>
+            /// <typeparam name="T">对象类型</typeparam>
+            /// <param name="items">对象列表</param>
+            /// <param name="tableTitle">表格标题(可选)</param>
+            /// <param name="columnMappings">列映射(属性名 -> 显示名称),如果为null则使用属性名</param>
+            /// <returns>WordContentItem实例</returns>
+            public static WordContentItem FromObjectList<T>(List<T> items, string? tableTitle = null, Dictionary<string, string>? columnMappings = null)
+            {
+                if (items == null || items.Count == 0)
+                {
+                    return new WordContentItem
+                    {
+                        Type = WordContentType.Table,
+                        TableTitle = tableTitle,
+                        TableData = new List<List<string>>()
+                    };
+                }
+
+                var tableData = new List<List<string>>();
+                var properties = typeof(T).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
+
+                // 添加表头
+                var headerRow = new List<string>();
+                foreach (var prop in properties)
+                {
+                    string headerName = columnMappings != null && columnMappings.ContainsKey(prop.Name)
+                        ? columnMappings[prop.Name]
+                        : prop.Name;
+                    headerRow.Add(headerName);
+                }
+                tableData.Add(headerRow);
+
+                // 添加数据行
+                foreach (var item in items)
+                {
+                    var dataRow = new List<string>();
+                    foreach (var prop in properties)
+                    {
+                        var value = prop.GetValue(item);
+                        dataRow.Add(value?.ToString() ?? "");
+                    }
+                    tableData.Add(dataRow);
+                }
+
+                return new WordContentItem
+                {
+                    Type = WordContentType.Table,
+                    TableTitle = tableTitle,
+                    TableData = tableData
+                };
+            }
+        }
+
+        /// <summary>
+        /// 将多个Markdown内容和表格合并转换为Word文档
+        /// </summary>
+        /// <param name="contentItems">内容项列表(可以是Markdown或表格)</param>
+        /// <param name="fileName">文件名(不含扩展名)</param>
+        /// <returns>返回文件访问URL</returns>
+        private string MarkdownToWord(List<WordContentItem> contentItems, string fileName)
+        {
+            if (contentItems == null || contentItems.Count == 0)
+            {
+                throw new ArgumentException("内容项不能为空", nameof(contentItems));
+            }
+
+            // 创建新的Word文档
+            Aspose.Words.Document doc = new Aspose.Words.Document();
+            Aspose.Words.DocumentBuilder builder = new Aspose.Words.DocumentBuilder(doc);
+
+            // 设置默认字体
+            builder.Font.Name = "微软雅黑";
+            builder.Font.Size = 10.5;
+
+            // Markdown转换管道
+            var pipeline = new Markdig.MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
+
+            // 处理每个内容项
+            for (int i = 0; i < contentItems.Count; i++)
+            {
+                var item = contentItems[i];
+
+                // 如果是第一个内容项,先清理文档开头的空段落
+                if (i == 0)
+                {
+                    // 清理文档开头的所有空段落
+                    var firstBody = doc.FirstSection.Body;
+                    while (firstBody.Paragraphs.Count > 0)
+                    {
+                        var firstPara = firstBody.Paragraphs[0];
+                        if (string.IsNullOrWhiteSpace(firstPara.GetText().Trim()))
+                        {
+                            firstPara.Remove();
+                        }
+                        else
+                        {
+                            break;
+                        }
+                    }
+                }
+                else
+                {
+                    // 如果不是第一个内容项,确保前一个段落没有过大的间距
+                    builder.MoveToDocumentEnd();
+                    var lastParagraph = doc.LastSection.Body.LastParagraph;
+                    if (lastParagraph != null)
+                    {
+                        // 减少前一个段落的间距
+                        lastParagraph.ParagraphFormat.SpaceAfter = 0;
+                        lastParagraph.ParagraphFormat.SpaceBefore = 0;
+                    }
+                    // 不插入额外的段落分隔,直接追加内容
+                }
+
+                if (item.Type == WordContentType.Markdown)
+                {
+                    // 处理Markdown内容
+                    if (!string.IsNullOrWhiteSpace(item.MarkdownContent))
+                    {
+                        // 将Markdown转换为HTML
+                        string htmlBody = Markdig.Markdown.ToHtml(item.MarkdownContent, pipeline);
+
+                        // 包装成完整的HTML片段,减少body的margin和padding
+                        string htmlContent = $@"<!DOCTYPE html>
+<html>
+<head>
+    <meta charset=""UTF-8"">
+    <meta http-equiv=""Content-Type"" content=""text/html; charset=UTF-8"">
+    <style>
+        body {{ font-family: '微软雅黑', 'Microsoft YaHei', SimSun, sans-serif; font-size: 10.5pt; line-height: 1.5; margin: 0; padding: 0; }}
+        h1, h2, h3, h4, h5, h6 {{ font-family: '微软雅黑', 'Microsoft YaHei', SimHei, sans-serif; margin-top: 12pt; margin-bottom: 6pt; }}
+        h1 {{ margin-top: 0; }}
+        p {{ margin: 0; padding: 0; }}
+        table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
+        table th, table td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
+        table th {{ background-color: #f2f2f2; font-weight: bold; }}
+        code {{ font-family: 'Consolas', 'Courier New', monospace; }}
+        pre {{ background-color: #f5f5f5; padding: 10px; border-radius: 4px; margin: 6pt 0; }}
+        ul, ol {{ margin: 6pt 0; padding-left: 20pt; }}
+        li {{ margin: 3pt 0; }}
+    </style>
+</head>
+<body>
+{htmlBody}
+</body>
+</html>";
+
+                        // 使用LoadOptions指定UTF-8编码
+                        var loadOptions = new Aspose.Words.LoadOptions
+                        {
+                            LoadFormat = Aspose.Words.LoadFormat.Html,
+                            Encoding = System.Text.Encoding.UTF8
+                        };
+
+                        // 从HTML创建临时文档
+                        byte[] htmlBytes = System.Text.Encoding.UTF8.GetBytes(htmlContent);
+                        using (MemoryStream htmlStream = new MemoryStream(htmlBytes))
+                        {
+                            Aspose.Words.Document tempDoc = new Aspose.Words.Document(htmlStream, loadOptions);
+
+                            if (i == 0)
+                            {
+                                // 第一个内容项:先清理所有空段落
+                                var firstBody = doc.FirstSection.Body;
+
+                                // 删除所有空段落
+                                while (firstBody.Paragraphs.Count > 0)
+                                {
+                                    var firstPara = firstBody.Paragraphs[0];
+                                    if (string.IsNullOrWhiteSpace(firstPara.GetText().Trim()))
+                                    {
+                                        firstPara.Remove();
+                                    }
+                                    else
+                                    {
+                                        break;
+                                    }
+                                }
+
+                                // 如果文档完全为空,直接替换整个文档内容
+                                if (firstBody.Paragraphs.Count == 0)
+                                {
+                                    // 清空原文档的所有内容
+                                    doc.FirstSection.Body.RemoveAllChildren();
+
+                                    // 将临时文档的所有内容节点复制到主文档
+                                    foreach (Aspose.Words.Node node in tempDoc.FirstSection.Body.ChildNodes.ToArray())
+                                    {
+                                        var importedNode = doc.ImportNode(node, true);
+                                        doc.FirstSection.Body.AppendChild(importedNode);
+                                    }
+                                }
+                                else
+                                {
+                                    // 文档有内容,在第一个段落之前插入
+                                    var firstPara = firstBody.Paragraphs[0];
+                                    builder.MoveTo(firstPara);
+                                    builder.InsertDocument(tempDoc, Aspose.Words.ImportFormatMode.KeepSourceFormatting);
+                                }
+
+                                // 插入后再次清理开头的空段落,并统一格式
+                                firstBody = doc.FirstSection.Body;
+                                while (firstBody.Paragraphs.Count > 0)
+                                {
+                                    var firstPara = firstBody.Paragraphs[0];
+                                    if (string.IsNullOrWhiteSpace(firstPara.GetText().Trim()))
+                                    {
+                                        firstPara.Remove();
+                                    }
+                                    else
+                                    {
+                                        // 找到第一个有内容的段落,确保它没有前间距
+                                        firstPara.ParagraphFormat.SpaceBefore = 0;
+                                        if (firstPara.ParagraphFormat.SpaceAfter > 6)
+                                        {
+                                            firstPara.ParagraphFormat.SpaceAfter = 6;
+                                        }
+
+                                        // 统一字体格式
+                                        foreach (Aspose.Words.Run run in firstPara.Runs)
+                                        {
+                                            if (string.IsNullOrEmpty(run.Font.Name) ||
+                                                (!run.Font.Name.Contains("微软") && !run.Font.Name.Contains("Microsoft") &&
+                                                 !run.Font.Name.Contains("Sim") && !run.Font.Name.Contains("宋体")))
+                                            {
+                                                run.Font.Name = "微软雅黑";
+                                            }
+                                            if (firstPara.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading1 &&
+                                                firstPara.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading2 &&
+                                                firstPara.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading3 &&
+                                                firstPara.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading4 &&
+                                                (run.Font.Size == 0 || (run.Font.Size != 12 && run.Font.Size != 14 && run.Font.Size != 16 && run.Font.Size != 18 && run.Font.Size != 20 && run.Font.Size != 24)))
+                                            {
+                                                run.Font.Size = 10.5;
+                                            }
+                                        }
+                                        break;
+                                    }
+                                }
+
+                                // 统一处理第一个内容项的所有段落格式
+                                foreach (Aspose.Words.Paragraph para in firstBody.Paragraphs)
+                                {
+                                    if (string.IsNullOrWhiteSpace(para.GetText().Trim()))
+                                    {
+                                        continue;
+                                    }
+
+                                    // 统一段落格式
+                                    para.ParagraphFormat.SpaceBefore = 0;
+                                    if (para.ParagraphFormat.SpaceAfter > 6)
+                                    {
+                                        para.ParagraphFormat.SpaceAfter = 6;
+                                    }
+
+                                    // 统一字体格式 - 与非第一个内容项的处理逻辑完全一致
+                                    foreach (Aspose.Words.Run run in para.Runs)
+                                    {
+                                        // 统一字体名称 - 强制设置为微软雅黑
+                                        if (string.IsNullOrEmpty(run.Font.Name) ||
+                                            (!run.Font.Name.Contains("微软") && !run.Font.Name.Contains("Microsoft") &&
+                                             !run.Font.Name.Contains("Sim") && !run.Font.Name.Contains("宋体")))
+                                        {
+                                            run.Font.Name = "微软雅黑";
+                                        }
+
+                                        // 统一字体大小 - 与非第一个内容项的逻辑完全一致
+                                        bool isHeading = para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading1 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading2 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading3 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading4;
+
+                                        if (!isHeading)
+                                        {
+                                            // 非标题:强制统一设置为10.5pt,确保与非第一个内容项一致
+                                            run.Font.Size = 10.5;
+                                        }
+                                        // 标题保持原有大小,但字体名称已在上面的逻辑中统一
+                                    }
+                                }
+                            }
+                            else
+                            {
+                                // 非第一个内容项:记录追加前的段落数量
+                                int paraCountBefore = doc.FirstSection.Body.Paragraphs.Count;
+
+                                // 追加到文档末尾
+                                doc.AppendDocument(tempDoc, Aspose.Words.ImportFormatMode.KeepSourceFormatting);
+
+                                // 追加后立即清理格式,确保与第一个内容项格式一致
+                                var appendedBody = doc.FirstSection.Body;
+                                var allParagraphs = appendedBody.Paragraphs;
+
+                                // 处理所有追加的段落(从paraCountBefore开始)
+                                for (int paraIdx = paraCountBefore; paraIdx < allParagraphs.Count; paraIdx++)
+                                {
+                                    var para = allParagraphs[paraIdx];
+
+                                    // 删除空段落
+                                    if (string.IsNullOrWhiteSpace(para.GetText().Trim()))
+                                    {
+                                        para.Remove();
+                                        paraIdx--; // 调整索引
+                                        continue;
+                                    }
+
+                                    // 统一段落格式:移除前间距,限制后间距
+                                    para.ParagraphFormat.SpaceBefore = 0;
+                                    if (para.ParagraphFormat.SpaceAfter > 6)
+                                    {
+                                        para.ParagraphFormat.SpaceAfter = 6;
+                                    }
+
+                                    // 统一字体格式 - 与第一个内容项的处理逻辑完全一致
+                                    foreach (Aspose.Words.Run run in para.Runs)
+                                    {
+                                        // 统一字体名称 - 强制设置为微软雅黑
+                                        if (string.IsNullOrEmpty(run.Font.Name) ||
+                                            (!run.Font.Name.Contains("微软") && !run.Font.Name.Contains("Microsoft") &&
+                                             !run.Font.Name.Contains("Sim") && !run.Font.Name.Contains("宋体")))
+                                        {
+                                            run.Font.Name = "微软雅黑";
+                                        }
+
+                                        // 统一字体大小 - 与第一个内容项的逻辑完全一致
+                                        bool isHeading = para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading1 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading2 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading3 ||
+                                                         para.ParagraphFormat.StyleIdentifier == Aspose.Words.StyleIdentifier.Heading4;
+
+                                        if (!isHeading)
+                                        {
+                                            // 非标题:强制统一设置为10.5pt,确保与第一个内容项一致
+                                            run.Font.Size = 10.5;
+                                        }
+                                        // 标题保持原有大小,但字体名称已在上面的逻辑中统一
+                                    }
+                                }
+
+                                // 确保追加内容前的最后一个段落没有过大的后间距
+                                if (paraCountBefore > 0 && paraCountBefore <= allParagraphs.Count)
+                                {
+                                    var lastParaBefore = allParagraphs[paraCountBefore - 1];
+                                    if (lastParaBefore != null)
+                                    {
+                                        lastParaBefore.ParagraphFormat.SpaceAfter = 0;
+                                    }
+                                }
+                            }
+                        }
+
+                        // 移动builder到文档末尾
+                        builder.MoveToDocumentEnd();
+                    }
+                }
+                else if (item.Type == WordContentType.Table)
+                {
+                    // 处理表格内容
+                    if (item.TableData != null && item.TableData.Count > 0)
+                    {
+                        // 确保builder在正确位置
+                        if (i == 0)
+                        {
+                            // 第一个内容项,确保在文档开头
+                            builder.MoveToDocumentStart();
+                            // 清理开头的空段落
+                            var tableBody = doc.FirstSection.Body;
+                            while (tableBody.Paragraphs.Count > 0)
+                            {
+                                var firstPara = tableBody.Paragraphs[0];
+                                if (string.IsNullOrWhiteSpace(firstPara.GetText().Trim()))
+                                {
+                                    firstPara.Remove();
+                                }
+                                else
+                                {
+                                    break;
+                                }
+                            }
+                            builder.MoveToDocumentStart();
+                        }
+                        else
+                        {
+                            builder.MoveToDocumentEnd();
+                        }
+
+                        // 添加表格标题(如果有)
+                        if (!string.IsNullOrWhiteSpace(item.TableTitle))
+                        {
+                            builder.InsertBreak(Aspose.Words.BreakType.ParagraphBreak);
+                            builder.InsertBreak(Aspose.Words.BreakType.ParagraphBreak);
+                            builder.InsertBreak(Aspose.Words.BreakType.ParagraphBreak);
+
+                            builder.ParagraphFormat.SpaceAfter = 6;
+                            builder.ParagraphFormat.SpaceBefore = 0; // 表格标题前不添加间距
+                            builder.Font.Size = 12;
+                            builder.Font.Bold = true;
+                            builder.Font.Name = "微软雅黑";
+                            builder.Writeln(item.TableTitle);
+                            builder.Font.Bold = false;
+                            builder.Font.Size = 10.5;
+                            builder.ParagraphFormat.SpaceAfter = 3; // 标题和表格之间的小间距
+                        }
+
+                        // 计算列数(取第一行的列数)
+                        int columnCount = item.TableData[0]?.Count ?? 0;
+                        if (columnCount == 0)
+                        {
+                            continue; // 跳过空表格
+                        }
+
+                        // 创建表格
+                        Aspose.Words.Tables.Table table = builder.StartTable();
+
+                        // 设置列宽(平均分配)- 在添加行之前设置
+                        for (int colIndex = 0; colIndex < columnCount; colIndex++)
+                        {
+                            builder.CellFormat.PreferredWidth = Aspose.Words.Tables.PreferredWidth.FromPercent(100.0 / columnCount);
+                        }
+
+                        // 先添加表头(第一行),这样表格就不是空的了
+                        if (item.TableData.Count > 0)
+                        {
+                            var headerRow = item.TableData[0];
+                            foreach (var headerCell in headerRow)
+                            {
+                                builder.InsertCell();
+                                builder.CellFormat.VerticalAlignment = Aspose.Words.Tables.CellVerticalAlignment.Center;
+                                builder.CellFormat.Shading.BackgroundPatternColor = System.Drawing.Color.FromArgb(240, 240, 240);
+                                builder.ParagraphFormat.Alignment = Aspose.Words.ParagraphAlignment.Center;
+                                builder.Font.Bold = true;
+                                builder.Font.Size = 10.5;
+                                builder.Font.Name = "微软雅黑";
+                                builder.Font.Color = System.Drawing.Color.Black;
+                                builder.Write(headerCell ?? "");
+                                builder.Font.Bold = false;
+                            }
+                            builder.EndRow();
+                        }
+
+                        // 现在表格有行了,可以设置表格样式
+                        table.StyleIdentifier = Aspose.Words.StyleIdentifier.LightGridAccent1;
+                        table.AllowAutoFit = true;
+                        table.PreferredWidth = Aspose.Words.Tables.PreferredWidth.FromPercent(100);
+                        table.Alignment = Aspose.Words.Tables.TableAlignment.Center;
+
+                        // 添加数据行
+                        for (int rowIndex = 1; rowIndex < item.TableData.Count; rowIndex++)
+                        {
+                            var row = item.TableData[rowIndex];
+                            // 确保列数一致
+                            while (row.Count < columnCount)
+                            {
+                                row.Add("");
+                            }
+
+                            foreach (var cell in row.Take(columnCount))
+                            {
+                                builder.InsertCell();
+                                builder.CellFormat.VerticalAlignment = Aspose.Words.Tables.CellVerticalAlignment.Center;
+                                builder.CellFormat.Shading.BackgroundPatternColor = System.Drawing.Color.White;
+                                builder.ParagraphFormat.Alignment = Aspose.Words.ParagraphAlignment.Left;
+                                builder.Font.Size = 10.5;
+                                builder.Font.Name = "微软雅黑";
+                                builder.Write(cell ?? "");
+                            }
+                            builder.EndRow();
+                        }
+
+                        builder.EndTable();
+
+                        // 设置较小的段落间距
+                        builder.ParagraphFormat.SpaceAfter = 6;
+                    }
+                }
+            }
+
+            // 清理文档开头的空段落,避免第一段内容前出现空白页
+            var body = doc.FirstSection.Body;
+            var paragraphs = body.Paragraphs;
+            while (paragraphs.Count > 0)
+            {
+                var firstPara = paragraphs[0];
+                var paraText = firstPara.GetText().Trim();
+                // 如果第一个段落是空的或只包含空白字符,删除它
+                if (string.IsNullOrWhiteSpace(paraText))
+                {
+                    firstPara.Remove();
+                }
+                else
+                {
+                    // 找到第一个有内容的段落,确保它没有前间距
+                    firstPara.ParagraphFormat.SpaceBefore = 0;
+                    break;
+                }
+            }
+
+            // 遍历所有段落,确保字体设置正确,并统一段落间距
+            foreach (Aspose.Words.Paragraph paragraph in doc.GetChildNodes(Aspose.Words.NodeType.Paragraph, true))
+            {
+                // 统一段落间距,避免内容项之间出现大段空白
+                if (paragraph.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading1 &&
+                    paragraph.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading2 &&
+                    paragraph.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading3 &&
+                    paragraph.ParagraphFormat.StyleIdentifier != Aspose.Words.StyleIdentifier.Heading4)
+                {
+                    // 普通段落保持较小的间距
+                    if (paragraph.ParagraphFormat.SpaceAfter > 6)
+                    {
+                        paragraph.ParagraphFormat.SpaceAfter = 6;
+                    }
+                    // 移除段落前的间距,避免内容项之间出现空白
+                    if (paragraph.ParagraphFormat.SpaceBefore > 0)
+                    {
+                        paragraph.ParagraphFormat.SpaceBefore = 0;
+                    }
+                }
+
+                foreach (Aspose.Words.Run run in paragraph.Runs)
+                {
+                    // 如果字体不是中文字体,设置为微软雅黑
+                    if (string.IsNullOrEmpty(run.Font.Name) ||
+                        (!run.Font.Name.Contains("微软") && !run.Font.Name.Contains("Microsoft") &&
+                         !run.Font.Name.Contains("Sim") && !run.Font.Name.Contains("宋体")))
+                    {
+                        run.Font.Name = "微软雅黑";
+                    }
+                }
+            }
+
+            // 保存文件
+            string saveFolder = $"{AppSettingsHelper.Get("WordBasePath")}PerformanceAnalysis";
+            if (!Directory.Exists(saveFolder))
+            {
+                Directory.CreateDirectory(saveFolder);
+            }
+
+            string outputFile = $"{fileName}_{DateTime.Now:yyyyMMddHHmmss}.pdf";
+            string filePath = Path.Combine(saveFolder, outputFile);
+
+            doc.Save(filePath, Aspose.Words.SaveFormat.Pdf);
+
+            // 返回访问URL
+            return $"{AppSettingsHelper.Get("WordBaseUrl")}Office/Word/PerformanceAnalysis/{outputFile}";
+        }
+
+        [HttpGet]
+        public async Task<IActionResult> AiPerformanceAnalysis_QueryAsync(int year, int month, int userId)
+        {
+            if (userId < 1)
+            {
+                return Ok(JsonView(false, "请传入有效的userId参数!"));
+            }
+
+            var data = new Pm_PerformanceAnalysis();
+
+            if (year < 1 && month < 1)
+            {
+                data = await _sqlSugar.Queryable<Pm_PerformanceAnalysis>()
+                .Where(x => x.UserId == userId && x.IsDel == 0)
+                .OrderByDescending(x => x.CreateTime)
+                .FirstAsync();
+            }
+            else
+            {
+                data = await _sqlSugar.Queryable<Pm_PerformanceAnalysis>()
+                .Where(x => x.UserId == userId && x.Year == year && x.Month == month && x.IsDel == 0)
+                .OrderByDescending(x => x.CreateTime)
+                .FirstAsync();
+            }
+
+            var result = JObject.Parse(data.JsonResult);
+
+            return Ok(JsonView(true, "操作成功!", new
+            {
+                data.JsonResult,
+                data.Year,
+                data.Month,
+                data.Id,
+                data.CreateTime,
+                data = result
+            }));
+        }
 
         /// <summary>
         /// Ai绩效分析
@@ -3313,11 +4107,23 @@ OPTION (MAXRECURSION 0); -- 允许无限递归      ";
 
             RefAsync<int> total = 0;
 
+            var notidsJson = _sqlSugar.Queryable<Sys_SetData>().First(x => x.Id == 1463 && x.IsDel == 0)?.Remark;
+            var notids = new List<int>();
+            if (!string.IsNullOrWhiteSpace(notidsJson))
+            {
+                try
+                {
+                    notids = JsonConvert.DeserializeObject<List<int>>(notidsJson) ?? new List<int>();
+                }
+                catch
+                { }
+            }
+
             var query = _sqlSugar.Queryable<Sys_Users>()
                 .LeftJoin<Sys_Company>((u, c) => u.CompanyId == c.Id)
                 .LeftJoin<Sys_Department>((u, c, d) => u.DepId == d.Id)
                 .LeftJoin<Sys_JobPost>((u, c, d, jp) => u.JobPostId == jp.Id)
-                .Where((u, c, d, jp) => u.IsDel == 0)
+                .Where((u, c, d, jp) => u.IsDel == 0 && !notids.Contains(u.Id))
                 .WhereIF(!string.IsNullOrEmpty(dto.ScreeningCriteria?.Trim()), (u, c, d, jp) =>
                     u.CnName.Contains(dto.ScreeningCriteria.Trim()) ||
                     c.CompanyName.Contains(dto.ScreeningCriteria.Trim()) ||

+ 1 - 0
OASystem/OASystem.Api/OASystem.API.csproj

@@ -48,6 +48,7 @@
     <PackageReference Include="EPPlus" Version="7.4.1" />
     <PackageReference Include="Flurl.Http" Version="3.2.4" />
     <PackageReference Include="JsonDiffPatch.Net" Version="2.3.0" />
+    <PackageReference Include="Markdig" Version="0.33.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.11" />
     <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0" />
     <PackageReference Include="NodaTime" Version="3.2.0" />

+ 10 - 0
OASystem/OASystem.Domain/Entities/PersonnelModule/Pm_PerformanceAnalysis.cs

@@ -0,0 +1,10 @@
+namespace OASystem.Domain.Entities.PersonnelModule
+{
+    public class Pm_PerformanceAnalysis : EntityBase
+    {
+        public int UserId { get; set; }
+        public int Year { get; set; }
+        public int Month { get; set; }
+        public string JsonResult { get; set; }
+    }
+}