PromotionQuerySamplingCore.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. using Dapper;
  2. using dodohold.core;
  3. using System.Text;
  4. using System.Text.Json;
  5. namespace molilian.core
  6. {
  7. public static class PromotionQuerySamplingCore
  8. {
  9. private const string SamplingRemainingKey = "promotion_query_sampling:remaining";
  10. private const int SamplingSwitchExpireSeconds = 7 * 86400;
  11. private const int MaxEnableCount = 5000;
  12. public const int MaxQueryCount = 1100;
  13. private static readonly SemaphoreSlim EnsureTableLock = new(1, 1);
  14. private static readonly JsonSerializerOptions JsonOptions = new()
  15. {
  16. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
  17. WriteIndented = true
  18. };
  19. private static bool _tableEnsured = false;
  20. public static int EnableSampling(int count)
  21. {
  22. count = Math.Clamp(count, 0, MaxEnableCount);
  23. if (count == 0)
  24. {
  25. RedisHelper.Del(SamplingRemainingKey);
  26. return 0;
  27. }
  28. RedisHelper.Set(SamplingRemainingKey, count, SamplingSwitchExpireSeconds);
  29. return count;
  30. }
  31. public static int GetRemainingCount()
  32. {
  33. return Math.Max(0, RedisHelper.Get<int>(SamplingRemainingKey));
  34. }
  35. public static async Task TrySampleAsync(string imageBase64, string ip, string oaid, string responseJson)
  36. {
  37. if (string.IsNullOrWhiteSpace(imageBase64)
  38. && string.IsNullOrWhiteSpace(ip)
  39. && string.IsNullOrWhiteSpace(oaid)
  40. && string.IsNullOrWhiteSpace(responseJson))
  41. {
  42. return;
  43. }
  44. if (!TryConsumeQuota())
  45. {
  46. return;
  47. }
  48. string sampleId = BuildSampleId();
  49. try
  50. {
  51. await EnsureTableAsync();
  52. using var conn = DBContext.GetOpenConnection();
  53. const string sql = @"
  54. INSERT INTO promotion_query_samples
  55. (id, ip, oaid, image_base64, response_json, create_time)
  56. VALUES
  57. (@id, @ip, @oaid, @image_base64, @response_json, @create_time);";
  58. await conn.ExecuteAsync(sql, new
  59. {
  60. id = sampleId,
  61. ip = ip ?? string.Empty,
  62. oaid = oaid ?? string.Empty,
  63. image_base64 = imageBase64 ?? string.Empty,
  64. response_json = responseJson ?? string.Empty,
  65. create_time = DateTime.Now
  66. });
  67. }
  68. catch (Exception ex)
  69. {
  70. RedisHelper.IncrBy(SamplingRemainingKey);
  71. RedisHelper.Expire(SamplingRemainingKey, SamplingSwitchExpireSeconds);
  72. _ = new LoggerLibrary("promotion_query_samples", "save_error")
  73. .Info(sampleId)
  74. .Info(ex.Message, ex.StackTrace)
  75. .SaveAsync();
  76. }
  77. }
  78. public static async Task<PromotionQuerySampleQueryResultDTO> QueryLogsAsync(int page, int pageSize, string keyword)
  79. {
  80. page = Math.Max(page, 1);
  81. pageSize = Math.Clamp(pageSize, 1, MaxQueryCount);
  82. keyword = NormalizeKeyword(keyword);
  83. await EnsureTableAsync();
  84. using var conn = DBContext.GetOpenConnection();
  85. string where = string.Empty;
  86. var args = new DynamicParameters();
  87. args.Add("count", pageSize);
  88. if (!string.IsNullOrWhiteSpace(keyword))
  89. {
  90. where = "WHERE id LIKE @keyword";
  91. args.Add("keyword", $"%{keyword}%");
  92. }
  93. string countSql = $@"
  94. SELECT COUNT(*)
  95. FROM promotion_query_samples
  96. {where}";
  97. int totalCount = await conn.QuerySingleAsync<int>(countSql, args);
  98. int totalPages = totalCount <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
  99. if (totalPages > 0 && page > totalPages)
  100. {
  101. page = totalPages;
  102. }
  103. int offset = (page - 1) * pageSize;
  104. args.Add("offset", offset);
  105. string sql = $@"
  106. SELECT id, ip, oaid, image_base64, response_json, create_time
  107. FROM promotion_query_samples
  108. {where}
  109. ORDER BY create_time DESC
  110. LIMIT @offset, @count";
  111. var list = (await conn.QueryAsync<PromotionQuerySampleDTO>(sql, args)).ToList();
  112. return new PromotionQuerySampleQueryResultDTO
  113. {
  114. page = totalCount > 0 ? page : 1,
  115. pageSize = pageSize,
  116. totalCount = totalCount,
  117. logs = list.Select(ToLogDto).ToList()
  118. };
  119. }
  120. private static PromotionQuerySampleLogDTO ToLogDto(PromotionQuerySampleDTO item)
  121. {
  122. return new PromotionQuerySampleLogDTO
  123. {
  124. id = item.id,
  125. fileName = $"{item.id}.log",
  126. writeTime = item.create_time.ToString("yyyy-MM-dd HH:mm:ss.fff"),
  127. content = BuildLegacyContent(item),
  128. ip = item.ip,
  129. oaid = item.oaid,
  130. imageBase64 = item.image_base64,
  131. responseJson = item.response_json
  132. };
  133. }
  134. private static string BuildLegacyContent(PromotionQuerySampleDTO item)
  135. {
  136. var builder = new StringBuilder();
  137. if (!string.IsNullOrWhiteSpace(item.image_base64))
  138. {
  139. string clientPost = JsonSerializer.Serialize(new
  140. {
  141. img = item.image_base64,
  142. ip = item.ip,
  143. oaid = item.oaid
  144. }, JsonOptions);
  145. AppendSection(builder, item.create_time, "clientPost", clientPost);
  146. }
  147. else if (!string.IsNullOrWhiteSpace(item.ip) || !string.IsNullOrWhiteSpace(item.oaid))
  148. {
  149. string clientPost = JsonSerializer.Serialize(new
  150. {
  151. ip = item.ip,
  152. oaid = item.oaid
  153. }, JsonOptions);
  154. AppendSection(builder, item.create_time, "clientPost", clientPost);
  155. }
  156. if (!string.IsNullOrWhiteSpace(item.response_json))
  157. {
  158. AppendSection(builder, item.create_time, "responseJson", item.response_json.Trim());
  159. }
  160. return builder.ToString().TrimEnd();
  161. }
  162. private static void AppendSection(StringBuilder builder, DateTime timestamp, string name, string content)
  163. {
  164. if (string.IsNullOrWhiteSpace(content))
  165. {
  166. return;
  167. }
  168. builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"))
  169. .Append('\t')
  170. .Append(name)
  171. .AppendLine()
  172. .AppendLine(content);
  173. }
  174. private static string NormalizeKeyword(string keyword)
  175. {
  176. keyword = (keyword ?? string.Empty).Trim();
  177. if (keyword.EndsWith(".log", StringComparison.OrdinalIgnoreCase))
  178. {
  179. keyword = keyword[..^4];
  180. }
  181. return keyword;
  182. }
  183. private static bool TryConsumeQuota()
  184. {
  185. int remaining = GetRemainingCount();
  186. if (remaining <= 0)
  187. {
  188. return false;
  189. }
  190. long after = RedisHelper.IncrBy(SamplingRemainingKey, -1);
  191. RedisHelper.Expire(SamplingRemainingKey, SamplingSwitchExpireSeconds);
  192. if (after < 0)
  193. {
  194. RedisHelper.Set(SamplingRemainingKey, 0, SamplingSwitchExpireSeconds);
  195. return false;
  196. }
  197. return true;
  198. }
  199. private static string BuildSampleId()
  200. {
  201. var now = DateTime.Now;
  202. return $"{now:yyyyMMdd_HHmmss_fff}_{Guid.NewGuid():N}_{now:yyyyMMdd}";
  203. }
  204. private static async Task EnsureTableAsync()
  205. {
  206. if (_tableEnsured)
  207. {
  208. return;
  209. }
  210. await EnsureTableLock.WaitAsync();
  211. try
  212. {
  213. if (_tableEnsured)
  214. {
  215. return;
  216. }
  217. const string sql = @"
  218. CREATE TABLE IF NOT EXISTS promotion_query_samples (
  219. id VARCHAR(80) NOT NULL PRIMARY KEY COMMENT '采样日志ID',
  220. ip VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求IP',
  221. oaid VARCHAR(128) NOT NULL DEFAULT '' COMMENT '请求OAID',
  222. image_base64 LONGTEXT NOT NULL COMMENT '入参图片base64',
  223. response_json LONGTEXT NOT NULL COMMENT '返回JSON',
  224. create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  225. KEY idx_create_time (create_time)
  226. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='PromotionQuery采样记录';";
  227. using var conn = DBContext.GetOpenConnection();
  228. await conn.ExecuteAsync(sql);
  229. _tableEnsured = true;
  230. }
  231. finally
  232. {
  233. EnsureTableLock.Release();
  234. }
  235. }
  236. }
  237. }