Sfoglia il codice sorgente

添加每周五四点定时任务。

yuanrf 3 giorni fa
parent
commit
020dbf31f8

File diff suppressed because it is too large
+ 32 - 1
OASystem/OASystem.Api/Controllers/PersonnelModuleController.cs


+ 44 - 3
OASystem/OASystem.Api/Controllers/TaskController.cs

@@ -12,6 +12,8 @@ using Dm.util;
 using OASystem.API.OAMethodLib.DoubaoAPI;
 using OASystem.API.OAMethodLib.QiYeWeChatAPI;
 using OASystem.Domain.Dtos.QiYeWeChat;
+using OASystem.Domain.ViewModels.QiYeWeChat;
+using QuzrtzJob.Factory;
 
 namespace OASystem.API.Controllers
 {
@@ -26,11 +28,14 @@ namespace OASystem.API.Controllers
         readonly SqlSugarClient _sqlsugar;
         readonly IDoubaoService _doubaoService;
         readonly IQiYeWeChatApiService _qiYeWeChatApiService;
-        public TaskController(SqlSugarClient sqlsugar, IDoubaoService doubaoService, IQiYeWeChatApiService qiYeWeChatApiService)
+        readonly QuartzFactory _quartzFactory;
+
+        public TaskController(SqlSugarClient sqlsugar, IDoubaoService doubaoService, IQiYeWeChatApiService qiYeWeChatApiService, QuartzFactory quartzFactory)
         {
             _sqlsugar = sqlsugar;
             _doubaoService = doubaoService;
             _qiYeWeChatApiService = qiYeWeChatApiService;
+            _quartzFactory = quartzFactory;
         }
 
         /// <summary>
@@ -1123,7 +1128,7 @@ namespace OASystem.API.Controllers
         }
 
         [HttpPost]
-        public async Task<IActionResult> TestQiYeWeChat(DateTime startDt, DateTime endDt, int cursor = 0, int limit = 10)
+        public async Task<IActionResult> TestQiYeWeChat(DateTime startDt, DateTime endDt, int cursor = 0, int limit = 10, int userId = 0)
         {
             var jw = JsonView(false);
             List<JournalFilter> filters = new List<JournalFilter>()
@@ -1131,6 +1136,13 @@ namespace OASystem.API.Controllers
                 new JournalFilter() { key = JournalFilterKey.template_id, value = "C4Rby4W2jqwk9mfiN3fZbQZHaawzwNZgn8vdt4g8y" }
             };
 
+            var user_entity = _sqlsugar.Queryable<Sys_Users>()
+                              .First(e => e.Id == userId && e.IsDel == 0);
+            if (user_entity != null)
+            {
+                filters.Add(new JournalFilter() { key = JournalFilterKey.creator, value = user_entity.QiyeChatUserId });
+            }
+
             var result = await _qiYeWeChatApiService.GetJournalRecordListAsync(startDt, endDt, cursor, limit, filters);
             jw.Code = 200;
             jw.Msg = "SUCCESS!";
@@ -1143,9 +1155,38 @@ namespace OASystem.API.Controllers
         {
             var jw = JsonView(false);
             var result = await _qiYeWeChatApiService.GetJournalRecordDetailAsync(journaluuid);
+            var client = result.info.GetFieldValue("客户单位信息", "单位全称");
+
             jw.Code = 200;
             jw.Msg = "SUCCESS!";
-            jw.Data = result;
+            jw.Data = new
+            {
+                result,
+                client
+            };
+
+            return Ok(jw);
+        }
+
+        /// <summary>
+        /// 手动触发 Quartz 定时任务
+        /// </summary>
+        /// <param name="jobName">任务名称,如 job7(WeeklyFridayJob)</param>
+        /// <param name="groupName">分组名称,默认 group</param>
+        [HttpPost]
+        public async Task<IActionResult> TriggerQuartzJob(string jobName = "job7", string groupName = "group")
+        {
+            var jw = JsonView(false);
+            try
+            {
+                await _quartzFactory.TriggerJobNow(jobName, groupName);
+                jw.Code = 200;
+                jw.Msg = $"任务 {jobName} 已触发执行";
+            }
+            catch (Exception ex)
+            {
+                jw.Msg = $"触发失败:{ex.Message}";
+            }
             return Ok(jw);
         }
     }

+ 163 - 0
OASystem/OASystem.Api/OAMethodLib/Quartz/Jobs/WeeklyFridayJob.cs

