|
|
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 可以为 null;C# 的值类型默认不能为 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;
|
|
|
}
|
|
|
}
|