feat(Backend): update API gateway and websocket hubs
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticBackend.Util;
|
||||
using FinlyticCore.Dtos;
|
||||
using FinlyticCore.Dtos.News;
|
||||
using FinlyticCore.Dtos.Sentiment;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace FinlyticBackend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/v1/news")]
|
||||
public class NewsController : ControllerBase
|
||||
{
|
||||
private readonly WebMqttClient _mqttClient;
|
||||
private readonly ILogger<NewsController> _logger;
|
||||
|
||||
public NewsController(WebMqttClient mqttClient, ILogger<NewsController> logger)
|
||||
{
|
||||
_mqttClient = mqttClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves paginated news articles with optional filters and enriched sentiment data.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetNews(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? symbol = null,
|
||||
[FromQuery] string? isin = null,
|
||||
[FromQuery] string? date = null,
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] bool? hasSentiment = null)
|
||||
{
|
||||
string? activeSymbol = !string.IsNullOrWhiteSpace(symbol) ? symbol.Trim() : isin?.Trim();
|
||||
string? effectiveStatus = status;
|
||||
|
||||
if (hasSentiment == true && string.IsNullOrEmpty(effectiveStatus))
|
||||
{
|
||||
effectiveStatus = "Analyzed";
|
||||
}
|
||||
|
||||
string? dateStr = !string.IsNullOrWhiteSpace(date) ? date.Trim() : null;
|
||||
if (string.Equals(dateStr, "today", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dateStr = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_mqttClient.IsConnected)
|
||||
{
|
||||
var payload = new DailyNewsRequest(
|
||||
Limit: pageSize,
|
||||
Offset: (page - 1) * pageSize,
|
||||
Isin: activeSymbol,
|
||||
Date: dateStr,
|
||||
Status: effectiveStatus,
|
||||
Query: query,
|
||||
HasSentiment: hasSentiment
|
||||
);
|
||||
|
||||
var articles = await _mqttClient.SendRpcRequestAsync<List<NewsArticleDto>, DailyNewsRequest>(
|
||||
"news_Get",
|
||||
payload,
|
||||
TimeSpan.FromSeconds(10)
|
||||
);
|
||||
|
||||
if (articles != null && articles.Count > 0)
|
||||
{
|
||||
// Parallelisierte Anreichung fehlender Sentiment-Daten
|
||||
if (hasSentiment == true || string.Equals(effectiveStatus, "Analyzed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var enrichmentTasks = articles.Select(async article =>
|
||||
{
|
||||
if (article.FinbertResult != null)
|
||||
{
|
||||
return article;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sentimentEntry = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
|
||||
"sentiment_GetArticle",
|
||||
new ArticleRequest(article.Id.ToString(), article.Id.ToString()),
|
||||
TimeSpan.FromSeconds(2)
|
||||
);
|
||||
|
||||
if (sentimentEntry?.FinbertResult != null)
|
||||
{
|
||||
var result = sentimentEntry.FinbertResult;
|
||||
return article with
|
||||
{
|
||||
Sentiment = result.Label,
|
||||
SentimentScore = result.CompoundScore,
|
||||
Confidence = result.Confidence,
|
||||
FinbertResult = result
|
||||
};
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// RPC Timeout oder nicht analysiert -> Un-angereicherten Artikel zurückgeben
|
||||
}
|
||||
|
||||
return article;
|
||||
});
|
||||
|
||||
var enrichedArticles = await Task.WhenAll(enrichmentTasks);
|
||||
return Ok(enrichedArticles);
|
||||
}
|
||||
|
||||
return Ok(articles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("[GetNews] Cannot send MQTT RPC - MQTT client is NOT connected to broker.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[GetNews] MQTT RPC news_Get request failed for symbol '{Symbol}'", activeSymbol);
|
||||
}
|
||||
|
||||
return Ok(new List<NewsArticleDto>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves sentiment summary for a specific ISIN.
|
||||
/// </summary>
|
||||
[HttpGet("sentiment/isin/{isin}")]
|
||||
public async Task<IActionResult> GetIsinSentimentSummary(string isin)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(isin)) return NotFound(new { message = "ISIN is required." });
|
||||
|
||||
var isinCode = isin.Trim().ToUpperInvariant();
|
||||
|
||||
try
|
||||
{
|
||||
if (_mqttClient.IsConnected)
|
||||
{
|
||||
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinSentimentSummaryDto, IsinRequest>(
|
||||
"sentiment_GetIsin",
|
||||
new IsinRequest(isinCode),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
if (rpcResult != null)
|
||||
{
|
||||
return Ok(rpcResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "MQTT RPC sentiment_GetIsin failed for ISIN '{Isin}'", isinCode);
|
||||
}
|
||||
|
||||
// File-System Fallback
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "isin", $"{isinCode}.json"),
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "isin", $"{isinCode}.json"),
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "..", "data", "summaries", "isin", $"{isinCode}.json")
|
||||
};
|
||||
|
||||
foreach (var path in candidates)
|
||||
{
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await System.IO.File.ReadAllTextAsync(path);
|
||||
return Content(json, "application/json");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return NotFound(new { message = $"No sentiment summary found for ISIN {isinCode}." });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves sentiment analysis for a specific article.
|
||||
/// </summary>
|
||||
[HttpGet("sentiment/article/{articleId}")]
|
||||
public async Task<IActionResult> GetArticleSentimentAnalysis(string articleId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(articleId)) return NotFound(new { message = "ArticleId is required." });
|
||||
|
||||
var targetId = articleId.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
if (_mqttClient.IsConnected)
|
||||
{
|
||||
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
|
||||
"sentiment_GetArticle",
|
||||
new ArticleRequest(targetId, targetId),
|
||||
TimeSpan.FromSeconds(5)
|
||||
);
|
||||
|
||||
if (rpcResult != null)
|
||||
{
|
||||
return Ok(rpcResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "MQTT RPC sentiment_GetArticle failed for articleId '{ArticleId}'", targetId);
|
||||
}
|
||||
|
||||
// File-System Fallback
|
||||
var articleCandidates = new[]
|
||||
{
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "articles", $"{targetId}.json"),
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "articles", $"{targetId}.json")
|
||||
};
|
||||
|
||||
foreach (var path in articleCandidates)
|
||||
{
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await System.IO.File.ReadAllTextAsync(path);
|
||||
return Content(json, "application/json");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return NotFound(new { message = $"No sentiment analysis found for article {articleId}." });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user