namespace Admin.NET.Plugin.HwPortal; [Route("portal/analytics")] public class HwAnalyticsController : HwPortalControllerBase { private readonly HwAnalyticsService _service; public HwAnalyticsController(HwAnalyticsService service) { _service = service; } // 这里故意用 POST,而不是 GET。 // 原因是埋点采集本质上是“写入事件”,语义上就应该是 POST。 // Spring Boot 里这个思路也一样,只是写法会是 @PostMapping("/collect")。 [HttpPost("collect")] [AllowAnonymous] // [Consumes] 是 ASP.NET Core 的请求体内容类型约束。 // 它告诉框架:这个接口允许 application/json 和 text/plain 两种 Content-Type。 // 之所以要写这个,是因为原 ruoyi-portal 明确支持前端把 JSON 字符串当 text/plain 发过来。 [Consumes("application/json", "text/plain")] [HwPortalIpRateLimit("portal_analytics_collect", 60, 240)] public async Task Collect() { // 这里没有直接写参数 [FromBody] AnalyticsCollectRequest request, // 是因为我们要同时兼容 “真正 JSON 请求体” 和 “text/plain 包着 JSON 字符串” 两种历史行为。 // 如果强行交给默认模型绑定,text/plain 场景很容易直接绑定失败。 string body = await ReadRequestBodyAsync(); if (string.IsNullOrWhiteSpace(body)) { return Error("请求体不能为空"); } try { // JsonSerializer.Deserialize() 是 C# 常见的 JSON 反序列化写法。 // 你可以把它理解成 Jackson 的 objectMapper.readValue(body, AnalyticsCollectRequest.class)。 AnalyticsCollectRequest request = JsonSerializer.Deserialize(body, new JsonSerializerOptions { // PropertyNameCaseInsensitive = true 表示“属性名大小写不敏感”。 // 这样前端传 eventType / EventType / eventtype 都能尽量兼容,减少历史字段命名不统一导致的问题。 PropertyNameCaseInsensitive = true }); // HttpContext 是 ASP.NET Core 里贯穿整个请求的核心上下文对象。 // 类似于你在 Spring MVC 里能拿到的 HttpServletRequest / SecurityContext / RequestAttributes 的组合入口。 await _service.Collect(request, HwPortalContextHelper.CurrentRequestIp(HttpContext), Request.Headers["User-Agent"].ToString()); return Success(); } catch (Exception ex) { // 这里不把异常直接抛出去,而是转成兼容 AjaxResult。 // 原因是这个接口在源模块里就是“失败返回 error(msg)”风格,迁移后要保持对外行为一致。 return Error($"采集失败: {ex.Message}"); } } [HttpGet("dashboard")] public async Task Dashboard([FromQuery] DateTime? statDate, [FromQuery(Name = "top")] int? top) { // DateTime? 里的问号表示“可空类型”。 // Java 里你常见 Date/LocalDate 可以为 null;C# 的值类型默认不能为 null,所以要写成 DateTime? 才能接收空值。 return Success(await _service.GetDashboard(statDate, top)); } [HttpPost("refreshDaily")] public async Task RefreshDaily([FromQuery] DateTime? statDate) { await _service.RefreshDailyStat(statDate); return Success(null, "刷新成功"); } private async Task ReadRequestBodyAsync() { // Request.EnableBuffering() 的作用是“允许请求体被重复读取”。 // 这一点和 Java Servlet 最大的差异之一在于: // Java 里 InputStream 一般读一次就没了,ASP.NET Core 这里也一样,所以如果你想自己读取后还让后续组件继续用,就必须先开启缓冲。 Request.EnableBuffering(); // Position = 0 表示把流指针移回开头。 // 你可以把它理解成“从头重新读一遍文件/流”。 Request.Body.Position = 0; // using StreamReader reader = new(...) 是 C# 的资源释放语法。 // Java 里你可以类比 try-with-resources。 // leaveOpen: true 的意思是“Reader 用完后,不要顺手把底层 Request.Body 也关掉”,否则后续框架再访问请求体就会出问题。 using StreamReader reader = new(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); string body = await reader.ReadToEndAsync(); // 读完后把流指针再拨回起点,这是为了保证后续如果别的组件还想读请求体,不会读到空内容。 Request.Body.Position = 0; return body; } }