PromotionQuerySamplingCore.cs 8.1 KB

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