feat(Backend): update API gateway and websocket hubs

This commit is contained in:
2026-08-09 21:32:52 +02:00
parent fdf4b6efcb
commit a9553e9fbf
66 changed files with 188878 additions and 0 deletions
@@ -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}." });
}
}