You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

95 lines
4.8 KiB
C#

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<HwPortalAjaxResult> Collect()
{
// 这里没有直接写参数 [FromBody] AnalyticsCollectRequest request
// 是因为我们要同时兼容 “真正 JSON 请求体” 和 “text/plain 包着 JSON 字符串” 两种历史行为。
// 如果强行交给默认模型绑定text/plain 场景很容易直接绑定失败。
string body = await ReadRequestBodyAsync();
if (string.IsNullOrWhiteSpace(body))
{
return Error("请求体不能为空");
}
try
{
// JsonSerializer.Deserialize<T>() 是 C# 常见的 JSON 反序列化写法。
// 你可以把它理解成 Jackson 的 objectMapper.readValue(body, AnalyticsCollectRequest.class)。
AnalyticsCollectRequest request = JsonSerializer.Deserialize<AnalyticsCollectRequest>(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<HwPortalAjaxResult> Dashboard([FromQuery] DateTime? statDate, [FromQuery(Name = "top")] int? top)
{
// DateTime? 里的问号表示“可空类型”。
// Java 里你常见 Date/LocalDate 可以为 nullC# 的值类型默认不能为 null所以要写成 DateTime? 才能接收空值。
return Success(await _service.GetDashboard(statDate, top));
}
[HttpPost("refreshDaily")]
public async Task<HwPortalAjaxResult> RefreshDaily([FromQuery] DateTime? statDate)
{
await _service.RefreshDailyStat(statDate);
return Success(null, "刷新成功");
}
private async Task<string> 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;
}
}