IESTimerTrigger.cs 22 KB

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