- Aggiornamento alla versione Microsoft.EntityFrameworkCore.Sqlite 8.0.0. - Aggiornamento alla versione Microsoft.Windows.SDK.BuildTools 10.0.26100.6584. - Migliorata l'interfaccia per l'inserimento di più URL/ID di aste. - Aggiunti pulsanti per "Aste Chiuse" e "Esporta" in MainWindow. - Creata finestra "Aste Chiuse" per visualizzare e gestire aste chiuse. - Implementato scraper per estrarre dati da aste chiuse. - Aggiunto supporto per esportazione dati in CSV, JSON e XML. - Introdotto contesto Entity Framework per statistiche delle aste. - Aggiunto servizio per calcolo e gestione delle statistiche. - Gestite preferenze di esportazione con salvataggio in file JSON.
110 lines
3.8 KiB
C#
110 lines
3.8 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using AutoBidder.Data;
|
|
using AutoBidder.Models;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AutoBidder.Services
|
|
{
|
|
public class StatsService
|
|
{
|
|
private readonly StatisticsContext _ctx;
|
|
|
|
public StatsService(StatisticsContext ctx)
|
|
{
|
|
_ctx = ctx;
|
|
// Ensure DB created
|
|
_ctx.Database.Migrate();
|
|
}
|
|
|
|
private static string NormalizeKey(string? productName, string? auctionUrl)
|
|
{
|
|
// Prefer auctionUrl numeric id if present
|
|
try
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(auctionUrl))
|
|
{
|
|
var uri = new Uri(auctionUrl);
|
|
// Try regex to find trailing numeric ID
|
|
var m = System.Text.RegularExpressions.Regex.Match(uri.AbsolutePath + uri.Query, @"(\d{6,})");
|
|
if (m.Success) return m.Groups[1].Value;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
if (!string.IsNullOrWhiteSpace(productName))
|
|
{
|
|
var key = productName.Trim().ToLowerInvariant();
|
|
key = System.Text.RegularExpressions.Regex.Replace(key, "[^a-z0-9]+", "_");
|
|
return key;
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
public async Task RecordClosedAuctionAsync(ClosedAuctionRecord rec)
|
|
{
|
|
if (rec == null) return;
|
|
var key = NormalizeKey(rec.ProductName, rec.AuctionUrl);
|
|
if (string.IsNullOrWhiteSpace(key)) return;
|
|
|
|
var stat = await _ctx.ProductStats.FirstOrDefaultAsync(p => p.ProductKey == key);
|
|
if (stat == null)
|
|
{
|
|
stat = new ProductStat
|
|
{
|
|
ProductKey = key,
|
|
ProductName = rec.ProductName ?? "",
|
|
TotalAuctions = 0,
|
|
TotalBidsUsed = 0,
|
|
TotalFinalPriceCents = 0,
|
|
LastSeen = DateTime.UtcNow
|
|
};
|
|
_ctx.ProductStats.Add(stat);
|
|
}
|
|
|
|
stat.TotalAuctions += 1;
|
|
stat.TotalBidsUsed += rec.BidsUsed ?? 0;
|
|
if (rec.FinalPrice.HasValue)
|
|
stat.TotalFinalPriceCents += (long)Math.Round(rec.FinalPrice.Value * 100.0);
|
|
stat.LastSeen = DateTime.UtcNow;
|
|
|
|
await _ctx.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<ProductStat?> GetStatsForKeyAsync(string productName, string auctionUrl)
|
|
{
|
|
var key = NormalizeKey(productName, auctionUrl);
|
|
if (string.IsNullOrWhiteSpace(key)) return null;
|
|
return await _ctx.ProductStats.FirstOrDefaultAsync(p => p.ProductKey == key);
|
|
}
|
|
|
|
public async Task<(int recommendedBids, double recommendedMaxPrice)> GetRecommendationAsync(string productName, string auctionUrl, double userRiskFactor = 1.0)
|
|
{
|
|
var stat = await GetStatsForKeyAsync(productName, auctionUrl);
|
|
if (stat == null || stat.TotalAuctions < 3)
|
|
{
|
|
return (1, 1.0); // conservative defaults
|
|
}
|
|
|
|
int recBids = (int)Math.Ceiling(stat.AverageBidsUsed * userRiskFactor);
|
|
if (recBids < 1) recBids = 1;
|
|
|
|
// recommended max price: avg * (1 + min(0.2, 1/sqrt(n)))
|
|
double factor = 1.0 + Math.Min(0.2, 1.0 / Math.Sqrt(Math.Max(1, stat.TotalAuctions)));
|
|
double recPrice = stat.AverageFinalPrice * factor;
|
|
return (recBids, recPrice);
|
|
}
|
|
|
|
// New: return all stats for export
|
|
public async Task<List<ProductStat>> GetAllStatsAsync()
|
|
{
|
|
return await _ctx.ProductStats
|
|
.OrderByDescending(p => p.LastSeen)
|
|
.ToListAsync();
|
|
}
|
|
}
|
|
}
|