IESTimerTrigger.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. using Azure.Cosmos;
  2. using Azure.Storage.Blobs.Models;
  3. using DinkToPdf;
  4. using DinkToPdf.Contracts;
  5. using HTEXLib.COMM.Helpers;
  6. using Microsoft.Azure.Cosmos.Table;
  7. using Microsoft.Azure.Functions.Worker;
  8. using Microsoft.Azure.Functions.Worker.Http;
  9. using Microsoft.Extensions.Logging;
  10. using StackExchange.Redis;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Net;
  16. using System.Net.Http;
  17. using System.Reflection;
  18. using System.Text;
  19. using System.Text.Json;
  20. using System.Threading.Tasks;
  21. using System.Web;
  22. using TEAMModelOS.SDK;
  23. using TEAMModelOS.SDK.DI;
  24. using TEAMModelOS.SDK.Extension;
  25. using TEAMModelOS.SDK.Models;
  26. using TEAMModelOS.SDK.Models.Cosmos.Teacher;
  27. using TEAMModelOS.SDK.Models.Service.BI;
  28. using TEAMModelOS.SDK.Models.Table;
  29. using static TEAMModelOS.SDK.Models.Teacher;
  30. namespace TEAMModelOS.FunctionV4.TimeTrigger
  31. {
  32. public class IESTimerTrigger
  33. {
  34. /// <summary>
  35. /// 文档。https://docs.microsoft.com/zh-cn/azure/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp
  36. /// Timer 在线测试 https://ncrontab.swimburger.net/
  37. /// </summary>
  38. private readonly AzureCosmosFactory _azureCosmos;
  39. private readonly DingDing _dingDing;
  40. private readonly AzureStorageFactory _azureStorage;
  41. private readonly AzureRedisFactory _azureRedis;
  42. private readonly IConverter _converter;
  43. private readonly SnowflakeId _snowflakeId;
  44. private readonly HttpClient _httpClient;
  45. private IPSearcher _ipSearcher;
  46. public IESTimerTrigger(IPSearcher ipSearcher,HttpClient httpClient,SnowflakeId snowflakeId,IConverter converter, AzureCosmosFactory azureCosmos, DingDing dingDing, AzureStorageFactory azureStorage, AzureRedisFactory azureRedis)
  47. {
  48. _azureCosmos = azureCosmos;
  49. _dingDing = dingDing;
  50. _azureStorage = azureStorage;
  51. _azureRedis = azureRedis;
  52. _converter = converter;
  53. _snowflakeId=snowflakeId;
  54. _httpClient = httpClient;
  55. _ipSearcher = ipSearcher;
  56. }
  57. /// <summary>
  58. /// 防火墙日志记录文件
  59. /// </summary>
  60. /// <param name="req"></param>
  61. /// <param name="log"></param>
  62. /// <returns></returns>
  63. [Function("FireWallFileLog")]
  64. //https://docs.azure.cn/zh-cn/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-csharp
  65. //0 1 * * * * 一天中每小时的第 1 分钟
  66. //0 */10 * * * * 每五分钟一次
  67. public async Task FireWallFileLog([TimerTrigger("0 1 * * * *")] TimerInfo myTimer, ILogger log)
  68. {
  69. try
  70. {
  71. string location = Environment.GetEnvironmentVariable("Option:Location");
  72. var datetime = DateTimeOffset.UtcNow.AddHours(-1);
  73. var y = datetime.Year;
  74. var m = datetime.Month >= 10 ? $"{datetime.Month}" : $"0{datetime.Month}";
  75. var d = datetime.Day >= 10 ? $"{datetime.Day}" : $"0{datetime.Day}";
  76. var h = datetime.Hour >= 10 ? $"{datetime.Hour}" : $"0{datetime.Hour}";
  77. #if DEBUG
  78. if (location.Equals("China-Dep"))
  79. #else
  80. if (location.Equals("China"))
  81. #endif
  82. {
  83. 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";
  84. var retn = await BILogAnalyseService.GetPathAnalyse(_azureStorage,_ipSearcher,_dingDing, path, "LogStorage");
  85. if (retn.recCnts.IsNotEmpty())
  86. {
  87. //https://teammodelos.blob.core.chinacloudapi.cn/0-public/pie-borderRadius.html
  88. 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)}";
  89. 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时")}";
  90. string ulr = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(ulrs,Encoding.UTF8)}&delay=5000";
  91. string image = "";
  92. try
  93. {
  94. string strs = await _httpClient.GetStringAsync(ulr);
  95. if (!string.IsNullOrWhiteSpace(strs)) {
  96. JsonElement json = strs.ToObject<JsonElement>();
  97. json.TryGetProperty("url", out JsonElement base64);
  98. using (MemoryStream ms = new MemoryStream(Convert.FromBase64String($"{base64}"))) {
  99. image = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer( ms, $"visitCnt/{y}{m}{d}", $"{y}{m}{d}{h}.png",false);
  100. }
  101. }
  102. }
  103. catch (Exception ex) {
  104. }
  105. 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")}" +
  106. $" [发布地址]({publishUrl}) \n", GroupNames.醍摩豆服務運維群組);
  107. }
  108. //处理昨天的防火墙日志
  109. if (h.Equals("00"))
  110. {
  111. var pastTime = datetime.AddHours(-1);
  112. var ptY = pastTime.Year;
  113. var ptM = pastTime.Month >= 10 ? $"{pastTime.Month}" : $"0{pastTime.Month}";
  114. var ptD = pastTime.Day >= 10 ? $"{pastTime.Day}" : $"0{pastTime.Day}";
  115. string dayPath = $"resourceId=/SUBSCRIPTIONS/73B7F9EF-D8B7-4444-9E8D-D80B43BF3CD4/RESOURCEGROUPS/TEAMMODELCHENGDU/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/OSFIREWARE/y={ptY}/m={ptM}/d={ptD}";
  116. var retnDay = await BILogAnalyseService.GetPathAnalyse(_azureStorage, _ipSearcher, _dingDing, dayPath, "LogStorage",timeType:"Day");
  117. if (retn.recCnts.IsNotEmpty())
  118. {
  119. //一天的统计
  120. 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)}";
  121. string dayUrls = $"https://teammodelos.blob.core.chinacloudapi.cn/0-public/api-count.html?url={retnDay.saveUrls.First()}&time={pastTime.ToString("yyyy年MM月dd日")}";
  122. string dayUrl = $"http://cdhabook.teammodel.cn:8805/screen/screenshot-png?width=1920&height=1450&url={HttpUtility.UrlEncode(dayUrls, Encoding.UTF8)}&delay=6000";
  123. string dayImage = "";
  124. try
  125. {
  126. string dayStr = await _httpClient.GetStringAsync(dayUrl);
  127. if (!string.IsNullOrWhiteSpace(dayStr))
  128. {
  129. JsonElement dayJson = dayStr.ToObject<JsonElement>();
  130. dayJson.TryGetProperty("url", out JsonElement dayBase64);
  131. using (MemoryStream dayMs = new(Convert.FromBase64String($"{dayBase64}")))
  132. {
  133. dayImage = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(dayMs, $"visitCnt/{ptY}{ptM}{ptD}", "days.png", false);
  134. }
  135. }
  136. }
  137. catch (Exception ex) { }
  138. await _dingDing.SendBotMarkdown("防火墙日志记录", $"#### 防火墙日志记录(天)\n> 记录时间:{pastTime.AddHours(8).ToString("yyyy-MM-dd")}\n> ![screenshot]({dayImage})\n> ###### 发布时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" +
  139. $" [发布地址]({dayPublishUrl}) \n", GroupNames.醍摩豆服務運維群組);
  140. }
  141. }
  142. }
  143. else if (location.Contains("Global"))
  144. {
  145. }
  146. }
  147. catch (Exception ex)
  148. {
  149. // await _dingDing.SendBotMsg($"FireWallFileLog 防火墙日志记录: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}\n{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
  150. }
  151. }
  152. /// <summary>
  153. /// 清理HiTeach教研类型的课例文件上传在Blob空间的文件。 0 */10 22-23 * * *
  154. /// 该代码执行之后 需要删除或者取消Timer任务。
  155. /// </summary>
  156. /// <param name="myTimer"></param>
  157. /// <param name="log"></param>
  158. /// <returns></returns>
  159. [Function("CleanUnusedLessonRecord")]
  160. //5分钟一次
  161. public async Task CleanUnusedLessonRecord([TimerTrigger("0 */10 15-23 * * *")] TimerInfo myTimer, ILogger log) {
  162. string location = Environment.GetEnvironmentVariable("Option:Location");
  163. try {
  164. if (location.Equals("China") || location.Equals("Global"))
  165. {
  166. bool lockKey = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"LessonRecord:Unused:Lock:{location}");
  167. bool keyLock = await _azureRedis.GetRedisClient(8).KeyExistsAsync($"LessonRecord:Unused:Lock:Lock");
  168. if (keyLock)
  169. {
  170. return;
  171. }
  172. //key不存在的时候 。
  173. if (!lockKey)
  174. {
  175. var schoolKeys = new List<IdCodeCount>();
  176. var teacherKeys = new List<IdCodeCount>();
  177. string sqlT = "select c.id,c.name, 'Teacher' as code from c where c.code='Base'";
  178. string sqlS = "select c.id,c.name, 'School' as code from c where c.code='Base'";
  179. await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher).
  180. GetItemQueryIterator<IdCodeCount>(queryText: sqlT, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Base") }))
  181. {
  182. teacherKeys.Add(item);
  183. }
  184. await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).
  185. GetItemQueryIterator<IdCodeCount>(queryText: sqlS, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey("Base") }))
  186. {
  187. schoolKeys.Add(item);
  188. }
  189. string sqlcount = "select value count(1) from c where c.pk='LessonRecord'";
  190. foreach (var key in schoolKeys)
  191. {
  192. await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.School).
  193. GetItemQueryIterator<int>(queryText: sqlcount, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"LessonRecord-{key.id}") }))
  194. {
  195. key.count = item;
  196. break;
  197. }
  198. }
  199. foreach (var key in teacherKeys)
  200. {
  201. await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Teacher).
  202. GetItemQueryIterator<int>(queryText: $"select value count(1) from c where c.pk='LessonRecord' and c.tmdid='{key.id}'",
  203. requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey($"LessonRecord") }))
  204. {
  205. key.count = item;
  206. break;
  207. }
  208. }
  209. schoolKeys.AddRange(teacherKeys);
  210. //以下代码作用,用于根据当前拥有的课例数判断区分学校和个人使用课例频率越多的,
  211. //并均分每次需要处理的学校和个人容器个数,避免每次处理批次花费时间相差太大。
  212. //将每个批次处理数量级控制在500-1000左右。
  213. List<IEnumerable<IdCodeCount>> counts = new List<IEnumerable<IdCodeCount>>();
  214. //501-∞
  215. var count501_ = schoolKeys.Where(x => x.count >= 501); //至少500条
  216. counts.AddRange(count501_.Page(1));//1个学校或个人
  217. //201-500
  218. var count201_500 = schoolKeys.Where(x => x.count >= 201 && x.count <= 500); //至少603条-1500条
  219. counts.AddRange(count201_500.Page(3));//2个学校或个人
  220. //100-200
  221. var count100_200 = schoolKeys.Where(x => x.count >= 100 && x.count <= 200);//至少500条-1000条
  222. counts.AddRange(count100_200.Page(10));//5个学校或个人
  223. //51-99
  224. var count51_99 = schoolKeys.Where(x => x.count >= 51 && x.count <= 99);//至少500条-990条
  225. counts.AddRange(count51_99.Page(10));//10个学校或个人
  226. //21-50
  227. var count21_50 = schoolKeys.Where(x => x.count >= 21 && x.count <= 50);//至少410条-1000条
  228. counts.AddRange(count21_50.Page(20));//20个学校或个人
  229. //10-20
  230. var count10_20 = schoolKeys.Where(x => x.count >= 10 && x.count <= 20);//至少500条-1000条
  231. counts.AddRange(count10_20.Page(50));//50个学校或个人
  232. //5-9
  233. var count0_9 = schoolKeys.Where(x => x.count >= 5 && x.count <= 9);//至少500条-900条
  234. counts.AddRange(count0_9.Page(100));//100个学校或个人
  235. //2-4
  236. var count2_4 = schoolKeys.Where(x => x.count >= 2 && x.count <= 4);//至少600条-1200条
  237. counts.AddRange(count2_4.Page(300));
  238. //0-1,已经处理过一次,不用再处理
  239. var count0_1 = schoolKeys.Where(x => x.count ==1);//至少500,就算没有课例也算一次。
  240. counts.AddRange(count0_1.Page(1000));
  241. int field = 1;
  242. foreach (var item in counts)
  243. {
  244. if (item.Any())
  245. {
  246. bool stuallstatus = await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", field,
  247. new UnusedLock { field = field, status = 0, item = item }.ToJsonString());
  248. field += 1;
  249. }
  250. }
  251. await _dingDing.SendBotMsg($"{location},获取到:{schoolKeys.Count} 个学校和个人的容器\n" +
  252. $"大概要处理:{schoolKeys.Sum(x => x.count) + schoolKeys.Where(x => x.count >= 0).Count()} 条数据\n" +
  253. $"将数据分为:{field - 1} 次处理,每次处理500-1000条数据。", GroupNames.醍摩豆服務運維群組);
  254. }
  255. //key存在的时候 开始进行数据处理
  256. var records = await _azureRedis.GetRedisClient(8).HashGetAllAsync($"LessonRecord:Unused:Lock:{location}");
  257. List<UnusedLock> unuseds = new List<UnusedLock>();
  258. foreach (var rcd in records)
  259. {
  260. var value = rcd.Value.ToString().ToObject<UnusedLock>();
  261. unuseds.Add(value);
  262. }
  263. int max = unuseds.Max(x => x.field);
  264. int index = max + 1;
  265. var unusedLock = unuseds.Where(s => s.status == 0)?.OrderBy(x => x.field)?.First();
  266. if (unusedLock != null)
  267. {
  268. var stime = DateTimeOffset.UtcNow;
  269. long s = stime.ToUnixTimeMilliseconds();
  270. string sdata = stime.ToString("yyyy-MM-dd HH:mm:ss");
  271. await _dingDing.SendBotMsg($"{location}-开始第{unusedLock.field}轮清理冗余的课例记录文件...\n开始时间:{sdata}\n清理容器有{unusedLock.item.Select(x => $"{x.name}-{x.id}").ToJsonString()}", GroupNames.醍摩豆服務運維群組);
  272. //将当前的标记为1锁定。
  273. bool stuallstatus = await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", unusedLock.field,
  274. new UnusedLock { field = unusedLock.field, status = 1, item = unusedLock.item }.ToJsonString());
  275. List<KeyValuePair<string, List<string>>> deleteUrls = new List<KeyValuePair<string, List<string>>>();
  276. foreach (IdCodeCount idCode in unusedLock.item)
  277. {
  278. try
  279. {
  280. List<string> urls = new List<string>();
  281. var ContainerClient = _azureStorage.GetBlobContainerClient(idCode.id);
  282. List<string> items = await ContainerClient.List("records");
  283. if (items.IsNotEmpty())
  284. {
  285. HashSet<string> set = new HashSet<string>();
  286. items.ForEach(z => {
  287. var uri = z.Split("/");
  288. if (uri.Length > 1)
  289. {
  290. set.Add(uri[1]);
  291. }
  292. });
  293. if (set.Any())
  294. {
  295. string tbname = idCode.code.Equals("Teacher") ? Constant.Teacher : Constant.School;
  296. string pk = idCode.code.Equals("Teacher") ? "LessonRecord" : $"LessonRecord-{idCode.id}";
  297. string sql = $"select value c.id from c where c.pk='LessonRecord' and c.id in ({string.Join(",", set.Select(m => $"'{m}'"))})";
  298. List<string> ids = new List<string>();
  299. await foreach (var item in _azureCosmos.GetCosmosClient().GetContainer(Constant.TEAMModelOS, tbname)
  300. .GetItemQueryIterator<string>(queryText: sql, requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(pk) }))
  301. {
  302. ids.Add(item);
  303. }
  304. var notin = set.Except(ids);
  305. if (notin.Any())
  306. {
  307. foreach (var not in notin)
  308. {
  309. if (!string.IsNullOrWhiteSpace(not)) {
  310. string url = $"records/{not}";
  311. long id = -1;
  312. long.TryParse(not, out id);
  313. if (id > -1)
  314. {
  315. id = _snowflakeId.ParseIdToTimeStamp(id);
  316. }
  317. if (id > -1)
  318. {
  319. //72小时之外的数据。需要被删除 72 * 60 * 60 *1000= 259200000
  320. //当前时间-课例产生的时间戳
  321. if (s - id > 259200000)
  322. {
  323. urls.Add(url);
  324. await _azureStorage.GetBlobServiceClient().DeleteBlobs(_dingDing, idCode.id, new List<string> { url });
  325. }
  326. }
  327. }
  328. }
  329. }
  330. }
  331. }
  332. if (urls.IsNotEmpty()) {
  333. deleteUrls.Add(new KeyValuePair<string, List<string>>($"{idCode.name}-{idCode.id}", urls));
  334. }
  335. }
  336. catch (Exception ex)
  337. {
  338. //异常的学校,需要将数据放回redis 重新处理。
  339. await _azureRedis.GetRedisClient(8).HashSetAsync($"LessonRecord:Unused:Lock:{location}", index,
  340. new UnusedLock { field = index, status = 0, item = new List<IdCodeCount>() { idCode } }.ToJsonString());
  341. index += 1;
  342. await _dingDing.SendBotMsg($"{location}-第{unusedLock.field}轮清理时容器:{idCode.id},类型:{idCode.code}出现异常。\n将{idCode.id}重新加入队列,队列编号:{index}\n" +
  343. $"异常信息:{ex.Message},{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
  344. }
  345. }
  346. await _azureRedis.GetRedisClient(8).HashDeleteAsync($"LessonRecord:Unused:Lock:{location}", unusedLock.field);
  347. var etime = DateTimeOffset.UtcNow;
  348. long e = etime.ToUnixTimeMilliseconds();
  349. string edata = etime.ToString("yyyy-MM-dd HH:mm:ss");
  350. long d = (e - s) / 1000;
  351. string timeStr = d >= 60 ? Math.Round(d / 60.0) + "分" : d + "秒";
  352. List<string> content = new List<string>();
  353. foreach (var url in deleteUrls)
  354. {
  355. if (url.Value.IsNotEmpty()) {
  356. content.Add($"{url.Key}\n {string.Join(" \t\n", url.Value)}");
  357. }
  358. }
  359. if (max == unusedLock.field)
  360. {
  361. //锁定15至少小时。
  362. await _azureRedis.GetRedisClient(8).StringSetAsync($"LessonRecord:Unused:Lock:Lock", "用于在清理完所有Blob容器后,锁定不再进行多次清理。");
  363. await _azureRedis.GetRedisClient(8).KeyExpireAsync($"LessonRecord:Unused:Lock:Lock", DateTime.UtcNow.AddHours(15) );
  364. }
  365. if (content.IsNotEmpty())
  366. {
  367. string str = string.Join("\n", content);
  368. await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:\n{str}", GroupNames.醍摩豆服務運維群組);
  369. }
  370. else {
  371. // await _dingDing.SendBotMsg($"{location}-结束第{unusedLock.field}轮清理冗余的课例记录文件...\n结束时间:{edata}\n耗时:{timeStr}\n清理内容:暂无", GroupNames.醍摩豆服務運維群組);
  372. }
  373. }
  374. }
  375. } catch (Exception ex) {
  376. await _dingDing.SendBotMsg($"{location} 清理时容器出现异常。\n异常信息:{ex.Message},{ex.StackTrace}", GroupNames.醍摩豆服務運維群組);
  377. }
  378. }
  379. public class UnusedLock {
  380. public int field { get; set; }
  381. /// <summary>
  382. /// 0默认未被执行,1 正在执行中
  383. /// </summary>
  384. public int status { get; set; }
  385. public IEnumerable<IdCodeCount> item { get; set; } = new List<IdCodeCount>();
  386. }
  387. }
  388. }