@@ -0,0 +1,163 @@
+using Quartz;
+using OASystem.API.OAMethodLib.QiYeWeChatAPI;
+using OASystem.Domain.ViewModels.QiYeWeChat;
+
+namespace OASystem.API.OAMethodLib.Quartz.Jobs
+{
+    /// <summary>
+    /// 每周五下午4点执行的定时任务
+    /// </summary>
+    public class WeeklyFridayJob : IJob
+    {
+        private readonly ILogger<WeeklyFridayJob> _logger;
+        private readonly SqlSugarClient _sqlSugar;
+        private readonly IQiYeWeChatApiService _qiYeWeChatApiService;
+        private readonly HttpClient _httpClient;
+
+        public WeeklyFridayJob(ILogger<WeeklyFridayJob> logger, SqlSugarClient sqlSugar
+        , IHttpClientFactory httpClientFactory, IQiYeWeChatApiService qiYeWeChatApiService)
+        {
+            _logger = logger;
+            _sqlSugar = sqlSugar;
+            _httpClient = httpClientFactory.CreateClient();
+            _qiYeWeChatApiService = qiYeWeChatApiService;
+        }
+
+        public async Task Execute(IJobExecutionContext context)
+        {
+            var jobName = context.JobDetail.Key.Name;
+            _logger.LogInformation($"开始执行任务 {jobName},时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}");
+
+            try
+            {
+                var startDate = DateTime.Today.AddDays(-4);
+                var endDate = DateTime.Today.AddDays(1);
+                string baseUrl = "http://localhost:5256";
+                var createUserId = 4;
+
+                var sqSetting = _sqlSugar.Queryable<Sys_SetData>().First(x => x.Id == 1417);
+                if (sqSetting != null)
+                {
+                    Dictionary<int, List<int>> result = JsonConvert.DeserializeObject<Dictionary<int, List<int>>>(sqSetting.Remark);
+
+                    result = new Dictionary<int, List<int>> { { 235, new List<int> { 258 } } };
+
+                    foreach (var item in result.Keys)
+                    {
+                        var keyUser = await _sqlSugar.Queryable<Sys_Users>()
+                        .FirstAsync(x => x.Id == item);
+                        var users = await _sqlSugar.Queryable<Sys_Users>()
+                            .LeftJoin<Sys_Department>((u, d) => u.DepId == d.Id)
+                            .LeftJoin<Sys_JobPost>((u, d, jp) => u.JobPostId == jp.Id)
+                            .Where((u, d, jp) => u.IsDel == 0 && result[item].Contains(u.Id) && u.Id != item)
+                            .Select((u, d, jp) => new { u.Id, u.CnName, DepName = d.DepName ?? "", JobName = jp.JobName ?? "" })
+                            .ToListAsync();
+
+                        foreach (var user in users)
+                        {
+                            // 构建API URL
+                            string apiUrl = $"{baseUrl}/api/PersonnelModule/AiPerformanceAnalysis_AllDepartment";
+
+                            // 构建查询参数
+                            var queryParams = new Dictionary<string, string>
+                            {
+                                { "userId", user.Id.ToString() },
+                                { "start", startDate.ToString("yyyy-MM-dd HH:mm:ss") },
+                                { "end", endDate.ToString("yyyy-MM-dd HH:mm:ss") },
+                                { "createUserId", createUserId.ToString() },
+                                { "isMonthData", "true" }
+                            };
+
+                            // 构建完整URL
+                            var queryString = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
+                            string fullUrl = $"{apiUrl}?{queryString}";
+
+                            _logger.LogInformation($"正在为用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 生成绩效数据...");
+                            _logger.LogInformation($"API URL: {fullUrl}");
+
+                            // 发送HTTP GET请求
+                            var response = await _httpClient.GetAsync(fullUrl);
+                            var responseContent = await response.Content.ReadAsStringAsync();
+
+                            if (response.IsSuccessStatusCode)
+                            {
+                                // 尝试解析响应结果
+                                try
+                                {
+                                    var resultJson = JsonConvert.DeserializeObject<JsonView>(responseContent);
+                                    if (resultJson != null && resultJson.Code == 200)
+                                    {
+                                        _logger.LogInformation($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 绩效数据生成成功");
+                                        //生成PDF
+                                        var url = $"{baseUrl}/api/PersonnelModule/AiPerformanceAnalysis_JobMarketingFileDown";
+                                        //构建PDF查询参数
+                                        var queryParamsPdf = new Dictionary<string, string>
+                                        {
+                                            { "userId", user.Id.ToString() },
+                                            { "start", startDate.ToString("yyyy-MM-dd") },
+                                            { "end", endDate.ToString("yyyy-MM-dd") },
+                                        };
+
+                                        var queryStringPdf = string.Join("&", queryParamsPdf.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"));
+                                        url = $"{url}?{queryStringPdf}";
+
+                                        _logger.LogInformation($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 开始生成PDF:{url}");
+
+                                        var pdfUrl = await _httpClient.PostAsync(url, new StringContent(queryStringPdf));
+
+                                        if (pdfUrl.IsSuccessStatusCode)
+                                        {
+                                            _logger.LogInformation($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 开始生成PDF:{pdfUrl.StatusCode}");
+                                            var pdfContent = await pdfUrl.Content.ReadAsStringAsync();
+                                            _logger.LogInformation($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 绩效数据生成PDF成功:{pdfContent}");
+                                            var pdfContentJson = JObject.Parse(pdfContent);
+                                            var pdfUrlString = pdfContentJson["data"]["url"].ToString();
+                                            var pdfUrlSeed = new Uri(pdfUrlString);
+                                            var pdfBytes = await _httpClient.GetByteArrayAsync(pdfUrlSeed);
+                                            var pdfFile = new FormFile(new MemoryStream(pdfBytes), 0, pdfBytes.Length, "pdf", "pdf");
+
+
+                                            await _qiYeWeChatApiService.EmailSendAsync(new EmailRequestDto
+                                            {
+                                                ToEmails = new List<string> { keyUser.Email },
+                                                Subject = $"用户 {user.CnName}(部门:{user.DepName} 日期区间:{startDate.ToString("yyyy-MM-dd")} 至 {endDate.ToString("yyyy-MM-dd")}) 绩效数据",
+                                                Body = "员工绩效推送,请查看附件。",
+                                                Files = new[] { pdfFile }
+                                            });
+                                        }
+                                        else
+                                        {
+
+                                            _logger.LogWarning($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 生成PDF失败:{pdfUrl.StatusCode}");
+                                            _logger.LogWarning($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 生成PDF失败:{pdfUrl.Content.ReadAsStringAsync()}");
+                                        }
+                                    }
+                                    else
+                                    {
+                                        _logger.LogWarning($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 绩效数据生成失败:{resultJson?.Msg ?? "未知错误"}");
+                                    }
+                                }
+                                catch (Exception ex)
+                                {
+                                    _logger.LogError(ex, $"解析用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 的API响应失败");
+                                }
+                            }
+                            else
+                            {
+                                _logger.LogError($"用户 {user.CnName}(ID:{user.Id}, 部门:{user.DepName}) 绩效数据生成API调用失败,状态码: {response.StatusCode}, 响应内容: {responseContent}");
+                            }
+
+                            // 添加延迟,避免请求过于频繁
+                            await Task.Delay(3000); // 延迟3秒
+                        }
+                    }
+                }
+
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, $"任务 {jobName} 执行失败");
+            }
+        }
+    }
+}

+ 15 - 0
OASystem/OASystem.Api/OAMethodLib/Quartz/QuartzFactory.cs

@@ -68,6 +68,9 @@ namespace QuzrtzJob.Factory
             // 团组流程提示、预警 企业微信消息通知 每天早上九点触发
             await CreateAndScheduleJob<GroupProcessNodeJob>("job6", "group", CreateTrigger("0 0 9 * * ?"));
 
+            // 每周五下午4点执行
+            await CreateAndScheduleJob<WeeklyFridayJob>("job7", "group", CreateTrigger("0 0 16 ? * FRI"));
+
             return await Task.FromResult("将触发器和任务器绑定到调度器中完成");
         }
 
@@ -85,5 +88,17 @@ namespace QuzrtzJob.Factory
               .WithCronSchedule(cronExpression)
               .Build();
         }
+
+        /// <summary>
+        /// 立即触发指定任务(用于手动执行)
+        /// </summary>
+        /// <param name="jobName">任务名称,如 job7</param>
+        /// <param name="groupName">分组名称,默认 group</param>
+        public async Task TriggerJobNow(string jobName, string groupName = "group")
+        {
+            if (_scheduler == null) return;
+            var jobKey = new JobKey(jobName, groupName);
+            await _scheduler.TriggerJob(jobKey);
+        }
     }
 }

+ 1 - 0
OASystem/OASystem.Api/Program.cs

@@ -569,6 +569,7 @@ builder.Services.AddSingleton<TaskJob>();
 builder.Services.AddSingleton<TaskNewsFeedJob>();
 builder.Services.AddSingleton<PerformanceJob>();
 builder.Services.AddSingleton<GroupProcessNodeJob>();
+builder.Services.AddSingleton<WeeklyFridayJob>();
 //# new business
 builder.Services.AddControllersWithViews();
 builder.Services.AddSingleton<IAPNsService, APNsService>();

+ 68 - 2
OASystem/OASystem.Domain/ViewModels/QiYeWeChat/JournalDataView.cs

@@ -1,5 +1,7 @@
 using OASystem.Domain.ViewModels.QiYeWeChat;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
 
 namespace OASystem.Domain.ViewModels.QiYeWeChat
 {
@@ -45,6 +47,11 @@ namespace OASystem.Domain.ViewModels.QiYeWeChat
         /// </summary>
         public string journal_uuid { get; set; }
 
+        /// <summary>
+        /// 汇报模板名称
+        /// </summary>
+        public string template_name { get; set; }
+
         /// <summary>
         /// 汇报模板ID
         /// </summary>
@@ -56,8 +63,9 @@ namespace OASystem.Domain.ViewModels.QiYeWeChat
         public long report_time { get; set; }
 
         /// <summary>
-        /// 汇报提交者
+        /// 汇报提交者(API 返回字段名为 submitter)
         /// </summary>
+        [JsonPropertyName("submitter")]
         public JournalCreator creator { get; set; }
 
         /// <summary>
@@ -119,21 +127,49 @@ namespace OASystem.Domain.ViewModels.QiYeWeChat
         public string new_money { get; set; }
         public JournalDate date { get; set; }
         public JournalSelector selector { get; set; }
+        public List<object> tips { get; set; }
         public List<JournalMember> members { get; set; }
         public List<JournalDepartment> departments { get; set; }
         public List<JournalFile> files { get; set; }
+        /// <summary>
+        /// 子控件列表,如 Table 的每一行,结构为 [{ "list": [ JournalContentItem, ... ] }]
+        /// </summary>
+        public List<JournalChild> children { get; set; }
+        public List<object> stat_field { get; set; }
+        public List<object> sum_field { get; set; }
+        public List<object> related_approval { get; set; }
+        public List<object> students { get; set; }
+        public List<object> classes { get; set; }
+        public List<object> docs { get; set; }
+        public List<object> wedrive_files { get; set; }
+    }
+
+    /// <summary>
+    /// 汇报子项容器,如 Table 控件的一行,包含 list 数组
+    /// </summary>
+    public class JournalChild
+    {
+        public List<JournalContentItem> list { get; set; }
     }
 
     public class JournalDate
     {
         public string type { get; set; }
-        public long s_timestamp { get; set; }
+        /// <summary>
+        /// 时间戳,企业微信API可能返回字符串或数字
+        /// </summary>
+        public string s_timestamp { get; set; }
+        /// <summary>
+        /// 解析后的时间戳(秒),解析失败时为 null
+        /// </summary>
+        public long? s_timestamp_long => long.TryParse(s_timestamp, out var v) ? v : null;
     }
 
     public class JournalSelector
     {
         public string type { get; set; }
         public List<JournalSelectorOption> options { get; set; }
+        public List<object> op_relations { get; set; }
     }
 
     public class JournalSelectorOption
@@ -167,4 +203,34 @@ namespace OASystem.Domain.ViewModels.QiYeWeChat
         public string commentid { get; set; }
         public List<string> media_id { get; set; }
     }
+
+    /// <summary>
+    /// 汇报详情字段提取辅助
+    /// </summary>
+    public static class JournalFieldHelper
+    {
+        /// <summary>
+        /// 从「客户单位信息」表格中获取「单位全称」
+        /// </summary>
+        public static string GetUnitName(this JournalRecordDetail detail) =>
+            GetFieldValue(detail, "客户单位信息", "单位全称");
+
+        /// <summary>
+        /// 从指定表格中按标题获取字段值(支持 Table 控件,取第一行)
+        /// </summary>
+        /// <param name="detail">汇报详情</param>
+        /// <param name="tableTitle">表格标题,如「客户单位信息」</param>
+        /// <param name="fieldTitle">字段标题,如「单位全称」「单位类型」</param>
+        public static string GetFieldValue(this JournalRecordDetail detail, string tableTitle, string fieldTitle)
+        {
+            var table = detail?.apply_data?.contents
+                ?.FirstOrDefault(c => c.control == "Table" && GetTitleText(c) == tableTitle);
+            var firstRow = table?.value?.children?.FirstOrDefault()?.list;
+            var field = firstRow?.FirstOrDefault(c => GetTitleText(c) == fieldTitle);
+            return field?.value?.text;
+        }
+
+        private static string GetTitleText(JournalContentItem item) =>
+            item?.title?.FirstOrDefault()?.text;
+    }
 }