|
|
@@ -3,6 +3,8 @@ using dodohold.core;
|
|
|
using Dapper;
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
using Org.BouncyCastle.Ocsp;
|
|
|
+using System.Collections.Concurrent;
|
|
|
+using System.Diagnostics;
|
|
|
using System.Text.Json;
|
|
|
using System.Runtime.InteropServices;
|
|
|
using System.Net;
|
|
|
@@ -23,20 +25,71 @@ namespace molilian.api.Controllers
|
|
|
{
|
|
|
[ApiController]
|
|
|
[Route("[controller]/[action]")]
|
|
|
- public class TestController : ControllerBase
|
|
|
- {
|
|
|
-
|
|
|
- protected IHttpContextAccessor _accessor;
|
|
|
- public TestController(IHttpContextAccessor accessor)
|
|
|
- {
|
|
|
- _accessor = accessor;
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- [HttpGet]
|
|
|
- public async Task<ActionResult> ecs_list_test()
|
|
|
- {
|
|
|
- var list = AliyunPoolCore.EcsList();
|
|
|
+ public class TestController : ControllerBase
|
|
|
+ {
|
|
|
+ private const string JdStressDeeplinkContent = "openapp.jdmobile://virtual?params=%7B%22category%22%3A%22jump%22%2C%22des%22%3A%22productDetail%22%2C%22skuId%22%3A%2210139003162781%22%2C%22sourceType%22%3A%22Item%22%2C%22sourceValue%22%3A%22view-ware%22%7D";
|
|
|
+
|
|
|
+ protected IHttpContextAccessor _accessor;
|
|
|
+ public TestController(IHttpContextAccessor accessor)
|
|
|
+ {
|
|
|
+ _accessor = accessor;
|
|
|
+ }
|
|
|
+
|
|
|
+ [HttpGet]
|
|
|
+ public async Task<ActionResult> jd_unsafe_parse_stress([FromQuery] int t = 5, [FromQuery] int time = 60, [FromQuery] string ip = "127.0.0.1", [FromQuery] string oaid = "", [FromQuery] bool wait = false)
|
|
|
+ {
|
|
|
+ t = t <= 0 ? 5 : t;
|
|
|
+ time = time <= 0 ? 60 : time;
|
|
|
+
|
|
|
+ string runId = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
|
|
|
+ string logName = $"run_{runId}_t{t}_time{time}";
|
|
|
+
|
|
|
+ string startLogStatus = await SaveJdStressStartLogAsync(runId, logName, t, time, ip, oaid, wait);
|
|
|
+
|
|
|
+ if (wait)
|
|
|
+ {
|
|
|
+ var result = await RunJdStressAsync(runId, logName, t, time, ip, oaid, startLogStatus, CancellationToken.None);
|
|
|
+ return new APIResult(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ _ = Task.Run(async () =>
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await RunJdStressAsync(runId, logName, t, time, ip, oaid, startLogStatus, CancellationToken.None);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _ = new LoggerLibrary("jd_api_stress", $"{logName}_runner_error")
|
|
|
+ .Info(ex.Message, ex.StackTrace)
|
|
|
+ .SaveAsync();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return new APIResult(new
|
|
|
+ {
|
|
|
+ success = true,
|
|
|
+ message = "后台压测已启动",
|
|
|
+ runId,
|
|
|
+ requestPerSecond = t,
|
|
|
+ timeSeconds = time,
|
|
|
+ expectedRequests = (long)t * time,
|
|
|
+ wait,
|
|
|
+ log = new
|
|
|
+ {
|
|
|
+ type = "LoggerLibrary",
|
|
|
+ dir = "jd_api_stress",
|
|
|
+ name = logName,
|
|
|
+ status = startLogStatus
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ [HttpGet]
|
|
|
+ public async Task<ActionResult> ecs_list_test()
|
|
|
+ {
|
|
|
+ var list = AliyunPoolCore.EcsList();
|
|
|
return new APIResult(new
|
|
|
{
|
|
|
success = true,
|
|
|
@@ -927,6 +980,404 @@ VALUES
|
|
|
public string end_point { get; set; } = string.Empty;
|
|
|
}
|
|
|
|
|
|
+ private static async Task<string> SaveJdStressStartLogAsync(string runId, string logName, int t, int time, string ip, string oaid, bool wait)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await new LoggerLibrary("jd_api_stress", logName)
|
|
|
+ .AppendLine($"status=started")
|
|
|
+ .AppendLine($"runId={runId}")
|
|
|
+ .AppendLine($"startedAt={DateTime.Now:O}")
|
|
|
+ .AppendLine($"t={t}")
|
|
|
+ .AppendLine($"timeSeconds={time}")
|
|
|
+ .AppendLine($"ip={ip}")
|
|
|
+ .AppendLine($"oaid={oaid}")
|
|
|
+ .AppendLine($"wait={wait}")
|
|
|
+ .AppendLine($"content={JdStressDeeplinkContent}")
|
|
|
+ .SaveAsync();
|
|
|
+ return "started";
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ return ex.Message;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<JdStressRunResult> RunJdStressAsync(string runId, string logName, int t, int time, string ip, string oaid, string startLogStatus, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var startedAt = DateTime.Now;
|
|
|
+ var stopwatch = Stopwatch.StartNew();
|
|
|
+
|
|
|
+ long scheduledCount = 0;
|
|
|
+ long completedCount = 0;
|
|
|
+ long successCount = 0;
|
|
|
+ long failCount = 0;
|
|
|
+ long exceptionCount = 0;
|
|
|
+ long riskControlCount = 0;
|
|
|
+ long totalElapsedMs = 0;
|
|
|
+ int activeCount = 0;
|
|
|
+ int peakActiveCount = 0;
|
|
|
+
|
|
|
+ var responseLines = new ConcurrentQueue<string>();
|
|
|
+ var messageCounts = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
|
|
|
+ var codeCounts = new ConcurrentDictionary<string, int>(StringComparer.Ordinal);
|
|
|
+ int initialTaskCapacity = (int)Math.Min((long)t * Math.Min(time, 60), 4096L);
|
|
|
+ var runningTasks = new List<Task>(initialTaskCapacity);
|
|
|
+ string message = "ok";
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ for (int second = 0; second < time && !cancellationToken.IsCancellationRequested; second++)
|
|
|
+ {
|
|
|
+ for (int i = 0; i < t; i++)
|
|
|
+ {
|
|
|
+ long sequence = Interlocked.Increment(ref scheduledCount);
|
|
|
+ runningTasks.Add(Task.Run(async () =>
|
|
|
+ {
|
|
|
+ int currentActive = Interlocked.Increment(ref activeCount);
|
|
|
+ UpdateMax(ref peakActiveCount, currentActive);
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var item = await ExecuteJdStressRequestAsync(sequence, ip, oaid, cancellationToken);
|
|
|
+ Interlocked.Increment(ref completedCount);
|
|
|
+ Interlocked.Add(ref totalElapsedMs, item.ElapsedMs);
|
|
|
+
|
|
|
+ if (item.Success)
|
|
|
+ {
|
|
|
+ Interlocked.Increment(ref successCount);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Interlocked.Increment(ref failCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (item.IsException)
|
|
|
+ {
|
|
|
+ Interlocked.Increment(ref exceptionCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (IsRiskControlResult(item))
|
|
|
+ {
|
|
|
+ Interlocked.Increment(ref riskControlCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ string messageKey = string.IsNullOrWhiteSpace(item.SubMessage)
|
|
|
+ ? item.Message
|
|
|
+ : $"{item.Message}:{item.SubMessage}";
|
|
|
+ if (string.IsNullOrWhiteSpace(messageKey)) messageKey = "empty";
|
|
|
+ messageCounts.AddOrUpdate(messageKey, 1, (_, value) => value + 1);
|
|
|
+ codeCounts.AddOrUpdate(item.Code.ToString(), 1, (_, value) => value + 1);
|
|
|
+
|
|
|
+ responseLines.Enqueue(
|
|
|
+ $"{item.FinishedAt:O}\tseq={item.Sequence}\telapsedMs={item.ElapsedMs}\tcode={item.Code}\tsuccess={item.Success}\tmessage={NormalizeLogValue(item.Message)}\tsub_message={NormalizeLogValue(item.SubMessage)}\tresponse={NormalizeLogValue(item.Response)}");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ Interlocked.Decrement(ref activeCount);
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ await ObserveCompletedTasksAsync(runningTasks);
|
|
|
+
|
|
|
+ var nextTick = TimeSpan.FromSeconds(second + 1);
|
|
|
+ var delay = nextTick - stopwatch.Elapsed;
|
|
|
+ if (delay > TimeSpan.Zero)
|
|
|
+ {
|
|
|
+ await Task.Delay(delay, cancellationToken);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (OperationCanceledException ex)
|
|
|
+ {
|
|
|
+ message = $"canceled:{ex.Message}";
|
|
|
+ responseLines.Enqueue($"{DateTime.Now:O}\tmessage={NormalizeLogValue(message)}");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ message = $"runner error:{ex.Message}";
|
|
|
+ responseLines.Enqueue($"{DateTime.Now:O}\tmessage={NormalizeLogValue(message)}\tstack={NormalizeLogValue(ex.StackTrace ?? string.Empty)}");
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ await Task.WhenAll(runningTasks);
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ message = $"task wait error:{ex.Message}";
|
|
|
+ responseLines.Enqueue($"{DateTime.Now:O}\tmessage={NormalizeLogValue(message)}\tstack={NormalizeLogValue(ex.StackTrace ?? string.Empty)}");
|
|
|
+ }
|
|
|
+
|
|
|
+ stopwatch.Stop();
|
|
|
+
|
|
|
+ string logStatus = "saved";
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var log = new LoggerLibrary("jd_api_stress", logName);
|
|
|
+ log.AppendLine($"status=finished");
|
|
|
+ log.AppendLine($"startLogStatus={startLogStatus}");
|
|
|
+ log.AppendLine($"runId={runId}");
|
|
|
+ log.AppendLine($"startedAt={startedAt:O}");
|
|
|
+ log.AppendLine($"finishedAt={DateTime.Now:O}");
|
|
|
+ log.AppendLine($"t={t}");
|
|
|
+ log.AppendLine($"timeSeconds={time}");
|
|
|
+ log.AppendLine($"ip={ip}");
|
|
|
+ log.AppendLine($"oaid={oaid}");
|
|
|
+ log.AppendLine($"content={JdStressDeeplinkContent}");
|
|
|
+ log.AppendLine($"scheduled={scheduledCount}");
|
|
|
+ log.AppendLine($"completed={completedCount}");
|
|
|
+ log.AppendLine($"success={successCount}");
|
|
|
+ log.AppendLine($"fail={failCount}");
|
|
|
+ log.AppendLine($"exception={exceptionCount}");
|
|
|
+ log.AppendLine($"riskControl={riskControlCount}");
|
|
|
+ log.AppendLine($"peakActive={peakActiveCount}");
|
|
|
+ log.AppendLine("messageCounts=");
|
|
|
+ foreach (var item in messageCounts.OrderByDescending(item => item.Value))
|
|
|
+ {
|
|
|
+ log.AppendLine($"{item.Key}\t{item.Value}");
|
|
|
+ }
|
|
|
+ log.AppendLine("codeCounts=");
|
|
|
+ foreach (var item in codeCounts.OrderByDescending(item => item.Value))
|
|
|
+ {
|
|
|
+ log.AppendLine($"{item.Key}\t{item.Value}");
|
|
|
+ }
|
|
|
+ log.AppendLine("responses=");
|
|
|
+ while (responseLines.TryDequeue(out string? line))
|
|
|
+ {
|
|
|
+ log.AppendLine(line);
|
|
|
+ }
|
|
|
+ await log.SaveAsync();
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ logStatus = ex.Message;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new JdStressRunResult
|
|
|
+ {
|
|
|
+ success = true,
|
|
|
+ message = message,
|
|
|
+ runId = runId,
|
|
|
+ requestPerSecond = t,
|
|
|
+ timeSeconds = time,
|
|
|
+ expectedRequests = (long)t * time,
|
|
|
+ scheduledCount = scheduledCount,
|
|
|
+ completedCount = completedCount,
|
|
|
+ successCount = successCount,
|
|
|
+ failCount = failCount,
|
|
|
+ exceptionCount = exceptionCount,
|
|
|
+ riskControlCount = riskControlCount,
|
|
|
+ peakActiveCount = peakActiveCount,
|
|
|
+ averageElapsedMs = completedCount == 0 ? 0 : Math.Round(totalElapsedMs / (double)completedCount, 2),
|
|
|
+ elapsedSeconds = Math.Round(stopwatch.Elapsed.TotalSeconds, 2),
|
|
|
+ messageCounts = messageCounts.OrderByDescending(item => item.Value).ToDictionary(item => item.Key, item => item.Value),
|
|
|
+ codeCounts = codeCounts.OrderByDescending(item => item.Value).ToDictionary(item => item.Key, item => item.Value),
|
|
|
+ log = new JdStressLogInfo
|
|
|
+ {
|
|
|
+ type = "LoggerLibrary",
|
|
|
+ dir = "jd_api_stress",
|
|
|
+ name = logName,
|
|
|
+ status = logStatus
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task ObserveCompletedTasksAsync(List<Task> runningTasks)
|
|
|
+ {
|
|
|
+ for (int i = runningTasks.Count - 1; i >= 0; i--)
|
|
|
+ {
|
|
|
+ if (!runningTasks[i].IsCompleted) continue;
|
|
|
+
|
|
|
+ await runningTasks[i];
|
|
|
+ runningTasks.RemoveAt(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<JdStressRequestResult> ExecuteJdStressRequestAsync(long sequence, string ip, string oaid, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var sw = Stopwatch.StartNew();
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var request = new UnionParseRequest
|
|
|
+ {
|
|
|
+ Content = JdStressDeeplinkContent,
|
|
|
+ Channel = "jd",
|
|
|
+ CommerceType = 0,
|
|
|
+ Ip = ip,
|
|
|
+ Oaid = oaid,
|
|
|
+ RiskStrategy = string.Empty,
|
|
|
+ LaunchScene = 0,
|
|
|
+ AccountId = 0,
|
|
|
+ SpecialText = 0,
|
|
|
+ QueryText = string.Empty,
|
|
|
+ ClickId = string.Empty,
|
|
|
+ Type = "dp",
|
|
|
+ Pic = string.Empty
|
|
|
+ };
|
|
|
+
|
|
|
+ var result = await UnionParseCore.DeeplinkJdParseAsync(request, cancellationToken);
|
|
|
+ sw.Stop();
|
|
|
+
|
|
|
+ string response = result.Content ?? string.Empty;
|
|
|
+ var parsed = ParseJdStressResponse(response);
|
|
|
+ parsed.Sequence = sequence;
|
|
|
+ parsed.ElapsedMs = sw.ElapsedMilliseconds;
|
|
|
+ parsed.Response = response;
|
|
|
+ parsed.FinishedAt = DateTime.Now;
|
|
|
+ return parsed;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ sw.Stop();
|
|
|
+ return new JdStressRequestResult
|
|
|
+ {
|
|
|
+ Sequence = sequence,
|
|
|
+ ElapsedMs = sw.ElapsedMilliseconds,
|
|
|
+ Success = false,
|
|
|
+ Message = "exception",
|
|
|
+ SubMessage = ex.Message,
|
|
|
+ Code = 0,
|
|
|
+ Response = ex.ToString(),
|
|
|
+ FinishedAt = DateTime.Now,
|
|
|
+ IsException = true
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JdStressRequestResult ParseJdStressResponse(string response)
|
|
|
+ {
|
|
|
+ var result = new JdStressRequestResult();
|
|
|
+ if (string.IsNullOrWhiteSpace(response))
|
|
|
+ {
|
|
|
+ result.Message = "empty response";
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var doc = JsonDocument.Parse(response);
|
|
|
+ var root = doc.RootElement;
|
|
|
+ result.Success = ReadBool(root, "success");
|
|
|
+ result.Message = ReadString(root, "message");
|
|
|
+ result.SubMessage = ReadString(root, "sub_message");
|
|
|
+ result.Code = ReadInt(root, "code");
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ result.Success = false;
|
|
|
+ result.Message = "parse response error";
|
|
|
+ result.SubMessage = ex.Message;
|
|
|
+ result.Code = 0;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool ReadBool(JsonElement root, string propertyName)
|
|
|
+ {
|
|
|
+ if (!root.TryGetProperty(propertyName, out var property)) return false;
|
|
|
+
|
|
|
+ return property.ValueKind switch
|
|
|
+ {
|
|
|
+ JsonValueKind.True => true,
|
|
|
+ JsonValueKind.False => false,
|
|
|
+ JsonValueKind.Number => property.TryGetInt32(out int value) && value != 0,
|
|
|
+ JsonValueKind.String => bool.TryParse(property.GetString(), out bool value) && value,
|
|
|
+ _ => false
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int ReadInt(JsonElement root, string propertyName)
|
|
|
+ {
|
|
|
+ if (!root.TryGetProperty(propertyName, out var property)) return 0;
|
|
|
+
|
|
|
+ return property.ValueKind switch
|
|
|
+ {
|
|
|
+ JsonValueKind.Number => property.TryGetInt32(out int value) ? value : 0,
|
|
|
+ JsonValueKind.String => int.TryParse(property.GetString(), out int value) ? value : 0,
|
|
|
+ _ => 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string ReadString(JsonElement root, string propertyName)
|
|
|
+ {
|
|
|
+ if (!root.TryGetProperty(propertyName, out var property)) return string.Empty;
|
|
|
+ if (property.ValueKind == JsonValueKind.Null || property.ValueKind == JsonValueKind.Undefined) return string.Empty;
|
|
|
+ return property.ValueKind == JsonValueKind.String ? property.GetString() ?? string.Empty : property.ToString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool IsRiskControlResult(JdStressRequestResult result)
|
|
|
+ {
|
|
|
+ if (!"放弃转链".Equals(result.Message, StringComparison.Ordinal)) return false;
|
|
|
+
|
|
|
+ return result.SubMessage.Contains("控制", StringComparison.Ordinal)
|
|
|
+ || result.SubMessage.Contains("风控", StringComparison.Ordinal)
|
|
|
+ || result.SubMessage.Contains("限流", StringComparison.Ordinal)
|
|
|
+ || result.SubMessage.Contains("频", StringComparison.Ordinal)
|
|
|
+ || result.SubMessage.Contains("系统繁忙", StringComparison.Ordinal);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string NormalizeLogValue(string value)
|
|
|
+ {
|
|
|
+ return (value ?? string.Empty)
|
|
|
+ .Replace("\r", "\\r")
|
|
|
+ .Replace("\n", "\\n")
|
|
|
+ .Replace("\t", " ");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void UpdateMax(ref int target, int value)
|
|
|
+ {
|
|
|
+ int snapshot;
|
|
|
+ while (value > (snapshot = Volatile.Read(ref target))
|
|
|
+ && Interlocked.CompareExchange(ref target, value, snapshot) != snapshot)
|
|
|
+ {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class JdStressRequestResult
|
|
|
+ {
|
|
|
+ public long Sequence { get; set; }
|
|
|
+ public bool Success { get; set; }
|
|
|
+ public string Message { get; set; } = string.Empty;
|
|
|
+ public string SubMessage { get; set; } = string.Empty;
|
|
|
+ public int Code { get; set; }
|
|
|
+ public long ElapsedMs { get; set; }
|
|
|
+ public string Response { get; set; } = string.Empty;
|
|
|
+ public DateTime FinishedAt { get; set; }
|
|
|
+ public bool IsException { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class JdStressRunResult
|
|
|
+ {
|
|
|
+ public bool success { get; set; }
|
|
|
+ public string message { get; set; } = string.Empty;
|
|
|
+ public string runId { get; set; } = string.Empty;
|
|
|
+ public int requestPerSecond { get; set; }
|
|
|
+ public int timeSeconds { get; set; }
|
|
|
+ public long expectedRequests { get; set; }
|
|
|
+ public long scheduledCount { get; set; }
|
|
|
+ public long completedCount { get; set; }
|
|
|
+ public long successCount { get; set; }
|
|
|
+ public long failCount { get; set; }
|
|
|
+ public long exceptionCount { get; set; }
|
|
|
+ public long riskControlCount { get; set; }
|
|
|
+ public int peakActiveCount { get; set; }
|
|
|
+ public double averageElapsedMs { get; set; }
|
|
|
+ public double elapsedSeconds { get; set; }
|
|
|
+ public Dictionary<string, int> messageCounts { get; set; } = [];
|
|
|
+ public Dictionary<string, int> codeCounts { get; set; } = [];
|
|
|
+ public JdStressLogInfo log { get; set; } = new();
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class JdStressLogInfo
|
|
|
+ {
|
|
|
+ public string type { get; set; } = string.Empty;
|
|
|
+ public string dir { get; set; } = string.Empty;
|
|
|
+ public string name { get; set; } = string.Empty;
|
|
|
+ public string status { get; set; } = string.Empty;
|
|
|
+ }
|
|
|
+
|
|
|
[HttpGet]
|
|
|
public async Task<ActionResult> xxx()
|
|
|
{
|