using Azure.Cosmos; using Azure.Storage.Blobs.Models; using DinkToPdf; using DinkToPdf.Contracts; using HTEXLib.COMM.Helpers; using Microsoft.Azure.Cosmos.Table; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using StackExchange.Redis; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Web; using TEAMModelOS.SDK; using TEAMModelOS.SDK.DI; using TEAMModelOS.SDK.Extension; using TEAMModelOS.SDK.Models; using TEAMModelOS.SDK.Models.Cosmos.Teacher; using TEAMModelOS.SDK.Models.Service; using TEAMModelOS.SDK.Models.Service.BI; using TEAMModelOS.SDK.Models.Table; using static TEAMModelOS.SDK.Models.Teacher; namespace TEAMModelOS.FunctionV4.TimeTrigger { public class IESTimerTrigger { /// /// 文档。https://docs.microsoft.com/zh-cn/azure/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp /// Timer 在线测试 https://ncrontab.swimburger.net/ /// private readonly AzureCosmosFactory _azureCosmos; private readonly DingDing _dingDing; private readonly AzureStorageFactory _azureStorage; private readonly AzureRedisFactory _azureRedis; private readonly IConverter _converter; private readonly SnowflakeId _snowflakeId; private readonly IHttpClientFactory _httpClient; private IPSearcher _ipSearcher; public IESTimerTrigger(IPSearcher ipSearcher, IHttpClientFactory httpClient,SnowflakeId snowflakeId,IConverter converter, AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis) { _azureCosmos = azureCosmos; _dingDing = dingDing; _azureStorage = azureStorage; _azureRedis = azureRedis; _converter = converter; _snowflakeId=snowflakeId; _httpClient = httpClient; _ipSearcher = ipSearcher; } /// /// 防火墙日志记录文件 /// /// /// /// // [Function("FireWallFileLog")] //https://docs.azure.cn/zh-cn/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp //0 1 * * * * 一天中每小时的第 1 分钟 //0 */10 * * * * 每五分钟一次 public async Task FireWallFileLog([TimerTrigger("0 1 * * * *")] TimerInfo myTimer, ILogger log) { try { string location = Environment.GetEnvironmentVariable("Option:Location"); var datetime = DateTimeOffset.UtcNow.AddHours(-1); var y = datetime.Year; var m = datetime.Month >= 10 ? $"{datetime.Month}" : $"0{datetime.Month}"; var d = datetime.Day >= 10 ? $"{datetime.Day}" : $"0{datetime.Day}"; var h = datetime.Hour >= 10 ? $"{datetime.Hour}" : $"0{datetime.Hour}"; #if DEBUG if (location.Equals("China-Dep")) #else if (location.Equals("China")) #endif { string path = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={y}/m={m}/d={d}/h={h}/m=00/PT1H.json"; var retn = await BILogAnalyseService.GetPathAnalyse(_azureStorage,_ipSearcher,_dingDing, path, "LogStorage"); if (retn.recCnts.IsNotEmpty()) { //https://teammodelos.blob.core.chinacloudapi.cn/0-public/pie-borderRadius.html string publishUrl = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={HttpUtility.UrlEncode(retn.saveUrls.First(),Encoding.UTF8)}&time={HttpUtility.UrlEncode(datetime.AddHours(8).ToString("yyyy年MM月dd日 HH时"),Encoding.UTF8)}"; string ulrs = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={retn.saveUrls.First()}&time={datetime.AddHours(8).ToString("yyyy年MM月dd日 HH时")}"; string ulr = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(ulrs,Encoding.UTF8)}&delay=5000"; string image = ""; try { string strs = await _httpClient.CreateClient().GetStringAsync(ulr); if (!string.IsNullOrWhiteSpace(strs)) { JsonElement json = strs.ToObject(); json.TryGetProperty("url", out JsonElement base64); using (MemoryStream ms = new MemoryStream(Convert.FromBase64String($"{base64}"))) { image = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer( ms, $"visitCnt/{y}{m}{d}", $"{y}{m}{d}{h}.png",false); } } } catch (Exception ex) { } await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(小时)\n> 记录时间:{datetime.AddHours(8).ToString("yyyy-MM-dd HH")}\n> ![screenshot]({image})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" + $" [发布地址]({publishUrl}) \n", GroupNames.醍摩豆服務運維群組); } //处理昨天的防火墙日志 if (h.Equals("00")) { var pastTime = datetime.AddHours(-1); var ptY = pastTime.Year; var ptM = pastTime.Month >= 10 ? $"{pastTime.Month}" : $"0{pastTime.Month}"; var ptD = pastTime.Day >= 10 ? $"{pastTime.Day}" : $"0{pastTime.Day}"; string dayPath = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={ptY}/m={ptM}/d={ptD}"; var retnDay = await BILogAnalyseService.GetPathAnalyse(_azureStorage, _ipSearcher, _dingDing, dayPath, "LogStorage",timeType:"Day"); if (retn.recCnts.IsNotEmpty()) { //一天的统计 string dayPublishUrl = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={HttpUtility.UrlEncode(retnDay.saveUrls.First(), Encoding.UTF8)}&time={HttpUtility.UrlEncode(pastTime.ToString("yyyy年MM月dd日"), Encoding.UTF8)}"; string dayUrls = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={retnDay.saveUrls.First()}&time={pastTime.ToString("yyyy年MM月dd日")}"; string dayUrl = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(dayUrls, Encoding.UTF8)}&delay=6000"; string dayImage = ""; try { string dayStr = await _httpClient.CreateClient().GetStringAsync(dayUrl); if (!string.IsNullOrWhiteSpace(dayStr)) { JsonElement dayJson = dayStr.ToObject(); dayJson.TryGetProperty("url", out JsonElement dayBase64); using (MemoryStream dayMs = new(Convert.FromBase64String($"{dayBase64}"))) { dayImage = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(dayMs, $"visitCnt/{ptY}{ptM}{ptD}", "days.png", false); } } } catch (Exception ex) { } await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(天)\n> 记录时间:{pastTime.AddHours(8).ToString("yyyy-MM-dd")}\n> ![screenshot]({dayImage})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" + $" [发布地址]({dayPublishUrl}) \n", GroupNames.醍摩豆服務運維群組); } } } else if (location.Contains("Global")) { } } catch (Exception ex) { // await _dingDing.SendBotMsg($"FireWallFileLog 防火墙日志记录: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組); } } /// /// 每天执行 一次清零动作 /// /// /// /// [Function("BIStatsDayDefault")] //https://docs.azure.cn/zh-cn/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp //0 1 0 * * * 一天中00的第 1 分钟 //0 1 * * * * 一天中每小时的第 1 分钟 //0 */10 * * * * 每五分钟一次 public async Task BIStatsDayDefault([TimerTrigger("0 1 0 * * *")] TimerInfo myTimer, ILogger log) { try { _ = BIStats.SetStatsZeroPoint(_azureCosmos, _dingDing); } catch (Exception ex) { await _dingDing.SendBotMsg($"BIStatsDayDefault 定时清理每天的数据: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組); } } /// /// 清理HiTeach教研类型的课例文件上传在Blob空间的文件。 0 */10 22-23 * * * /// 该代码执行之后 需要删除或者取消Timer任务。 /// /// /// /// //[Function("CleanUnusedLessonRecord")] //5分钟一次 public async Task CleanUnusedLessonRecord( //[TimerTrigger("0 */10 15-23 * * *")] TimerInfo myTimer, ILogger log ) { string location = Environment.GetEnvironmentVariable("Option:Location"); try { if (location.Equals("China") || location.Equals("Global")) { bool lockKey = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"LessonRecord:Unused:Lock:{location}"); bool keyLock = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"LessonRecord:Unused:Lock:Lock"); if (keyLock) { return; } //key不存在的时候 。 if (!lockKey) { var schoolKeys = new List(); var teacherKeys = new List(); string sqlT = "select c.id,c.name, 'Teacher' as code from c where c.code='Base'"; string sqlS = "select c.id,c.name, 'School' as code from c where c.code='Base'"; await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher). GetItemQueryIterator(queryText: sqlT, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Base") })) { teacherKeys.Add(item); } await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School). GetItemQueryIterator(queryText: sqlS, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Base") })) { schoolKeys.Add(item); } string sqlcount = "select value count(1) from c where c.pk='LessonRecord'"; foreach (var key in schoolKeys) { await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School). GetItemQueryIterator(queryText: sqlcount, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"LessonRecord-{key.id}") })) { key.count = item; break; } } foreach (var key in teacherKeys) { await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher). GetItemQueryIterator(queryText: $"select value count(1) from c where c.pk='LessonRecord' and c.tmdid='{key.id}'", requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"LessonRecord") })) { key.count = item; break; } } schoolKeys.AddRange(teacherKeys); //以下代码作用,用于根据当前拥有的课例数判断区分学校和个人使用课例频率越多的, //并均分每次需要处理的学校和个人容器个数,避免每次处理批次花费时间相差太大。 //将每个批次处理数量级控制在500-1000左右。 List> counts = new List>(); //501-∞ var count501_ = schoolKeys.Where(x => x.count >= 501); //至少500条 counts.AddRange(count501_.Page(1));//1个学校或个人 //201-500 var count201_500 = schoolKeys.Where(x => x.count >= 201 && x.count <= 500); //至少603条-1500条 counts.AddRange(count201_500.Page(3));//2个学校或个人 //100-200 var count100_200 = schoolKeys.Where(x => x.count >= 100 && x.count <= 200);//至少500条-1000条 counts.AddRange(count100_200.Page(10));//5个学校或个人 //51-99 var count51_99 = schoolKeys.Where(x => x.count >= 51 && x.count <= 99);//至少500条-990条 counts.AddRange(count51_99.Page(10));//10个学校或个人 //21-50 var count21_50 = schoolKeys.Where(x => x.count >= 21 && x.count <= 50);//至少410条-1000条 counts.AddRange(count21_50.Page(20));//20个学校或个人 //10-20 var count10_20 = schoolKeys.Where(x => x.count >= 10 && x.count <= 20);//至少500条-1000条 counts.AddRange(count10_20.Page(50));//50个学校或个人 //5-9 var count0_9 = schoolKeys.Where(x => x.count >= 5 && x.count <= 9);//至少500条-900条 counts.AddRange(count0_9.Page(100));//100个学校或个人 //2-4 var count2_4 = schoolKeys.Where(x => x.count >= 2 && x.count <= 4);//至少600条-1200条 counts.AddRange(count2_4.Page(300)); //0-1,已经处理过一次,不用再处理 var count0_1 = schoolKeys.Where(x => x.count ==1);//至少500,就算没有课例也算一次。 counts.AddRange(count0_1.Page(1000)); int field = 1; foreach (var item in counts) { if (item.Any()) { bool stuallstatus = await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", field, new UnusedLock { field = field, status = 0, item = item }.ToJsonString()); field += 1; } } await _dingDing.SendBotMsg($"{location},获取到:{schoolKeys.Count} 个学校和个人的容器\n" + $"大概要处理:{schoolKeys.Sum(x => x.count) + schoolKeys.Where(x => x.count >= 0).Count()} 条数据\n" + $"将数据分为:{field - 1} 次处理,每次处理500-1000条数据。", GroupNames.醍摩豆服務運維群組); } //key存在的时候 开始进行数据处理 var records = await _azureRedis.GetRedisClient(8).HashGetAllAsync($"LessonRecord:Unused:Lock:{location}"); List unuseds = new List(); foreach (var rcd in records) { var value = rcd.Value.ToString().ToObject(); unuseds.Add(value); } int max = unuseds.Max(x => x.field); int index = max + 1; var unusedLock = unuseds.Where(s => s.status == 0)?.OrderBy(x => x.field)?.First(); if (unusedLock != null) { var stime = DateTimeOffset.UtcNow; long s = stime.ToUnixTimeMilliseconds(); string sdata = stime.ToString("yyyy-MM-dd HH:mm:ss"); await _dingDing.SendBotMsg($"{location}-开始第{unusedLock.field}轮清理冗余的课例记录文件...\n开始时间:{sdata}\n清理容器有{unusedLock.item.Select(x => $"{x.name}-{x.id}").ToJsonString()}", GroupNames.醍摩豆服務運維群組); //将当前的标记为1锁定。 bool stuallstatus = await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", unusedLock.field, new UnusedLock { field = unusedLock.field, status = 1, item = unusedLock.item }.ToJsonString()); List>> deleteUrls = new List>>(); foreach (IdCodeCount idCode in unusedLock.item) { try { List urls = new List(); var ContainerClient = _azureStorage.GetBlobContainerClient(idCode.id); List items = await ContainerClient.List("records"); if (items.IsNotEmpty()) { HashSet set = new HashSet(); items.ForEach(z => { var uri = z.Split("/"); if (uri.Length > 1) { set.Add(uri[1]); } }); if (set.Any()) { string tbname = idCode.code.Equals("Teacher") ? Constant.Teacher : Constant.School; string pk = idCode.code.Equals("Teacher") ? "LessonRecord" : $"LessonRecord-{idCode.id}"; string sql = $"select value c.id from c where c.pk='LessonRecord' and c.id in ({string.Join(",", set.Select(m => $"'{m}'"))})"; List ids = new List(); await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, tbname) .GetItemQueryIterator(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(pk) })) { ids.Add(item); } var notin = set.Except(ids); if (notin.Any()) { foreach (var not in notin) { if (!string.IsNullOrWhiteSpace(not)) { string url = $"records/{not}"; long id = -1; long.TryParse(not, out id); if (id > -1) { id = _snowflakeId.ParseIdToTimeStamp(id); } if (id > -1) { //72小时之外的数据。需要被删除 72 * 60 * 60 *1000= 259200000 //当前时间-课例产生的时间戳 if (s - id > 259200000) { urls.Add(url); await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, idCode.id, new List { url }); } } } } } } } if (urls.IsNotEmpty()) { deleteUrls.Add(new KeyValuePair>($"{idCode.name}-{idCode.id}", urls)); } } catch (Exception ex) { //异常的学校,需要将数据放回redis 重新处理。 await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", index, new UnusedLock { field = index, status = 0, item = new List() { idCode } }.ToJsonString()); index += 1; await _dingDing.SendBotMsg($"{location}-第{unusedLock.field}轮清理时容器:{idCode.id},类型:{idCode.code}出现异常。\n将{idCode.id}重新加入队列,队列编号:{index}\n" + $"异常信息:{ex.Message},{ex.StackTrace}", GroupNames.醍摩豆服務運維群組); } } await _azureRedis.GetRedisClient(8).HashDeleteAsync($"LessonRecord:Unused:Lock:{location}", unusedLock.field); var etime = DateTimeOffset.UtcNow; long e = etime.ToUnixTimeMilliseconds(); string edata = etime.ToString("yyyy-MM-dd HH:mm:ss"); long d = (e - s) / 1000; string timeStr = d >= 60 ? Math.Round(d / 60.0) + "分" : d + "秒"; List content = new List(); foreach (var url in deleteUrls) { if (url.Value.IsNotEmpty()) { content.Add($"{url.Key}\n {string.Join(" \t\n", url.Value)}"); } } if (max == unusedLock.field) { //锁定15至少小时。 await _azureRedis.GetRedisClient(8).StringSetAsync($"LessonRecord:Unused:Lock:Lock", "用于在清理完所有Blob容器后,锁定不再进行多次清理。"); await _azureRedis.GetRedisClient(8).KeyExpireAsync($"LessonRecord:Unused:Lock:Lock", DateTime.UtcNow.AddHours(15) ); } if (content.IsNotEmpty()) { string str = string.Join("\n", content); await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:\n{str}", GroupNames.醍摩豆服務運維群組); } else { // await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:暂无", GroupNames.醍摩豆服務運維群組); } } } } catch (Exception ex) { await _dingDing.SendBotMsg($"{location} 清理时容器出现异常。\n异常信息:{ex.Message},{ex.StackTrace}", GroupNames.醍摩豆服務運維群組); } } /// /// 每天執行 取得IOT TeachingData 並統計昨日每校Redis資料 執行時間:每日凌晨1時1分 /// [Function("BICrtDailyAnal")] //0 1 0 * * * 一天中00的第 1 分钟 //0 1 * * * * 一天中每小时的第 1 分钟 //0 */10 * * * * 每五分钟一次 public async Task BICreatDailyAnalData([TimerTrigger("0 1 1 * * *")] TimerInfo myTimer, ILogger log) { var _azureCosmosClient = _azureCosmos.GetCosmosClient(); var _azureCosmosClientCsv2CnRead = _azureCosmos.GetCosmosClient(name: "CoreServiceV2CnRead"); var datetime = DateTimeOffset.UtcNow.AddHours(-12); //統計昨天的數據 var y = $"{datetime.Year}"; var m = datetime.Month >= 10 ? $"{datetime.Month}" : $"0{datetime.Month}"; var d = datetime.Day >= 10 ? $"{datetime.Day}" : $"0{datetime.Day}"; //生成學校IOT數據 await BIProdAnalysis.BICreatDailyAnalData(_azureRedis, _azureCosmosClient, _azureCosmosClientCsv2CnRead, _dingDing, y, m, d); //刪除三個月以前的Redis數據 [待做] } /// /// 每天執行 計算各學生各科錯題庫的數量,記入Redis 執行時間:每日2時1分 /// 「新增錯題數」取得方法改由智慧錯題的時間戳記判斷,每日統計錯題數已不需要 /// //[Function("CntStuErrorItems")] //public async Task CntStuErrorItems([TimerTrigger("0 1 2 * * *")] TimerInfo myTimer, ILogger log) //{ // var _azureCosmosClient = _azureCosmos.GetCosmosClient(); // await ErrorItemsService.cntStuErrorItemsAsync(_azureRedis, _azureCosmosClient, _dingDing); //} public class UnusedLock { public int field { get; set; } /// /// 0默认未被执行,1 正在执行中 /// public int status { get; set; } public IEnumerable item { get; set; } = new List(); } } }