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#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}
}