Tutto ciò che l'applicazione registra sta ora in due file SQLite in %LocalAppData%\AutoBidder\Database: autobidder.sqlite per le osservazioni e esercizio.sqlite per i documenti d'esercizio (aste nel monitor, prodotti, promozioni, modelli appresi con prefisso ml/) e per i registri applicativo e del riscatto puntate. Il motore comune è SqliteDatabase; i file JSON e la cartella Apprendimento delle versioni precedenti vengono importati la prima volta e lasciati dove sono. La cartella si cambia dalle Impostazioni: al salvataggio si chiede se spostare i file, e si riavvia. Le cartelle Dati/Statistiche/Registri, i registri su file, le misure dell'anticipo e le impostazioni morte (LogBids, AutoApplyProductDefaults, NewAuctionLimitsPriority…) non esistono più. Il kill-switch se ne va: al suo posto un solo interruttore in barra, accanto ad Avvia/Osserva/Ferma, che spegne l'apprendimento. Spento, il motore punta solo entro i limiti dell'utente (prezzo, puntate, budget, fascia oraria, rischio) con l'anticipo fisso: niente valore atteso, regime, duello, bandit, anticipo adattivo. Le aste vengono comunque registrate e studiate. Tre difetti visti osservando il motore dal vivo per un'ora, in Osserva: - la copertura «Compralo Ora» con V lasciato al listino azzerava la perdita coperta e il motore diceva BID a ogni scadenza con P dell'1%: ora vale solo per i prodotti con «Valore reale €» scritto, altrimenti la puntata si conta persa e P decide; - la chiusura di un'asta arrivava due volte (fine, poi rimozione dal monitor) e di nuovo al riavvio: modello, profilo, bandit e statistiche per prodotto contavano ogni asta due volte. Il monitor la comunica una volta sola, l'apprendimento salta le aste già apprese, e le schede prodotto vengono ricostruite una volta dallo storico; - la pagina delle ricompense veniva scambiata per la pagina di accesso perché porta un collegamento a /login.php: prima si guarda se è la pagina delle ricompense. Interfaccia: la scheda Esporta e tutto il suo codice sono tolti; le esportazioni chiedono sempre dove salvare. Prodotti, Storico e Apprendimento hanno barre a sole icone con tooltip che dicono esattamente cosa succede, colorate come il monitor, con i nuovi pulsanti di pulizia (spegni tutte le stelline, togli i non seguiti, pulizia completa; seleziona tutte / elimina le selezionate / svuota lo storico). Impostazioni: sezione Database, Manutenzione con «Elimina tutti i dati (tranne la login)» e «Impostazioni di fabbrica», esportazione dei registri in testo. Modifiche.txt, il prompt ormai realizzato, esce dal repository. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
157 lines
5.3 KiB
C#
157 lines
5.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
|
|
namespace AutoBidder.Utilities
|
|
{
|
|
/// <summary>
|
|
/// Ricorda quali collegamenti promozionali sono già stati aperti.
|
|
///
|
|
/// <para>È la parte che rende sensata la raccolta automatica. La pagina di origine tiene
|
|
/// gli stessi collegamenti per giorni: senza memoria, ogni giro riaprirebbe tutto —
|
|
/// decine di richieste identiche allo stesso indirizzo, per premi già presi. Con la
|
|
/// memoria, dopo il primo giro il lavoro normale è "trovati venti, nessuno nuovo".</para>
|
|
///
|
|
/// <para>Un codice fallito viene ritentato un numero limitato di volte: un collegamento
|
|
/// scaduto resta pubblicato per sempre, e ritentarlo all'infinito sarebbe lo stesso
|
|
/// spreco da cui questa classe difende.</para>
|
|
/// </summary>
|
|
public static class ClaimedPromoStore
|
|
{
|
|
/// <summary>Oltre questa soglia i codici più vecchi vengono dimenticati.</summary>
|
|
private const int MaxRecords = 3000;
|
|
|
|
private static readonly object Sync = new();
|
|
private static StoreData? _cache;
|
|
|
|
public const string DocumentKey = "claimed-promos";
|
|
|
|
public sealed class PromoRecord
|
|
{
|
|
public string Code { get; set; } = "";
|
|
public bool Claimed { get; set; }
|
|
public int Attempts { get; set; }
|
|
public DateTime FirstSeen { get; set; } = DateTime.Now;
|
|
public DateTime LastAttempt { get; set; } = DateTime.Now;
|
|
public string LastDetail { get; set; } = "";
|
|
}
|
|
|
|
private sealed class StoreData
|
|
{
|
|
public List<PromoRecord> Records { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// True se vale la pena provare questo codice: mai preso e non ancora ritentato
|
|
/// troppe volte.
|
|
/// </summary>
|
|
public static bool ShouldTry(string code, int maxAttempts)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code)) return false;
|
|
|
|
lock (Sync)
|
|
{
|
|
var record = Find(Load(), code);
|
|
|
|
if (record == null) return true;
|
|
if (record.Claimed) return false;
|
|
|
|
return record.Attempts < Math.Max(1, maxAttempts);
|
|
}
|
|
}
|
|
|
|
public static void RecordOutcome(string code, bool claimed, string detail)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code)) return;
|
|
|
|
lock (Sync)
|
|
{
|
|
var data = Load();
|
|
var record = Find(data, code);
|
|
|
|
if (record == null)
|
|
{
|
|
record = new PromoRecord { Code = code.Trim() };
|
|
data.Records.Add(record);
|
|
}
|
|
|
|
record.Claimed |= claimed;
|
|
record.Attempts++;
|
|
record.LastAttempt = DateTime.Now;
|
|
record.LastDetail = detail.Length <= 200 ? detail : detail[..200];
|
|
|
|
if (data.Records.Count > MaxRecords)
|
|
data.Records.RemoveRange(0, data.Records.Count - MaxRecords);
|
|
|
|
Save();
|
|
}
|
|
}
|
|
|
|
/// <summary>Quanti codici sono stati riscossi con successo, da sempre.</summary>
|
|
public static int ClaimedCount
|
|
{
|
|
get { lock (Sync) return Load().Records.Count(r => r.Claimed); }
|
|
}
|
|
|
|
/// <summary>Gli ultimi codici trattati, dal più recente. Per la scheda Puntate.</summary>
|
|
public static IReadOnlyList<PromoRecord> Recent(int count)
|
|
{
|
|
lock (Sync)
|
|
{
|
|
return Load().Records
|
|
.OrderByDescending(r => r.LastAttempt)
|
|
.Take(Math.Max(1, count))
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dimentica tutto. Utile quando si cambia account: i codici presi da un utente non
|
|
/// dicono nulla su quelli ancora disponibili per un altro.
|
|
/// </summary>
|
|
public static void Clear()
|
|
{
|
|
lock (Sync)
|
|
{
|
|
_cache = new StoreData();
|
|
Save();
|
|
}
|
|
}
|
|
|
|
private static PromoRecord? Find(StoreData data, string code) =>
|
|
data.Records.FirstOrDefault(r => string.Equals(r.Code, code.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static StoreData Load()
|
|
{
|
|
if (_cache != null) return _cache;
|
|
|
|
try
|
|
{
|
|
var json = Data.OperationalDatabase.Instance.GetDocumentOrImport(DocumentKey, AppPaths.LegacyClaimedPromosFile);
|
|
_cache = string.IsNullOrWhiteSpace(json)
|
|
? new StoreData()
|
|
: JsonSerializer.Deserialize<StoreData>(json) ?? new StoreData();
|
|
}
|
|
catch
|
|
{
|
|
// Un file illeggibile non deve fermare il riscatto: al massimo si riprova un
|
|
// codice già preso, che il sito rifiuterà da sé.
|
|
_cache = new StoreData();
|
|
}
|
|
|
|
return _cache;
|
|
}
|
|
|
|
private static void Save()
|
|
{
|
|
try
|
|
{
|
|
Data.OperationalDatabase.Instance.SetDocument(DocumentKey, JsonSerializer.Serialize(_cache));
|
|
}
|
|
catch { /* la persistenza non deve mai bloccare l'interfaccia */ }
|
|
}
|
|
}
|
|
}
|