From 0841f51a10e04d67d471efa76b40d56600a32af1 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Mon, 29 Jun 2026 03:34:36 +0300 Subject: [PATCH] Enhance transaction fetching with pagination support and improved error handling Signed-off-by: Dmitrii Took 22 minutes --- .github/workflows/workflow.yml | 2 +- .../Controllers/v1/TransactionsController.cs | 11 +- .../client/ui/TransactionHistoryScreen.java | 111 +++++++++++++++--- .../ui/service/BackendAuthenticator.java | 87 ++++++++++++-- .../git/yawaflua/tech/spmega/ModConfig.java | 6 +- 5 files changed, 187 insertions(+), 30 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index d6e2eb3..84187a7 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -22,7 +22,7 @@ jobs: - name: Restore dependencies run: dotnet restore --project backend/SpMega.Backend/SpMega.Backend.csproj - name: Build - run: dotnet build --no-restore --project backend/SpMega.Backend/SpMega.Backend.csproj + run: dotnet build --no-restore backend/SpMega.Backend/SpMega.Backend.csproj - name: Test run: dotnet test --no-build --verbosity normal --project backend/SpMega.Backend/SpMega.Backend.csproj diff --git a/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs b/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs index 034ecb9..2dc2445 100644 --- a/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs +++ b/backend/SpMega.Backend/Controllers/v1/TransactionsController.cs @@ -5,6 +5,8 @@ using System.Text.Json.Nodes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using MongoDB.Bson; +using MongoDB.Driver; using SpMega.Backend.Persistent.Database; using SpMega.Backend.Persistent.Models.Transactions; using SpMega.Backend.Persistent.Models.Users; @@ -16,6 +18,7 @@ namespace SpMega.Backend.Controllers.v1; [ApiController] public class TransactionsController(AppDbContext context, ILogger logger, IConfiguration config) : ControllerBase { + private const int LIMIT = 10; private const string BASE_URL = "https://spworlds.ru/api/public/"; [HttpGet("{billId}")] @@ -127,9 +130,13 @@ public class TransactionsController(AppDbContext context, ILogger>> GetAllTransactions() + public async Task>> GetAllTransactions([FromQuery] int p = 1) { - return Ok(await context.Transactions.Where(k => k.Sender.Id == ((User)HttpContext.Items["@me"]).Id).ToListAsync()); + return Ok(await context.Transactions + .Where(k => EF.Property(k, "SenderId") == ((User)HttpContext.Items["@me"]).Id) + .Skip(p - 1 * LIMIT) + .Take(p * LIMIT) + .ToListAsync()); } [NonAction] diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/TransactionHistoryScreen.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/TransactionHistoryScreen.java index bddf26f..a32f091 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/TransactionHistoryScreen.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/TransactionHistoryScreen.java @@ -3,6 +3,7 @@ package git.yawaflua.tech.spmega.client.ui; import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator; import git.yawaflua.tech.spmega.client.ui.service.BankDatabase.LocalTransaction; import git.yawaflua.tech.spmega.client.ui.service.BankUiService; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; @@ -19,6 +20,8 @@ public class TransactionHistoryScreen extends Screen { private final List transactions = new ArrayList<>(); private boolean loading = true; private String errorMessage = ""; + private int scrollOffset = 0; + private int currentPage = 1; public TransactionHistoryScreen(Screen parent, String cardId, String cardTitle) { super(Text.literal("История транзакций")); @@ -36,40 +39,112 @@ public class TransactionHistoryScreen extends Screen { .dimensions(centerX - 60, startY + 10, 120, 20) .build()); + // Scroll Up/Down buttons + this.addDrawableChild(ButtonWidget.builder(Text.literal("▲"), button -> scrollUp()) + .dimensions(centerX + 140, 65, 20, 20) + .build()); + + this.addDrawableChild(ButtonWidget.builder(Text.literal("▼"), button -> scrollDown()) + .dimensions(centerX + 140, 155, 20, 20) + .build()); + + // Prev Page button + this.addDrawableChild(ButtonWidget.builder(Text.literal("<"), button -> { + if (currentPage > 1) { + currentPage--; + scrollOffset = 0; + loadTransactions(); + } + }).dimensions(centerX - 100, startY - 15, 30, 20).build()); + + // Next Page button + this.addDrawableChild(ButtonWidget.builder(Text.literal(">"), button -> { + currentPage++; + scrollOffset = 0; + loadTransactions(); + }).dimensions(centerX + 70, startY - 15, 30, 20).build()); + loadTransactions(); } + private void scrollUp() { + if (scrollOffset > 0) { + scrollOffset--; + } + } + + private void scrollDown() { + if (scrollOffset < Math.max(0, transactions.size() - 6)) { + scrollOffset++; + } + } + + // Support pre-1.20.2 mouse scroll + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (amount > 0) { + scrollUp(); + } else if (amount < 0) { + scrollDown(); + } + return true; + } + + // Support 1.20.2+ mouse scroll + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (verticalAmount > 0) { + scrollUp(); + } else if (verticalAmount < 0) { + scrollDown(); + } + return true; + } + private void loadTransactions() { loading = true; errorMessage = ""; transactions.clear(); + UiNotifications.instance().show(Text.literal("Загрузка...")); + CompletableFuture.runAsync(() -> { try { + System.out.println("[SPMEGA] Transaction history loading started for card: " + cardId + ", page: " + currentPage); List list = null; git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig(); if (config != null && config.allowBackend()) { try { - list = BackendAuthenticator.fetchTransactionsFromBackend(cardId); + list = BackendAuthenticator.fetchTransactionsFromBackend(cardId, currentPage); + System.out.println("[SPMEGA] Fetched " + (list != null ? list.size() : "null") + " transactions from backend."); } catch (Exception e) { System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: " + e.getMessage()); + e.printStackTrace(); } } if (list == null) { list = BankUiService.instance().getDatabase().loadTransferHistory(cardId); + System.out.println("[SPMEGA] Loaded " + (list != null ? list.size() : "null") + " transactions from database."); } - List finalList = list; - if (this.client != null) { - this.client.execute(() -> { + List finalList = list != null ? list : new ArrayList<>(); + MinecraftClient minecraftClient = MinecraftClient.getInstance(); + if (minecraftClient != null) { + minecraftClient.execute(() -> { + System.out.println("[SPMEGA] Setting transactions list on main thread. Count: " + finalList.size()); this.transactions.addAll(finalList); + this.scrollOffset = 0; this.loading = false; }); + } else { + System.out.println("[SPMEGA] MinecraftClient.getInstance() is null in loadTransactions callback!"); } } catch (Exception e) { - if (this.client != null) { - this.client.execute(() -> { + System.err.println("[SPMEGA] Outer exception in loadTransactions: " + e.getMessage()); + e.printStackTrace(); + MinecraftClient minecraftClient = MinecraftClient.getInstance(); + if (minecraftClient != null) { + minecraftClient.execute(() -> { this.errorMessage = e.getMessage(); this.loading = false; }); @@ -91,30 +166,38 @@ public class TransactionHistoryScreen extends Screen { int centerX = this.width / 2; int startY = 50; + int bottomStartY = this.height / 2 + 50; - context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF); - context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xBFBFBF); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFFFF); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xFFBFBFBF); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Стр. " + currentPage), centerX, bottomStartY - 9, 0xFFFFFFFF); if (loading) { - context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xCCCCCC); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xFFCCCCCC); } else if (!errorMessage.isEmpty()) { - context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFF5555); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFFFF5555); } else if (transactions.isEmpty()) { - context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xCCCCCC); + context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xFFCCCCCC); } else { int y = startY + 15; - for (int i = 0; i < Math.min(6, transactions.size()); i++) { - LocalTransaction tx = transactions.get(i); + int limit = Math.min(6, transactions.size() - scrollOffset); + for (int i = 0; i < limit; i++) { + LocalTransaction tx = transactions.get(scrollOffset + i); String amountText = tx.amount() + " АР"; String dateText = tx.createdAt(); + if (dateText == null) { + dateText = ""; + } if (dateText.length() > 19) { dateText = dateText.substring(0, 19).replace("T", " "); } String commentText = tx.comment().isEmpty() ? "" : " (" + tx.comment() + ")"; String line = String.format("%s -> %s | %s%s", dateText, tx.receiver(), amountText, commentText); - int color = tx.status().equalsIgnoreCase("SUCCESS") ? 0x55FF55 : 0xFF5555; + + String status = tx.status(); + int color = (status != null && status.equalsIgnoreCase("SUCCESS")) ? 0xFF55FF55 : 0xFFFF5555; context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(line), centerX, y, color); y += 18; diff --git a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BackendAuthenticator.java b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BackendAuthenticator.java index 5ad58d0..1d67163 100644 --- a/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BackendAuthenticator.java +++ b/src/client/java/git/yawaflua/tech/spmega/client/ui/service/BackendAuthenticator.java @@ -1,5 +1,6 @@ package git.yawaflua.tech.spmega.client.ui.service; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.authlib.exceptions.AuthenticationException; @@ -314,9 +315,11 @@ public class BackendAuthenticator { return true; } - public static List fetchTransactionsFromBackend(String cardId) throws IOException, InterruptedException { + public static List fetchTransactionsFromBackend(String cardId, int page) throws IOException, InterruptedException { + System.out.println("[SPMEGA] fetchTransactionsFromBackend called for cardId: " + cardId + ", page: " + page); ModConfig config = SPMega.getConfig(); if (config == null || !config.allowBackend()) { + System.out.println("[SPMEGA] fetchTransactionsFromBackend aborted: config is null or backend not allowed."); return List.of(); } @@ -328,7 +331,8 @@ public class BackendAuthenticator { apiDomain = apiDomain.substring(0, apiDomain.length() - 1); } - String url = apiDomain + "/api/v1/transactions?cardId=" + cardId; + String url = apiDomain + "/api/v1/transactions?p=" + page; + System.out.println("[SPMEGA] Requesting transactions from URL: " + url); HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder(URI.create(url)) @@ -338,22 +342,85 @@ public class BackendAuthenticator { .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println("[SPMEGA] Response status code: " + response.statusCode()); if (response.statusCode() != 200) { + System.err.println("[SPMEGA] Failed response body: " + response.body()); throw new IOException("Server returned status code " + response.statusCode() + " on fetch transactions."); } - com.google.gson.JsonArray transactionsArray = JsonParser.parseString(response.body()).getAsJsonArray(); + String body = response.body(); + System.out.println("[SPMEGA] Response body: " + (body.length() > 500 ? body.substring(0, 500) + "..." : body)); + + JsonElement parsed = JsonParser.parseString(body); + com.google.gson.JsonArray transactionsArray = null; + + if (parsed.isJsonArray()) { + transactionsArray = parsed.getAsJsonArray(); + } else if (parsed.isJsonObject()) { + JsonObject obj = parsed.getAsJsonObject(); + if (obj.has("$values") && obj.get("$values").isJsonArray()) { + transactionsArray = obj.getAsJsonArray("$values"); + } else if (obj.has("transactions") && obj.get("transactions").isJsonArray()) { + transactionsArray = obj.getAsJsonArray("transactions"); + } else if (obj.has("values") && obj.get("values").isJsonArray()) { + transactionsArray = obj.getAsJsonArray("values"); + } + } + + if (transactionsArray == null) { + System.err.println("[SPMEGA] Response body is not a JSON array or wrapped array: " + body); + return List.of(); + } + + System.out.println("[SPMEGA] Total transactions returned from server: " + transactionsArray.size()); List list = new ArrayList<>(); for (com.google.gson.JsonElement el : transactionsArray) { - JsonObject json = el.getAsJsonObject(); - String receiver = json.has("receiver") ? json.get("receiver").getAsString() : (json.has("recipient") ? json.get("recipient").getAsString() : "unknown"); - long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L; - String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : ""; - String status = json.has("status") ? json.get("status").getAsString() : "SUCCESS"; - String createdAt = json.has("createdAt") ? json.get("createdAt").getAsString() : (json.has("created_at") ? json.get("created_at").getAsString() : ""); + try { + JsonObject json = el.getAsJsonObject(); - list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt)); + // Check cardId filter + String senderCardNumber = ""; + if (json.has("senderCardNumber") && !json.get("senderCardNumber").isJsonNull()) { + senderCardNumber = json.get("senderCardNumber").getAsString(); + } + + if (!senderCardNumber.equalsIgnoreCase(cardId)) { + continue; + } + + String receiver = "unknown"; + if (json.has("receiver") && !json.get("receiver").isJsonNull()) { + receiver = json.get("receiver").getAsString(); + } else if (json.has("receiverName") && !json.get("receiverName").isJsonNull()) { + receiver = json.get("receiverName").getAsString(); + } else if (json.has("receiverCardNumber") && !json.get("receiverCardNumber").isJsonNull()) { + receiver = json.get("receiverCardNumber").getAsString(); + } else if (json.has("recipient") && !json.get("recipient").isJsonNull()) { + receiver = json.get("recipient").getAsString(); + } + + long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L; + String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : ""; + String status = json.has("status") && !json.get("status").isJsonNull() ? json.get("status").getAsString() : "SUCCESS"; + + String createdAt = ""; + if (json.has("transactionDate") && !json.get("transactionDate").isJsonNull()) { + createdAt = json.get("transactionDate").getAsString(); + } else if (json.has("createdAt") && !json.get("createdAt").isJsonNull()) { + createdAt = json.get("createdAt").getAsString(); + } else if (json.has("created_at") && !json.get("created_at").isJsonNull()) { + createdAt = json.get("created_at").getAsString(); + } + + System.out.printf("[SPMEGA] Parsed transaction: %s -> %s, amount=%d, comment=%s, status=%s%n", + createdAt, receiver, amount, comment, status); + + list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt)); + } catch (Exception parseEx) { + System.err.println("[SPMEGA] Error parsing transaction element: " + parseEx.getMessage()); + } } + System.out.println("[SPMEGA] Returning " + list.size() + " filtered transactions for card " + cardId); return list; } } diff --git a/src/main/java/git/yawaflua/tech/spmega/ModConfig.java b/src/main/java/git/yawaflua/tech/spmega/ModConfig.java index 05595f8..c2f5e5c 100644 --- a/src/main/java/git/yawaflua/tech/spmega/ModConfig.java +++ b/src/main/java/git/yawaflua/tech/spmega/ModConfig.java @@ -3,11 +3,11 @@ package git.yawaflua.tech.spmega; public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled, boolean gpsEnabled, GpsHudPosition gpsPosition) { public static final String DEFAULT_API_DOMAIN = "http://localhost:5129"; - public static final boolean ALLOW_BACKEND = false; - public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3"; + public static final boolean ALLOW_BACKEND = true; + public static final String DEFAULT_API_TOKEN = "-"; public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true; public static final boolean DEFAULT_GPS_ENABLED = true; - public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_LEFT; + public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_CENTER; public static ModConfig createDefault() { return new ModConfig(