Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion modules/MedalCabinet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
<repository>
<id>helpchat</id>
<url>https://repo.helpch.at/releases/</url>
</repository>
</repositories>

<dependencies>
Expand All @@ -78,8 +82,26 @@
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.1</version>
<version>2.11.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.mcatk.medalcabinet.command.MedalAdminCmd;
import com.mcatk.medalcabinet.command.MedalShowCmd;
import com.mcatk.medalcabinet.command.MedalUsualCmd;
import com.mcatk.medalcabinet.listener.MedalPlayerListener;
import com.mcatk.medalcabinet.papi.MedalPapi;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
Expand All @@ -21,6 +22,7 @@ public void onEnable() {
saveDefaultConfig();
regCommand();
regDependency();
Bukkit.getPluginManager().registerEvents(new MedalPlayerListener(), this);
getLogger().info("启动成功");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mcatk.medalcabinet.listener;

import com.mcatk.medalcabinet.sql.SQLManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

public class MedalPlayerListener implements Listener {

@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
SQLManager.getInstance().clearCache(e.getPlayer().getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,32 @@

import java.sql.*;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SQLManager {
private Connection connection;

private static SQLManager instance = null;
private final Map<String, Medal> mainMedalCache = new ConcurrentHashMap<>();
private static final Medal EMPTY_MEDAL = new Medal("", "", "", "");

public static SQLManager getInstance() {
return instance == null ? instance = new SQLManager() : instance;
}

public static void setInstance(SQLManager instance) {
SQLManager.instance = instance;
}

private SQLManager() {
connectMySQL();
}

protected SQLManager(Connection connection) {
this.connection = connection;
}

private void connectMySQL() {
String ip = MedalCabinet.getPlugin().getConfig().getString("mysql.ip");
String databaseName = MedalCabinet.getPlugin().getConfig().getString("mysql.databasename");
Expand Down Expand Up @@ -157,6 +169,12 @@ public boolean setMainMedal(String playerID, String medalID) {
ps.setString(2, medalID);
ps.setString(3, medalID);
ps.executeUpdate();

Medal newMain = getMedal(medalID);
if (newMain != null) {
mainMedalCache.put(playerID, newMain);
}

Comment on lines +174 to +177
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully updating the main medal in the database, the cache should be cleared or updated regardless of whether getMedal returns null. Currently, if getMedal(medalID) returns null (e.g., if the medal was deleted from the medal table), the cache retains stale data. This causes getMainMedal to return outdated information until the player quits.

Consider either:

  1. Always clearing the cache after a successful database update: mainMedalCache.remove(playerID);
  2. Or updating the cache even when newMain is null: mainMedalCache.put(playerID, newMain != null ? newMain : EMPTY_MEDAL);
Suggested change
if (newMain != null) {
mainMedalCache.put(playerID, newMain);
}
mainMedalCache.put(playerID, newMain != null ? newMain : EMPTY_MEDAL);

Copilot uses AI. Check for mistakes.
return true;
} catch (SQLException e) {
e.printStackTrace();
Expand All @@ -166,6 +184,11 @@ public boolean setMainMedal(String playerID, String medalID) {
}

public Medal getMainMedal(String playerID) {
if (mainMedalCache.containsKey(playerID)) {
Medal cached = mainMedalCache.get(playerID);
return cached == EMPTY_MEDAL ? null : cached;
}

Medal medal = null;
try (PreparedStatement ps = connection.prepareStatement(
"SELECT medal_id FROM `player_main_medal` WHERE player_id = ?"
Expand All @@ -178,7 +201,12 @@ public Medal getMainMedal(String playerID) {
} catch (SQLException e) {
e.printStackTrace();
}
mainMedalCache.put(playerID, medal == null ? EMPTY_MEDAL : medal);
return medal;
}

public void clearCache(String playerID) {
mainMedalCache.remove(playerID);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.mcatk.medalcabinet.sql;

import com.mcatk.medalcabinet.medal.Medal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class SQLManagerTest {

@Mock
private Connection connection;
@Mock
private PreparedStatement preparedStatement;
@Mock
private ResultSet resultSet;

private SQLManager sqlManager;

@BeforeEach
void setUp() throws SQLException {
sqlManager = new SQLManager(connection);
SQLManager.setInstance(sqlManager);

lenient().when(connection.prepareStatement(anyString())).thenReturn(preparedStatement);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PreparedStatement is not always closed on method exit.

Copilot uses AI. Check for mistakes.
lenient().when(preparedStatement.executeQuery()).thenReturn(resultSet);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ResultSet is not always closed on method exit.

Copilot uses AI. Check for mistakes.
}

@Test
void testGetMainMedalCached() throws SQLException {
// Setup mock to return a medal ID "medal1" for player "player1"

// Simulating:
// First query (get main medal ID) returns "medal1"
// Second query (get medal details) returns medal details

when(resultSet.next()).thenReturn(true, true); // Sufficient for 1 call (2 queries)
when(resultSet.getString("medal_id")).thenReturn("medal1");
when(resultSet.getString("medal_name")).thenReturn("Medal One");

// Call 1 - Should hit DB
Medal m1 = sqlManager.getMainMedal("player1");
assertNotNull(m1);
assertEquals("Medal One", m1.getName());

// Call 2 - Should NOT hit DB (cached)
Medal m2 = sqlManager.getMainMedal("player1");
assertNotNull(m2);
assertEquals("Medal One", m2.getName());
assertEquals(m1, m2); // Should be same object reference if cached

// Verify that prepareStatement was called exactly 2 times (for the first call only)
verify(connection, times(2)).prepareStatement(anyString());

// Clear cache
sqlManager.clearCache("player1");

// Setup mock for another call
when(resultSet.next()).thenReturn(true, true);
when(resultSet.getString("medal_id")).thenReturn("medal1");
when(resultSet.getString("medal_name")).thenReturn("Medal One");

// Call 3 - Should hit DB again
Medal m3 = sqlManager.getMainMedal("player1");
assertNotNull(m3);

// Verify prepareStatement called 2 more times (total 4)
verify(connection, times(4)).prepareStatement(anyString());
}

@Test
void testGetMainMedalNullCaching() throws SQLException {
// Setup mock to return no medal (false on first next())
when(resultSet.next()).thenReturn(false);

// Call 1 - Should hit DB (1 query)
Medal m1 = sqlManager.getMainMedal("noplayer");
assertNull(m1);

// Call 2 - Should NOT hit DB (cached empty)
Medal m2 = sqlManager.getMainMedal("noplayer");
assertNull(m2);

// Verify query count.
// Logic:
// 1. SELECT medal_id ... returns empty (resultSet.next() -> false).
// 2. Returns null.
// 3. Cache put(noplayer, EMPTY_MEDAL).

// So only 1 query "SELECT medal_id..." executed.
verify(connection, times(1)).prepareStatement(anyString());
}
}
Comment on lines +40 to +104
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn't cover the setMainMedal method's cache update behavior. Consider adding tests for:

  1. Verifying cache is updated when setMainMedal succeeds
  2. Verifying cache behavior when setMainMedal succeeds but getMedal returns null
  3. Verifying cache state when setMainMedal fails due to SQLException

These scenarios are critical for ensuring the caching implementation works correctly in all cases.

Copilot uses AI. Check for mistakes.
Loading