feat: 新增优惠券、订单管理等功能

This commit is contained in:
ypsdf1
2026-05-26 15:42:42 +08:00
parent 851375502d
commit 3c65360f9c
32 changed files with 7169 additions and 2661 deletions
+10 -10
View File
@@ -1,10 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+6 -6
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+13 -13
View File
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
Generated
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+48
View File
@@ -0,0 +1,48 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
#################################################################################
# WARNING: Do not store sensitive information in this file, #
# as its contents will be included in the Qodana report. #
#################################################################################
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
projectJDK: "21" #(Applied in CI/CD pipeline)
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
# severityThresholds - configures maximum thresholds for different problem severities
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
# Code Coverage is available in Ultimate and Ultimate Plus plans
#failureConditions:
# severityThresholds:
# any: 15
# critical: 5
# testCoverageThresholds:
# fresh: 70
# total: 50
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-jvm:2026.1
+197 -197
View File
@@ -1,197 +1,197 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
public class AFKManager {
// 玩家最后活动时间(空闲才开始计时)
private final Map<UUID, Long> lastAction =
new HashMap<>();
// 每个玩家当前的警告阶段
private final Map<UUID, Integer> lastStage =
new HashMap<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
new HashMap<>();
private final ConfigManager config;
private final JavaPlugin plugin;
public AFKManager(ConfigManager config,
JavaPlugin plugin) {
this.config = config;
this.plugin = plugin;
}
public void startCheck() {
new BukkitRunnable() {
public void run() {
tick();
}
}.runTaskTimer(plugin, 20L, 20L);
}
private void tick() {
if (!config.afkEnabled) return;
long now = System.currentTimeMillis();
for (Player p : Bukkit.getOnlinePlayers()) {
String name = p.getName();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
continue;
}
int stage = lastStage.getOrDefault(uuid, 0);
// 阶段3:最后10秒
if (remain <= 10) {
if (stage < 3) {
lastStage.put(uuid, 3);
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + remain + " 秒踢出",
0, 22, 3);
playSound(p);
continue;
}
// 阶段2:1分钟~10秒
if (remain <= 60) {
if (stage < 2) {
lastStage.put(uuid, 2);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + fmtSec(remain) + " 踢出",
5, 25, 5);
playSound(p);
continue;
}
// 阶段1:3分钟提醒
if (remain <= 180 && stage < 1) {
lastStage.put(uuid, 1);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§f还剩 " + fmtSec(remain) + " 踢出",
10, 60, 20);
for (int i = 0; i < 3; i++) {
final int delay = i * 10;
Bukkit.getScheduler().runTaskLater(
plugin, () -> {
if (p.isOnline()) playSound(p);
}, delay);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
private void playSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
private String fmtSec(long sec) {
if (sec < 60) return sec + "秒";
long m = sec / 60;
long s = sec % 60;
return m + "分" + (s > 0 ? s + "秒" : "");
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
public class AFKManager {
// 玩家最后活动时间(空闲才开始计时)
private final Map<UUID, Long> lastAction =
new HashMap<>();
// 每个玩家当前的警告阶段
private final Map<UUID, Integer> lastStage =
new HashMap<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
new HashMap<>();
private final ConfigManager config;
private final JavaPlugin plugin;
public AFKManager(ConfigManager config,
JavaPlugin plugin) {
this.config = config;
this.plugin = plugin;
}
public void startCheck() {
new BukkitRunnable() {
public void run() {
tick();
}
}.runTaskTimer(plugin, 20L, 20L);
}
private void tick() {
if (!config.afkEnabled) return;
long now = System.currentTimeMillis();
for (Player p : Bukkit.getOnlinePlayers()) {
String name = p.getName();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
continue;
}
int stage = lastStage.getOrDefault(uuid, 0);
// 阶段3:最后10秒
if (remain <= 10) {
if (stage < 3) {
lastStage.put(uuid, 3);
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + remain + " 秒踢出",
0, 22, 3);
playSound(p);
continue;
}
// 阶段2:1分钟~10秒
if (remain <= 60) {
if (stage < 2) {
lastStage.put(uuid, 2);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
}
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§c还剩 " + fmtSec(remain) + " 踢出",
5, 25, 5);
playSound(p);
continue;
}
// 阶段1:3分钟提醒
if (remain <= 180 && stage < 1) {
lastStage.put(uuid, 1);
p.sendMessage(
"§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c"
+ fmtSec(idle)
+ "§f,还剩 §c"
+ fmtSec(remain)
+ "§f踢出");
p.sendTitle(
"§e§l挂机了 " + fmtSec(idle),
"§f还剩 " + fmtSec(remain) + " 踢出",
10, 60, 20);
for (int i = 0; i < 3; i++) {
final int delay = i * 10;
Bukkit.getScheduler().runTaskLater(
plugin, () -> {
if (p.isOnline()) playSound(p);
}, delay);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
private void playSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
private String fmtSec(long sec) {
if (sec < 60) return sec + "秒";
long m = sec / 60;
long s = sec % 60;
return m + "分" + (s > 0 ? s + "秒" : "");
}
}
@@ -1,146 +1,146 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AccountRequestManager {
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
public static class Request {
public final int id;
public final String requester;
public final String target;
public final String ip;
public final long time;
public String status;
public String denyReason;
public Request(int id, String requester, String target, String ip) {
this.id = id;
this.requester = requester;
this.target = target;
this.ip = ip;
this.time = System.currentTimeMillis();
this.status = "PENDING";
this.denyReason = "";
}
}
private final Main plugin;
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
private int nextId = 1;
private ApprovalMode mode = ApprovalMode.MANUAL;
private int autoDelayMinutes = 60;
public AccountRequestManager(Main plugin) {
this.plugin = plugin;
}
public ApprovalMode getMode() { return mode; }
public void setMode(String m) {
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
}
public void setAutoDelayMinutes(int min) {
this.autoDelayMinutes = min;
}
public Request createRequest(String requester, String target, String ip) {
int id = nextId++;
Request req = new Request(id, requester, target, ip);
requests.put(id, req);
pendingByPlayer.put(requester, id);
if (mode == ApprovalMode.AUTO) {
scheduleAutoApprove(id);
} else if (mode == ApprovalMode.HYBRID) {
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
if (ip != null && ip.equals(targetIp)) {
scheduleAutoApprove(id);
}
}
return req;
}
private void scheduleAutoApprove(int id) {
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
Request req = requests.get(id);
if (req == null) return;
String requesterName = req.requester;
String targetName = req.target;
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Request r = requests.get(id);
if (r != null && "PENDING".equals(r.status)) {
approveRequest(id);
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
Player requester = Bukkit.getPlayer(requesterName);
if (requester != null && requester.isOnline()) {
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
}
}
}, delayTicks);
}
public boolean approveRequest(int id) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "APPROVED";
plugin.getDb().deleteUser(req.target);
pendingByPlayer.remove(req.requester);
// 清背包+传送出生点+踢出
Player target = Bukkit.getPlayer(req.target);
if (target != null && target.isOnline()) {
target.getInventory().clear();
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
target.teleport(target.getWorld().getSpawnLocation());
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
}
return true;
}
public boolean denyRequest(int id, String reason) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "DENIED";
req.denyReason = reason;
pendingByPlayer.remove(req.requester);
return true;
}
public List<Request> getPendingRequests() {
List<Request> list = new ArrayList<>();
for (Request r : requests.values()) {
if ("PENDING".equals(r.status)) list.add(r);
}
return list;
}
public Request getRequest(int id) {
return requests.get(id);
}
public boolean hasPending(String player) {
return pendingByPlayer.containsKey(player);
}
public void cancelPending(String player) {
Integer id = pendingByPlayer.remove(player);
if (id != null) {
Request r = requests.get(id);
if (r != null) r.status = "CANCELLED";
}
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AccountRequestManager {
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
public static class Request {
public final int id;
public final String requester;
public final String target;
public final String ip;
public final long time;
public String status;
public String denyReason;
public Request(int id, String requester, String target, String ip) {
this.id = id;
this.requester = requester;
this.target = target;
this.ip = ip;
this.time = System.currentTimeMillis();
this.status = "PENDING";
this.denyReason = "";
}
}
private final Main plugin;
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
private int nextId = 1;
private ApprovalMode mode = ApprovalMode.MANUAL;
private int autoDelayMinutes = 60;
public AccountRequestManager(Main plugin) {
this.plugin = plugin;
}
public ApprovalMode getMode() { return mode; }
public void setMode(String m) {
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
}
public void setAutoDelayMinutes(int min) {
this.autoDelayMinutes = min;
}
public Request createRequest(String requester, String target, String ip) {
int id = nextId++;
Request req = new Request(id, requester, target, ip);
requests.put(id, req);
pendingByPlayer.put(requester, id);
if (mode == ApprovalMode.AUTO) {
scheduleAutoApprove(id);
} else if (mode == ApprovalMode.HYBRID) {
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
if (ip != null && ip.equals(targetIp)) {
scheduleAutoApprove(id);
}
}
return req;
}
private void scheduleAutoApprove(int id) {
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
Request req = requests.get(id);
if (req == null) return;
String requesterName = req.requester;
String targetName = req.target;
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
Bukkit.getScheduler().runTaskLater(plugin, () -> {
Request r = requests.get(id);
if (r != null && "PENDING".equals(r.status)) {
approveRequest(id);
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
Player requester = Bukkit.getPlayer(requesterName);
if (requester != null && requester.isOnline()) {
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
}
}
}, delayTicks);
}
public boolean approveRequest(int id) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "APPROVED";
plugin.getDb().deleteUser(req.target);
pendingByPlayer.remove(req.requester);
// 清背包+传送出生点+踢出
Player target = Bukkit.getPlayer(req.target);
if (target != null && target.isOnline()) {
target.getInventory().clear();
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
target.teleport(target.getWorld().getSpawnLocation());
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
}
return true;
}
public boolean denyRequest(int id, String reason) {
Request req = requests.get(id);
if (req == null || !"PENDING".equals(req.status)) return false;
req.status = "DENIED";
req.denyReason = reason;
pendingByPlayer.remove(req.requester);
return true;
}
public List<Request> getPendingRequests() {
List<Request> list = new ArrayList<>();
for (Request r : requests.values()) {
if ("PENDING".equals(r.status)) list.add(r);
}
return list;
}
public Request getRequest(int id) {
return requests.get(id);
}
public boolean hasPending(String player) {
return pendingByPlayer.containsKey(player);
}
public void cancelPending(String player) {
Integer id = pendingByPlayer.remove(player);
if (id != null) {
Request r = requests.get(id);
if (r != null) r.status = "CANCELLED";
}
}
}
+107 -4
View File
@@ -182,6 +182,23 @@ public class BondManager {
} catch (SQLException e) { return 0; }
}
/** 该玩家是否在 bond 表中有真实记录 */
public boolean hasBondAccount(String player) {
try {
PreparedStatement ps = db.prepareStatement(
"SELECT 1 FROM bonds "
+ "WHERE player_name=? "
+ "AND amount > 0");
ps.setString(1, player);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next();
rs.close(); ps.close();
return exists;
} catch (SQLException e) {
return false;
}
}
// ======================== 增加债券 ========================
public int addBonds(String player, int amount) {
@@ -272,6 +289,53 @@ public class BondManager {
int balanceBefore,
int balanceAfter) {
try {
// 合并模式:shop_buy 30秒内同商品合并
if ("shop_buy".equals(type)
&& reason != null
&& !reason.isEmpty()) {
long mergeWindow =
System.currentTimeMillis() - 30000L;
PreparedStatement chk =
db.prepareStatement(
"SELECT id, amount "
+ "FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND type=? "
+ "AND reason=? "
+ "AND time > ? "
+ "ORDER BY time DESC "
+ "LIMIT 1");
chk.setString(1, player);
chk.setString(2, type);
chk.setString(3, reason);
chk.setLong(4, mergeWindow);
ResultSet rs = chk.executeQuery();
if (rs.next()) {
int existId = rs.getInt("id");
int existAmt = rs.getInt("amount");
rs.close();
chk.close();
PreparedStatement upd =
db.prepareStatement(
"UPDATE bond_transaction "
+ "SET amount=?, "
+ "balance_after=?, "
+ "time=? "
+ "WHERE id=?");
upd.setInt(1, existAmt + amount);
upd.setInt(2, balanceAfter);
upd.setLong(3,
System.currentTimeMillis());
upd.setInt(4, existId);
upd.executeUpdate();
upd.close();
return;
}
rs.close();
chk.close();
}
// 原始插入
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bond_transaction "
+ "(player_name, type, amount,"
@@ -283,16 +347,14 @@ public class BondManager {
ps.setString(2, type);
ps.setInt(3, amount);
ps.setString(4,
targetPlayer != null
? targetPlayer : "");
targetPlayer != null ? targetPlayer : "");
ps.setString(5,
operator != null ? operator : "");
ps.setString(6,
reason != null ? reason : "");
ps.setInt(7, balanceBefore);
ps.setInt(8, balanceAfter);
ps.setLong(9,
System.currentTimeMillis());
ps.setLong(9, System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
@@ -314,6 +376,47 @@ public class BondManager {
type, targetPlayer, operator, reason);
}
public boolean checkoutCart(String player,
String[] itemNames,
int[] subtotals) {
int total = 0;
for (int s : subtotals) total += s;
if (isFrozen(player)) return false;
int before = getBonds(player);
if (before < total) return false;
int after = before - total;
try {
PreparedStatement ps = db.prepareStatement(
"INSERT INTO bonds "
+ "(player_name,amount,status) "
+ "VALUES (?,?,'normal') "
+ "ON CONFLICT(player_name) "
+ "DO UPDATE SET amount=?");
ps.setString(1, player);
ps.setInt(2, after);
ps.setInt(3, after);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
int running = before;
for (int i = 0; i < itemNames.length; i++) {
int next = running - subtotals[i];
logTransaction(player, "shop_buy", subtotals[i],
"", "商店系统",
"购物车购买-" + itemNames[i],
running, next);
running = next;
}
logLegacy(player, -total);
return true;
}
public boolean setBonds(String player, int amount) {
ensureAccount(player);
+79 -57
View File
@@ -9,12 +9,7 @@ import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
public class BondPrinter {
@@ -38,9 +33,11 @@ public class BondPrinter {
TYPE_NAMES.put("transfer_in", "转入");
TYPE_NAMES.put("redeem", "CDK兑换");
TYPE_NAMES.put("shop_buy", "商城购买");
TYPE_NAMES.put("shop_sell", "商城出售"); // ← 补上
TYPE_NAMES.put("daily_sign", "签到奖励");
TYPE_NAMES.put("freeze", "冻结");
TYPE_NAMES.put("unfreeze", "解冻");
}
public BondPrinter(Main plugin) {
@@ -151,8 +148,7 @@ public class BondPrinter {
.getAllPlayerNames();
if (players.isEmpty()) {
if (sender != null) {
sender.sendMessage(
"§c没有流水数据");
sender.sendMessage("§c没有流水数据");
}
return;
}
@@ -162,59 +158,84 @@ public class BondPrinter {
+ ".xls";
File file = new File(outputDir, fileName);
// ===== 收集所有流水 =====
// key = 时间戳+玩家+金额,用于去重
// value = 一行数据
List<String[]> allRows = new ArrayList<>();
Set<String> seenKeys = new HashSet<>();
for (String player : players) {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(player);
for (Map<String, Object> tx : txs) {
int txId = ((Number) tx.get("id"))
.intValue();
String dedupKey =
String.valueOf(txId);
if (!seenKeys.add(dedupKey)) {
continue;
}
String timeStr = txTime(tx);
long timeMs = 0;
Object t = tx.get("time");
if (t != null) {
timeMs =
((Number) t).longValue();
}
allRows.add(new String[]{
player,
timeStr,
txTypeName(tx),
String.valueOf(
txInt(tx, "amount")),
txStr(tx, "target_player"),
txStr(tx, "operator"),
txStr(tx, "reason"),
String.valueOf(
txInt(tx,
"balance_before")),
String.valueOf(
txInt(tx,
"balance_after")),
// 额外存时间戳用于排序
String.valueOf(timeMs)
});
}
}
// ===== 按时间戳排序 =====
allRows.sort((a, b) -> {
long ta = Long.parseLong(
a[a.length - 1]);
long tb = Long.parseLong(
b[b.length - 1]);
return Long.compare(ta, tb);
});
// ===== 写入文件 =====
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(file),
StandardCharsets.UTF_8));
try {
writeXmlHeader(pw, "全服流水");
writeXmlRow(pw,
new String[]{
"玩家", "时间",
"类型", "金额",
"对象", "操作者",
"理由",
"操作前余额",
"操作后余额"});
writeXmlRow(pw, new String[]{
"玩家", "时间", "类型",
"金额", "对象", "操作者",
"理由", "操作前余额",
"操作后余额"
});
Set<String> written = new HashSet<>();
int totalRows = 0;
for (String player : players) {
List<Map<String, Object>> txs =
plugin.getBonds()
.getAllTransactions(
player);
for (Map<String, Object> tx : txs) {
int txId = ((Number) tx
.get("id"))
.intValue();
if (!written.add(
String.valueOf(
txId))) {
continue;
}
writeXmlRow(pw, new String[]{
player,
txTime(tx),
txTypeName(tx),
String.valueOf(
txInt(tx,
"amount")),
txStr(tx,
"target_player"),
txStr(tx, "operator"),
txStr(tx, "reason"),
String.valueOf(
txInt(tx,
"balance_before")),
String.valueOf(
txInt(tx,
"balance_after"))
});
totalRows++;
}
for (String[] row : allRows) {
// 去掉最后的时间戳列
String[] cells =
new String[row.length - 1];
System.arraycopy(row, 0,
cells, 0, cells.length);
writeXmlRow(pw, cells);
}
writeXmlFooter(pw);
@@ -222,14 +243,15 @@ public class BondPrinter {
if (sender != null) {
sender.sendMessage(
"§a[打印] §f全服流水"
+ " §a已导出");
"§a[打印] §f全服流水 §a已导出");
sender.sendMessage("§7文件: §f"
+ fileName);
sender.sendMessage("§7共 §f"
+ players.size()
+ " 名玩家,§f"
+ totalRows + " §7条");
+ allRows.size() + " §7条");
sender.sendMessage(
"§7按时间从早到晚排序");
sender.sendMessage(
"§724小时后自动删除");
}
+107 -365
View File
@@ -2,13 +2,13 @@ package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Scoreboard;
import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.charset.StandardCharsets;
public class CDKManager {
private final Main plugin;
@@ -41,198 +41,18 @@ public class CDKManager {
st.close();
} catch (Exception e) {
throw new RuntimeException(
"[CDK] DB init failed: " + e.getMessage(), e);
"[CDK] DB init failed: "
+ e.getMessage(), e);
}
}
public void loadCDKsFromDir() {
File dir = new File(
plugin.getDataFolder(), "cdk");
if (!dir.exists()) {
dir.mkdirs();
plugin.getLogger().info(
"[CDK] 已创建 cdk/ 目录");
return;
}
File[] files = dir.listFiles(
(d, n) -> n.endsWith(".txt"));
if (files == null || files.length == 0) {
plugin.getLogger().warning(
"[CDK] cdk/ 目录下"
+ "无 .txt 文件");
return;
}
int count = 0;
for (File f : files) {
int loaded = loadCDKFile(f);
plugin.getLogger().info(
"[CDK] 文件: " + f.getName()
+ " 加载 " + loaded + " 条");
count += loaded;
}
plugin.getLogger().info(
"[CDK] 共导入 " + count + " 条");
}
// ===== CDK兑换(供SDF1调用) =====
/** 单文件加载,返回成功数 */
private int loadCDKFile(File f) {
int count = 0;
try {
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8));
String line;
int lineNum = 0;
while ((line = br.readLine()) != null) {
lineNum++;
line = line.trim();
if (line.isEmpty()) continue;
if (line.startsWith("#")) continue;
if (line.startsWith("//")) continue;
// 支持格式:
// CODE 100
// CODE,100
// CODE 100 bond
// CODE,100,bond
String[] parts = line
.split("[,\\s]+");
if (parts.length < 2) {
plugin.getLogger().warning(
"[CDK] " + f.getName()
+ " L" + lineNum
+ " 格式错误: "
+ line);
continue;
}
String code = parts[0].trim();
if (code.isEmpty()) continue;
int amount;
try {
amount = Integer.parseInt(
parts[1].trim());
} catch (NumberFormatException e) {
plugin.getLogger().warning(
"[CDK] " + f.getName()
+ " L" + lineNum
+ " 金额格式错误: "
+ parts[1]);
continue;
}
String type = parts.length >= 3
? parts[2].trim() : "bond";
boolean ok = addCDK(code, amount,
type);
if (ok) {
count++;
} else {
plugin.getLogger().warning(
"[CDK] " + f.getName()
+ " L" + lineNum
+ " 重复跳过: "
+ code);
}
}
br.close();
} catch (Exception e) {
plugin.getLogger().warning(
"[CDK] 读取失败: " + f.getName()
+ " " + e.getMessage());
}
return count;
}
public int importFromFile(String fileName) {
File dir = new File(
plugin.getDataFolder(), "cdk");
if (!dir.exists()) {
dir.mkdirs();
}
// 先在 cdk/ 目录下找
File f = new File(dir, fileName);
if (!f.exists()) {
// 再在插件根目录找
f = new File(
plugin.getDataFolder(), fileName);
}
if (!f.exists()) {
plugin.getLogger().warning(
"[CDK] 文件不存在: "
+ fileName);
return -1;
}
plugin.getLogger().info(
"[CDK] 开始导入: "
+ f.getAbsolutePath()
+ " 大小:" + f.length()
+ " bytes");
int count = loadCDKFile(f);
plugin.getLogger().info(
"[CDK] 导入完成: " + count + " 条");
return count;
}
public int importFromSdf1(String fileName) {
int count = 0;
Scoreboard board = Bukkit.getScoreboardManager()
.getMainScoreboard();
// 尝试从sdf1的计分板读取口令
// 这部分依赖sdf1插件的配置
return count;
}
private boolean addCDK(String code, int amount,
String type) {
try {
// 先检查是否已存在
PreparedStatement check =
db.prepareStatement(
"SELECT code FROM cdk "
+ "WHERE code=?");
check.setString(1, code);
ResultSet rs = check.executeQuery();
if (rs.next()) {
rs.close();
check.close();
return false;
}
rs.close();
check.close();
PreparedStatement ps =
db.prepareStatement(
"INSERT INTO cdk "
+ "(code, amount, type,"
+ " created_time)"
+ " VALUES (?,?,?,?)");
ps.setString(1, code);
ps.setInt(2, amount);
ps.setString(3,
type != null ? type : "bond");
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
return true;
} catch (SQLException e) {
plugin.getLogger().warning(
"[CDK] addCDK失败: " + code
+ " " + e.getMessage());
return false;
}
}
// ===== 替换整个 redeem 方法 =====
/**
* 兑换CDK,返回结果字符串
* @return "success:金额:余额前:余额后"
* 或 "fail:原因"
*/
public String redeem(String code, String player) {
try {
PreparedStatement ps = db.prepareStatement(
@@ -242,45 +62,54 @@ public class CDKManager {
ResultSet rs = ps.executeQuery();
if (!rs.next()) {
rs.close(); ps.close();
return null;
return "fail:not_found";
}
int amount = rs.getInt("amount");
String type = rs.getString("type");
int used = rs.getInt("used");
rs.close(); ps.close();
if (used == 1) return "该兑换码已被使用";
if (used == 1)
return "fail:already_used";
// [新增] 冻结检查
if (plugin.getBonds().isFrozen(player)) {
return "账户已被冻结,无法兑换";
}
if (plugin.getBonds()
.isFrozen(player))
return "fail:frozen";
PreparedStatement up = db.prepareStatement(
"UPDATE cdk SET used=1,"
+ "used_by=?,used_time=? "
+ "WHERE code=?");
up.setString(1, player);
up.setLong(2, System.currentTimeMillis());
up.setLong(2,
System.currentTimeMillis());
up.setString(3, code);
up.executeUpdate(); up.close();
if ("bond".equals(type)) {
// [修改] 使用带流水的重载方法
plugin.getBonds().addBonds(player, amount,
BondManager.TX_REDEEM, "",
"system", "CDK兑换: " + code);
return "兑换成功!获得 " + amount + " 债券";
int bef = plugin.getBonds()
.getBonds(player);
plugin.getBonds().addBonds(player,
amount,
BondManager.TX_REDEEM,
"", "CDK系统",
"CDK兑换: " + code);
int aft = plugin.getBonds()
.getBonds(player);
return "success:" + amount
+ ":" + bef + ":" + aft;
}
return null;
return "fail:unknown_type";
} catch (SQLException e) {
return "兑换失败";
return "fail:" + e.getMessage();
}
}
// ===== 聊天输入系统(主插件转发调用)=====
// ===== 聊天输入系统 =====
public void requestInput(Player p, String type, String context) {
public void requestInput(Player p,
String type,
String context) {
CDKInput input = new CDKInput();
input.type = type;
input.context = context;
@@ -289,129 +118,133 @@ public class CDKManager {
}
public boolean isListening(String playerName) {
return pendingInputs.containsKey(playerName);
return pendingInputs
.containsKey(playerName);
}
public void onChat(Player p, String msg) {
CDKInput input = pendingInputs.remove(p.getName());
CDKInput input = pendingInputs
.remove(p.getName());
if (input == null) return;
if (System.currentTimeMillis() - input.time > 60000) {
p.sendMessage("§c输入超时,请重新操作");
if (System.currentTimeMillis()
- input.time > 60000) {
p.sendMessage("§c输入超时");
return;
}
if ("cdk".equals(input.type)) {
String result = redeem(msg.trim(), p.getName());
p.sendMessage(result != null
? "§e§l[CDK] §f" + result
: "§c§l[CDK] §f无效的兑换码");
String result = redeem(
msg.trim(), p.getName());
if (result != null
&& result.startsWith("success:")) {
String[] parts = result.split(":");
int amt = Integer.parseInt(parts[1]);
int bef = Integer.parseInt(parts[2]);
int aft = Integer.parseInt(parts[3]);
p.sendMessage("§e§l[CDK] §f兑换成功!"
+ "获得 §c" + amt + "§f债券");
p.sendMessage("§7余额: §e" + bef
+ " §7-> §a" + aft);
} else {
p.sendMessage("§c§l[CDK] §f"
+ (result != null
? result.replace("fail:", "")
: "无效的兑换码"));
}
return;
}
if ("econ".equals(input.type)) {
handleEconInput(p, msg.trim(), input.context);
handleEconInput(p, msg.trim(),
input.context);
return;
}
if ("bond".equals(input.type)) {
handleBondInput(p, msg.trim(), input.context);
handleBondInput(p, msg.trim(),
input.context);
}
}
private void handleEconInput(Player admin, String msg, String target) {
net.milkbowl.vault.economy.Economy econ = null;
private void handleEconInput(Player admin,
String msg,
String target) {
net.milkbowl.vault.economy.Economy econ
= null;
try {
var reg = plugin.getServer().getServicesManager()
.getRegistration(net.milkbowl.vault.economy.Economy.class);
if (reg != null) econ = reg.getProvider();
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault
.economy
.Economy.class);
if (reg != null)
econ = reg.getProvider();
} catch (Exception ignored) {}
if (econ == null) {
admin.sendMessage("§c经济系统不可用");
return;
}
if (!msg.startsWith("+") && !msg.startsWith("-")) {
admin.sendMessage("§c格式: §a+100 §7给钱 / §c-50 §7扣钱");
if (!msg.startsWith("+")
&& !msg.startsWith("-")) {
admin.sendMessage("§c格式: §a+100"
+ " §7或 §c-50");
return;
}
try {
String numStr = msg.substring(1).trim();
double amount = Double.parseDouble(numStr);
String numStr =
msg.substring(1).trim();
double amount =
Double.parseDouble(numStr);
if (amount <= 0) {
admin.sendMessage("§c金额必须大于0");
admin.sendMessage("§c金额必须>0");
return;
}
if (msg.startsWith("-")) {
Player tp = plugin.getServer().getPlayerExact(target);
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp == null) {
admin.sendMessage("§c" + target + " 不在线");
admin.sendMessage("§c" + target
+ " 不在线");
return;
}
if (!econ.has(tp, amount)) {
admin.sendMessage("§c" + target + " 余额不足");
admin.sendMessage("§c余额不足");
return;
}
double oldBal = econ.getBalance(tp);
econ.withdrawPlayer(tp, amount);
double newBal = econ.getBalance(tp);
admin.sendMessage("§7============== §c余额扣款 §7==============");
admin.sendMessage("§7目标: §e" + target);
admin.sendMessage("§c扣款: §e-$" + String.format("%.2f", amount));
admin.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §c" + String.format("%.2f", newBal));
admin.sendMessage("§7============== §c余额扣款 §7==============");
tp.sendMessage("§7============== §c余额扣款 §7==============");
tp.sendMessage("§c扣款: §e-$" + String.format("%.2f", amount));
tp.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §c" + String.format("%.2f", newBal));
tp.sendMessage("§7操作者: §e" + admin.getName());
tp.sendMessage("§7============== §c余额扣款 §7==============");
admin.sendMessage("§a扣款成功");
} else {
double oldBal = econ.getBalance(target);
econ.depositPlayer(target, amount);
double newBal = econ.getBalance(target);
Player tp = plugin.getServer().getPlayerExact(target);
admin.sendMessage("§7============== §6余额转账 §7==============");
admin.sendMessage("§7目标: §e" + target);
admin.sendMessage("§a到账: §e$" + String.format("%.2f", amount));
admin.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §a" + String.format("%.2f", newBal));
admin.sendMessage("§7============== §6余额转账 §7==============");
if (tp != null) {
tp.sendMessage("§7============== §a收到转账 §7==============");
tp.sendMessage("§a到账: §e$" + String.format("%.2f", amount));
tp.sendMessage("§7余额: §e" + String.format("%.2f", oldBal) + " → §a" + String.format("%.2f", newBal));
tp.sendMessage("§7来自: §e" + admin.getName());
tp.sendMessage("§7============== §a收到转账 §7==============");
}
admin.sendMessage("§a到账成功");
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误,请输入 §a+数字 §7或 §c-数字");
admin.sendMessage("§c格式错误");
}
}
// ===== 替换整个 handleBondInput 方法 =====
private void handleBondInput(Player admin,
String msg,
String target) {
if (!msg.startsWith("+")
&& !msg.startsWith("-")) {
admin.sendMessage(
"§c格式: §a+100 §7给债券"
+ " / §c-50 §7扣债券");
admin.sendMessage(
"§7可附加理由: §a+100 活动奖励");
admin.sendMessage("§c格式: §a+100"
+ " §7或 §c-50");
return;
}
try {
String sign = msg.substring(0, 1);
String rest = msg.substring(1).trim();
// 拆分金额和理由
String[] parts = rest.split("\\s+", 2);
String rest =
msg.substring(1).trim();
String[] parts =
rest.split("\\s+", 2);
int amount = Integer.parseInt(parts[0]);
String reason = parts.length >= 2
? parts[1].trim()
: "管理员手动调整";
if (amount <= 0) {
admin.sendMessage("§c金额必须大于0");
admin.sendMessage("§c金额必须>0");
return;
}
String opName = admin.getName();
@@ -422,123 +255,32 @@ public class CDKManager {
if ("-".equals(sign)) {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c" + target
+ " 账户已被冻结");
admin.sendMessage("§c账户冻结");
return;
}
int oldBal = plugin.getBonds()
.getBonds(target);
if (plugin.getBonds().deductBonds(
target, amount, txType,
"", opName, reason)) {
int newBal = plugin.getBonds()
.getBonds(target);
admin.sendMessage(
"§7============== §c"
+ "债券扣款 §7"
+ "=============");
admin.sendMessage(
"§7目标: §e" + target);
admin.sendMessage("§c扣款: §c-"
+ amount + " §6枚");
admin.sendMessage("§7理由: §f"
+ reason);
admin.sendMessage("§7债券: §e"
+ oldBal + " → §c"
+ newBal);
admin.sendMessage(
"§7============== §c"
+ "债券扣款 §7"
+ "=============");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null) {
tp.sendMessage(
"§7============== §c"
+ "债券扣款 §7"
+ "=============");
tp.sendMessage("§c扣款: §c-"
+ amount + " §6枚");
tp.sendMessage("§7理由: §f"
+ reason);
tp.sendMessage("§7债券: §e"
+ oldBal + " → §c"
+ newBal);
tp.sendMessage("§7操作者: §e"
+ opName);
tp.sendMessage(
"§7============== §c"
+ "债券扣款 §7"
+ "=============");
}
admin.sendMessage("§a扣除成功");
} else {
admin.sendMessage("§c" + target
+ " 债券不足(当前: "
+ oldBal + ")");
admin.sendMessage("§c债券不足");
}
} else {
if (plugin.getBonds()
.isFrozen(target)) {
admin.sendMessage("§c" + target
+ " 账户已被冻结");
admin.sendMessage("§c账户冻结");
return;
}
int oldBal = plugin.getBonds()
.getBonds(target);
plugin.getBonds().addBonds(target,
amount, txType,
"", opName, reason);
int newBal = plugin.getBonds()
.getBonds(target);
admin.sendMessage(
"§7============== §6"
+ "债券转账 §7"
+ "=============");
admin.sendMessage(
"§7目标: §e" + target);
admin.sendMessage("§a到账: §a+"
+ amount + " §6枚");
admin.sendMessage("§7理由: §f"
+ reason);
admin.sendMessage("§7债券: §e"
+ oldBal + " → §a"
+ newBal);
admin.sendMessage(
"§7============== §6"
+ "债券转账 §7"
+ "=============");
Player tp = plugin.getServer()
.getPlayerExact(target);
if (tp != null) {
tp.sendMessage(
"§7============== §a"
+ "收到债券 §7"
+ "=============");
tp.sendMessage("§a到账: §a+"
+ amount + " §6枚");
tp.sendMessage("§7理由: §f"
+ reason);
tp.sendMessage("§7债券: §e"
+ oldBal + " → §a"
+ newBal);
tp.sendMessage("§7来自: §e"
+ opName);
tp.sendMessage(
"§7============== §a"
+ "收到债券 §7"
+ "=============");
}
admin.sendMessage("§a发放成功");
}
} catch (NumberFormatException e) {
admin.sendMessage("§c格式错误,请输入 "
+ "§a+数字 §7或 §c-数字");
admin.sendMessage("§c格式错误");
}
}
public void cancelInput(String player) {
pendingInputs.remove(player);
}
+114 -88
View File
@@ -26,6 +26,7 @@ public class CheckInManager {
return today.equals(last);
}
// ===== 签到 =====
// ===== 签到 =====
public String checkIn(Player p) {
String name = p.getName();
@@ -53,7 +54,8 @@ public class CheckInManager {
java.util.Date todayD =
new SimpleDateFormat("yyyy-MM-dd")
.parse(today);
long diff = todayD.getTime() - lastD.getTime();
long diff = todayD.getTime()
- lastD.getTime();
if (diff > 86400000L * 2) {
streak = 0;
} else if (diff > 86400000L) {
@@ -71,35 +73,49 @@ public class CheckInManager {
else if (streak >= 14) multi = 1.3;
else if (streak >= 7) multi = 1.1;
int basePoints = plugin.getConfig2().checkinPoints;
int basePoints =
plugin.getConfig2().checkinPoints;
int points = (int) (basePoints * multi);
plugin.getDb().addPoints(name, points);
plugin.getDb().setField(name, "checkin_streak", streak);
plugin.getDb().setField(name, "last_checkin_date", today);
plugin.getDb().setField(name,
"checkin_streak", streak);
plugin.getDb().setField(name,
"last_checkin_date", today);
Object totalObj = plugin.getDb()
.getField(name, "total_checkin_days");
int total = totalObj != null
? ((Number) totalObj).intValue()
: 0;
plugin.getDb().setField(name, "total_checkin_days",
total + 1);
plugin.getDb().setField(name,
"total_checkin_days", total + 1);
int bondReward = 1;
if (streak >= 21) bondReward = 3;
else if (streak >= 14) bondReward = 2;
else if (streak >= 7) bondReward = 2;
plugin.getBonds().addBonds(name, bondReward);
// ★ 债券盲盒奖励(6参数版,写入流水)★
int bondMin =
plugin.getConfig2().checkinBondMin;
int bondMax =
plugin.getConfig2().checkinBondMax;
if (bondMin <= 0) bondMin = 1;
if (bondMax < bondMin) bondMax = bondMin;
int bondReward = bondMin
+ new java.util.Random()
.nextInt(bondMax - bondMin + 1);
plugin.getBonds().addBonds(name, bondReward,
"daily_sign", "", "签到系统",
"每日签到连续" + streak + "天");
return plugin.getConfig2().msg("checkin_success")
.replace("{points}", String.valueOf(points))
.replace("{streak}", String.valueOf(streak))
.replace("{multi}", String.valueOf(multi))
return plugin.getConfig2()
.msg("checkin_success")
.replace("{points}",
String.valueOf(points))
.replace("{streak}",
String.valueOf(streak))
.replace("{multi}",
String.valueOf(multi))
+ " §e+" + bondReward + "债券";
}
// ===== 获取连续天数 =====
public int getStreak(String name) {
Object val = plugin.getDb()
@@ -111,7 +127,16 @@ public class CheckInManager {
}
return 0;
}
// 标准化日期
private String normalizeDate(String input) {
String s = input.trim().replace("/", "-");
if (s.matches("\\d{8}")) {
return s.substring(0, 4) + "-"
+ s.substring(4, 6) + "-"
+ s.substring(6, 8);
}
return s;
}
// ===== 补签入口 =====
public String backCheckIn(
Player p, String dateStr) {
@@ -130,14 +155,12 @@ public class CheckInManager {
} catch (Exception ex) {
return "§c无效日期: " + dateStr;
}
String reformat =
sdf.format(target);
String reformat = sdf.format(target);
if (!reformat.equals(normalized)) {
return "§c无效日期: " + dateStr;
}
String today = sdf.format(new Date());
if (normalized.compareTo(today) >= 0) {
return "§c今天不需要补签";
}
@@ -146,19 +169,16 @@ public class CheckInManager {
if (normalized.compareTo(
threeDaysAgo) < 0) {
return "§c只能补签3天内"
+ "(最多到 "
+ threeDaysAgo + ")";
+ "(最多到 " + threeDaysAgo + ")";
}
Object val = plugin.getDb()
.getField(name,
"backcheck_dates");
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
boolean alreadyBacked = false;
if (!existing.isEmpty()) {
for (String d
: existing.split(",")) {
for (String d : existing.split(",")) {
if (normalizeDate(d.trim())
.equals(normalized)) {
alreadyBacked = true;
@@ -167,71 +187,94 @@ public class CheckInManager {
}
}
if (alreadyBacked) {
return "§c该日期已补签: "
+ normalized;
return "§c该日期已补签: " + normalized;
}
Object normalCheck =
plugin.getDb().getField(
name,
"last_checkin_date");
String lastCheckin = normalCheck
!= null
? normalCheck.toString()
: "";
name, "last_checkin_date");
String lastCheckin = normalCheck != null
? normalCheck.toString() : "";
if (lastCheckin.equals(normalized)) {
return "§c该日期已签到: "
+ normalized;
return "§c该日期已签到: " + normalized;
}
if (!plugin.getDb()
.deductPoints(name, 10)) {
return "§c积分不足,补签需 10 积分";
}
// ★ 补签消耗:3债券 + 10积分
// 积分不够时:用 9债券(3倍)代替 ★
int bondCost = 3;
int pointCost = 10;
int bondAltCost = 9; // 3倍债券替代
net.milkbowl.vault.economy
.Economy econ =
plugin.getEconomy();
if (econ != null) {
double bal = econ.getBalance(p);
if (bal < 10.0) {
plugin.getDb()
.addPoints(name, 10);
int mult = plugin.getConfig2()
.backCheckPointMultiplier;
int needed = 10 * mult;
p.sendMessage("§e§l[补签] "
+ "§f经济不足($"
+ String.format(
"%.2f", bal)
+ " < $10)");
p.sendMessage("§7是否用 §a§l"
+ needed + "积分 "
+ "§7代替 10经济?");
p.sendMessage("§7输入 §a1 "
+ "§7同意,"
+ "§c其他 §7取消");
return "NEED_CONFIRM:"
+ normalized
+ ":" + needed;
int myBonds = plugin.getBonds()
.getBonds(name);
Object ptsObj = plugin.getDb()
.getField(name, "points");
int myPoints = ptsObj != null
? ((Number) ptsObj).intValue()
: 0;
// 方案A:3债券 + 10积分
boolean canA = myBonds >= bondCost
&& myPoints >= pointCost;
// 方案B:9债券(积分不够时)
boolean canB = myBonds >= bondAltCost
&& myPoints < pointCost;
if (!canA && !canB) {
if (myPoints < pointCost
&& myBonds < bondAltCost) {
return "§c补签费用不足"
+ "(需3债券+10积分,"
+ "或9债券代替)"
+ " 当前: "
+ myBonds + "债券 "
+ myPoints + "积分";
}
econ.withdrawPlayer(p, 10);
if (myBonds < bondCost) {
return "§c债券不足(需"
+ bondCost + "枚)";
}
return "§c积分不足(需"
+ pointCost + "分)";
}
// 执行扣款
if (canA) {
// 方案A:3债券 + 10积分
plugin.getBonds().deductBonds(
name, bondCost,
"admin_deduct", "",
"补签系统", "补签扣款");
plugin.getDb().deductPoints(
name, pointCost);
} else {
// 方案B:9债券代替
plugin.getBonds().deductBonds(
name, bondAltCost,
"admin_deduct", "",
"补签系统", "补签扣款(债券替代)");
}
doBackCheckIn(p, normalized);
String costMsg = canA
? bondCost + "债券+" + pointCost + "积分"
: bondAltCost + "债券(替代)";
return "§a补签成功!日期: "
+ normalized
+ " §7(扣除10积分+10经济)";
+ " §7消耗: " + costMsg;
}
// ===== 执行补签 =====
// ===== 执行补签 =====
public void doBackCheckIn(
Player p, String dateStr) {
String name = p.getName();
Object val = plugin.getDb()
.getField(name,
"backcheck_dates");
.getField(name, "backcheck_dates");
String existing = val != null
? val.toString() : "";
String updated = existing.isEmpty()
@@ -241,25 +284,8 @@ public class CheckInManager {
"backcheck_dates", updated);
int streak = getStreak(name);
plugin.getDb().setField(name,
"checkin_streak",
streak + 1);
int bondReward = 1;
if (streak >= 21) bondReward = 3;
else if (streak >= 14) bondReward = 2;
else if (streak >= 7) bondReward = 2;
plugin.getBonds().addBonds(name, bondReward);
}
// ===== 标准化日期 =====
private String normalizeDate(
String input) {
String s = input.trim()
.replace("/", "-");
if (s.matches("\\d{8}")) {
return s.substring(0, 4) + "-"
+ s.substring(4, 6) + "-"
+ s.substring(6, 8);
}
return s;
"checkin_streak", streak + 1);
// ★ 补签不发放奖励,只记录
}
// ===== 获取N天前日期 =====
+165 -311
View File
@@ -7,10 +7,10 @@ import java.util.*;
public class ConfigManager {
private final File dataFolder;
private final Map<String, String> messages =
new LinkedHashMap<>();
private final Map<String, String> smtpSettings =
new LinkedHashMap<>();
private final Map<String, String> messages
= new LinkedHashMap<>();
private final Map<String, String> smtpSettings
= new LinkedHashMap<>();
// ===== 公共字段 =====
public String adminTag = "admin";
@@ -30,7 +30,6 @@ public class ConfigManager {
public int httpPort = 8080;
public String garbageTag = "admin";
// 签到经济奖励
public double checkinRewardFixed = 0;
public double checkinRewardMin = 0;
public double checkinRewardMax = 0;
@@ -38,35 +37,36 @@ public class ConfigManager {
public int checkinPoints = 10;
public String checkinRewardType = "none";
public int backCheckPointMultiplier = 5;
// 提成/工单
public int checkinBondMin = 1;
public int checkinBondMax = 3;
public double providerPointsPerScore = 2.0;
public int baseEconomyReward = 100;
public double providerEconomyPerScore = 50.0;
public int requesterPointsReward = 10;
// 垃圾站
public boolean garbageEnabled = true;
public int garbageInterval = 300;
public int garbageMaxRounds = 1;
// ★ 构造函数接收 File,和 Main.java 调用一致 ★
public ConfigManager(File dataFolder) {
this.dataFolder = dataFolder;
}
public String rewardChannel = "债券"; // bonds = 债券,economy = Vault经济
// ==================== 消息 ====================
public void loadMessages() {
messages.clear();
File file =
new File(dataFolder, "消息.txt");
if (!file.exists())
createDefaultMessages(file);
File file = new File(dataFolder, "消息.txt");
if (!file.exists()) createDefaultMessages(file);
loadMap(file, messages);
}
public String msg(String key) {
String val =
messages.getOrDefault(key, key);
String val = messages.getOrDefault(key, key);
val = val.replace("<b>", "&l");
val = val.replace("</b>", "&r");
val = val.replace("<br>", "\n");
@@ -79,81 +79,22 @@ public class ConfigManager {
private void createDefaultMessages(File f) {
List<String> L = new ArrayList<>();
L.add("# ===== Sdf1_login 消息配置 =====");
L.add("# 占位符用 {xxx} 表示");
L.add("# 修改后执行 /sdf1_login reload");
L.add("");
L.add("# ---- 登录注册 ----");
L.add("login_timeout=§c登录超时,请重新登录");
L.add("not_registered=§e您尚未注册,请使用 /reg <密码>");
L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>");
L.add("already_registered=§c您已注册过账号");
L.add("login_timeout=§c登录超时");
L.add("not_registered=§e您尚未注册");
L.add("not_logged_in=§e您尚未登录");
L.add("already_registered=§c您已注册过");
L.add("already_logged_in=§c您已登录");
L.add("reg_success=§a注册成功!欢迎来到服务器");
L.add("reg_success=§a注册成功!");
L.add("login_success=§a登录成功!");
L.add("login_failed=§c密码错误,请重试");
L.add("");
L.add("# ---- 密码 ----");
L.add("login_failed=§c密码错误");
L.add("password_wrong=§c密码错误");
L.add("password_changed=§a密码修改成功!");
L.add("password_format_error=§c密码需6位以上,含大小写字母和数字");
L.add("password_format_error=§c密码需6位以上含大小写和数字");
L.add("password_same=§c新密码不能与旧密码相同");
L.add("reset_no_email=§c您未绑定邮箱,无法找回密码");
L.add("reset_sent=§a临时密码已发送到您的邮箱");
L.add("");
L.add("# ---- 签到积分 ----");
L.add("checkin_already=§c您今日已签到");
L.add("checkin_success=§a签到成功!获得 {points} 积分");
L.add("points_insufficient=§c积分不足,当前: {points}");
L.add("points_purchase_success=§a购买成功!消耗 {count} 积分");
L.add("");
L.add("# ---- 挂机检测 ----");
L.add("afk_set_enabled=§a挂机检测已开启");
L.add("afk_set_disabled=§c挂机检测已关闭");
L.add("afk_kick=§c因挂机过久被踢出");
L.add("");
L.add("# ---- 邮箱 ----");
L.add("email_set=§a邮箱设置成功:{email}");
L.add("email_format_error=§c邮箱格式不正确");
L.add("");
L.add("# ---- 邀请 ----");
L.add("invite_code_generated=§a邀请码:{code}");
L.add("invite_used=§a邀请成功,邀请人:{player}");
L.add("invite_self=§c不能使用自己的邀请码");
L.add("invite_not_found=§c邀请码不存在");
L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分");
L.add("");
L.add("# ---- 礼包 ----");
L.add("gift_not_ready=§c礼包条件未满足");
L.add("gift_claimed=§a礼包领取成功!");
L.add("");
L.add("# ---- 审批与IP ----");
L.add("need_approval=§e注册需审批 (工单#{id})");
L.add("ip_max_accounts=§cIP注册已达上限");
L.add("");
L.add("# ---- 管理员 ----");
L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除");
L.add("");
L.add("# ---- 聊天过滤 ----");
L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言");
L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规链接: §e{url}");
L.add("chat_url_violation=§7当前违规次数: §e{count}");
L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url}");
L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚");
L.add("");
L.add("# ---- 通用 ----");
L.add("chat_muted=§c你已被禁言");
L.add("未知参数=§c未知参数或权限不足");
writeLines(f, L);
}
@@ -161,10 +102,8 @@ public class ConfigManager {
public void loadSmtp() {
smtpSettings.clear();
File file =
new File(dataFolder, "SMTP设置.txt");
if (!file.exists())
createDefaultSmtp(file);
File file = new File(dataFolder, "SMTP设置.txt");
if (!file.exists()) createDefaultSmtp(file);
loadMap(file, smtpSettings);
}
@@ -177,10 +116,9 @@ public class ConfigManager {
}
public void saveSmtp() {
File file =
new File(dataFolder, "SMTP设置.txt");
File file = new File(dataFolder, "SMTP设置.txt");
List<String> L = new ArrayList<>();
L.add("# ===== SMTP 邮件服务器配置 =====");
L.add("# ===== SMTP 配置 =====");
for (Map.Entry<String, String> e
: smtpSettings.entrySet()) {
L.add(e.getKey() + "=" + e.getValue());
@@ -190,10 +128,10 @@ public class ConfigManager {
private void createDefaultSmtp(File f) {
List<String> L = new ArrayList<>();
L.add("# ===== SMTP 邮件服务器配置 =====");
L.add("# ===== SMTP 配置 =====");
L.add("smtp地址=smtp.example.com");
L.add("smtp端口=465");
L.add("smtp账号=your_email@example.com");
L.add("smtp账号=");
L.add("smtp密码=");
L.add("发件人名称=Sdf1_login");
L.add("smtp加密=true");
@@ -203,131 +141,134 @@ public class ConfigManager {
// ==================== 插件设置 ====================
public void loadSettings() {
File file =
new File(dataFolder, "插件设置.txt");
if (!file.exists())
createDefaultSettings(file);
Map<String, String> m =
new LinkedHashMap<>();
File file = new File(dataFolder, "插件设置.txt");
if (!file.exists()) createDefaultSettings(file);
Map<String, String> m = new LinkedHashMap<>();
loadMap(file, m);
adminTag = m.getOrDefault(
"管理标签", adminTag);
adminPassword = m.getOrDefault(
"管理密码", adminPassword);
maxAccountsPerIP = parseInt(
m.getOrDefault("每IP最大账号数", "3"), 3);
approvalMode = m.getOrDefault(
"审批模式", approvalMode);
autoApproveDelayMinutes = parseInt(
m.getOrDefault("自动审批延迟分钟", "30"),
30);
loginTimeout = parseInt(
m.getOrDefault("登录超时秒数", "60"), 60);
afkEnabled = Boolean.parseBoolean(
m.getOrDefault("挂机踢出", "false"));
afkTimeout = parseInt(
m.getOrDefault("挂机超时秒数", "300"), 300);
smtpSsl = Boolean.parseBoolean(
m.getOrDefault("smtp加密", "true"));
httpPort = parseInt(
m.getOrDefault("http-port", "8080"),
8080);
broadcastServerUrl = m.getOrDefault(
"resource-pack-url", "");
adminTag = m.getOrDefault("管理标签", adminTag);
adminPassword = m.getOrDefault("管理密码", adminPassword);
maxAccountsPerIP = parseInt(m.getOrDefault("每IP最大账号数", "3"), 3);
approvalMode = m.getOrDefault("审批模式", approvalMode);
autoApproveDelayMinutes = parseInt(m.getOrDefault("自动审批延迟分钟", "30"), 30);
loginTimeout = parseInt(m.getOrDefault("登录超时秒数", "60"), 60);
afkEnabled = Boolean.parseBoolean(m.getOrDefault("挂机踢出", "false"));
afkTimeout = parseInt(m.getOrDefault("挂机超时秒数", "300"), 300);
smtpSsl = Boolean.parseBoolean(m.getOrDefault("smtp加密", "true"));
httpPort = parseInt(m.getOrDefault("http-port", "8080"), 8080);
broadcastServerUrl = m.getOrDefault("resource-pack-url", "");
// 提成
providerPointsPerScore = parseDouble(
m.getOrDefault(
"服务商积分倍率", "2.0"), 2.0);
baseEconomyReward = parseInt(
m.getOrDefault(
"基础经济奖励", "100"), 100);
providerEconomyPerScore = parseDouble(
m.getOrDefault(
"经济奖励每评分", "50.0"), 50.0);
requesterPointsReward = parseInt(
m.getOrDefault(
"报单人积分奖励", "10"), 10);
providerPointsPerScore = parseDouble(m.getOrDefault("服务商积分倍率", "2.0"), 2.0);
baseEconomyReward = parseInt(m.getOrDefault("基础经济奖励", "100"), 100);
providerEconomyPerScore = parseDouble(m.getOrDefault("经济奖励每评分", "50.0"), 50.0);
requesterPointsReward = parseInt(m.getOrDefault("报单人积分奖励", "10"), 10);
// 垃圾站
garbageEnabled = Boolean.parseBoolean(
m.getOrDefault(
"垃圾站_启用", "true"));
garbageInterval = parseInt(
m.getOrDefault(
"垃圾站_清理间隔秒", "300"), 300);
garbageMaxRounds = parseInt(
m.getOrDefault(
"垃圾站_保留轮数", "1"), 1);
garbageEnabled = Boolean.parseBoolean(m.getOrDefault("垃圾站_启用", "true"));
garbageInterval = parseInt(m.getOrDefault("垃圾站_清理间隔秒", "300"), 300);
garbageMaxRounds = parseInt(m.getOrDefault("垃圾站_保留轮数", "1"), 1);
// 签到
checkinRewardFixed = parseDouble(
m.getOrDefault(
"签到固定金额", "0"), 0);
checkinRewardMin = parseDouble(
m.getOrDefault(
"签到最小金额", "0"), 0);
checkinRewardMax = parseDouble(
m.getOrDefault(
"签到最大金额", "0"), 0);
checkinGivePoints = Boolean.parseBoolean(
m.getOrDefault(
"签到给积分", "true"));
checkinPoints = parseInt(
m.getOrDefault(
"签到积分数量", "10"), 10);
backCheckPointMultiplier = parseInt(
m.getOrDefault(
"补签积分倍率", "5"), 5);
checkinRewardFixed = parseDouble(m.getOrDefault("签到固定金额", "0"), 0);
checkinRewardMin = parseDouble(m.getOrDefault("签到最小金额", "0"), 0);
checkinRewardMax = parseDouble(m.getOrDefault("签到最大金额", "0"), 0);
checkinGivePoints = Boolean.parseBoolean(m.getOrDefault("签到给积分", "true"));
checkinPoints = parseInt(m.getOrDefault("签到积分数量", "10"), 10);
backCheckPointMultiplier = parseInt(m.getOrDefault("补签积分倍率", "5"), 5);
checkinBondMin = parseInt(m.getOrDefault("签到债券最小", "1"), 1);
checkinBondMax = parseInt(m.getOrDefault("签到债券最大", "3"), 3);
if (checkinBondMax < checkinBondMin) checkinBondMax = checkinBondMin;
if (checkinRewardFixed > 0) checkinRewardType = "fixed";
else if (checkinRewardMin > 0 && checkinRewardMax > checkinRewardMin) checkinRewardType = "range";
else checkinRewardType = "none";
rewardChannel = m.getOrDefault("奖励发放方式", "债券");
rewardChannel = normalizeRewardChannel(rewardChannel);
// 自动判断模式
if (checkinRewardFixed > 0) {
checkinRewardType = "fixed";
} else if (checkinRewardMin > 0
&& checkinRewardMax > 0
&& checkinRewardMax
> checkinRewardMin) {
checkinRewardType = "range";
} else {
checkinRewardType = "none";
// 验证合法值
if (!"economy".equalsIgnoreCase(rewardChannel)
&& !"bonds".equalsIgnoreCase(rewardChannel)) {
rewardChannel = "bonds";
}
// ★ 自动补充新配置项 ★
boolean needAppend = false;
List<String> newKeys = new ArrayList<>();
if (!m.containsKey("签到债券最小")) { needAppend = true; newKeys.add("签到债券最小=" + checkinBondMin); }
if (!m.containsKey("签到债券最大")) { needAppend = true; newKeys.add("签到债券最大=" + checkinBondMax); }
if (!m.containsKey("奖励发放方式")) {
needAppend = true;
newKeys.add("奖励发放方式=债券");
}
if (needAppend) {
try {
FileWriter fw = new FileWriter(file, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.newLine();
for (String key : newKeys) { bw.write(key); bw.newLine(); }
bw.flush(); bw.close();
System.out.println("[Config] 已补充签到债券配置");
} catch (IOException ignored) {}
if (!m.containsKey("奖励发放方式")) {
try {
FileWriter fw = new FileWriter(file, true);
BufferedWriter bw = new BufferedWriter(fw);
bw.newLine();
bw.write("奖励发放方式=bonds");
bw.newLine();
bw.flush(); bw.close();
} catch (IOException ignored) {}
}
}
}
/**
* 奖励方式标准化:中英文全兼容
* "债券" "bonds" "Bond" "BONDS" → "bonds"
* "经济" "economy" "Economy" "ECONOMY" → "economy"
* 其他任何值 → "bonds"(默认债券)
*/
private String normalizeRewardChannel(String raw) {
if (raw == null) return "bonds";
String s = raw.trim().toLowerCase();
// 中文
if (s.contains("债券")) return "bonds";
if (s.contains("经济")) return "economy";
// 英文
if (s.contains("bond")) return "bonds";
if (s.contains("econ")) return "economy";
if (s.contains("Vault")) return "economy";
// 兜底
return "bonds";
}
public void saveSettings() {
File file =
new File(dataFolder, "插件设置.txt");
File file = new File(dataFolder, "插件设置.txt");
List<String> L = new ArrayList<>();
L.add("# ===== Sdf1_login 插件设置 =====");
L.add("管理标签=" + adminTag);
L.add("管理密码=" + adminPassword);
L.add("每IP最大账号数=" + maxAccountsPerIP);
L.add("审批模式=" + approvalMode);
L.add("自动审批延迟分钟="
+ autoApproveDelayMinutes);
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
L.add("登录超时秒数=" + loginTimeout);
L.add("挂机踢出=" + afkEnabled);
L.add("挂机超时秒数=" + afkTimeout);
L.add("# ---- 资源包 ----");
L.add("resource-pack-url="
+ broadcastServerUrl);
L.add("resource-pack-url=" + broadcastServerUrl);
L.add("http-port=" + httpPort);
L.add("# ---- 垃圾站 ----");
L.add("垃圾站_启用=" + garbageEnabled);
L.add("垃圾站_清理间隔秒="
+ garbageInterval);
L.add("垃圾站_清理间隔秒=" + garbageInterval);
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
L.add("# ---- 签到盲盒奖励 ----");
L.add("签到固定金额=" + checkinRewardFixed);
L.add("签到最小金额=" + checkinRewardMin);
L.add("签到最大金额=" + checkinRewardMax);
L.add("签到给积分=" + checkinGivePoints);
L.add("签到积分数量=" + checkinPoints);
L.add("补签积分倍率="
+ backCheckPointMultiplier);
L.add("签到债券最小=" + checkinBondMin);
L.add("签到债券最大=" + checkinBondMax);
L.add("补签积分倍率=" + backCheckPointMultiplier);
L.add("奖励发放方式=债券");
writeLines(file, L);
}
@@ -342,177 +283,90 @@ public class ConfigManager {
L.add("登录超时秒数=60");
L.add("挂机踢出=false");
L.add("挂机超时秒数=300");
L.add("# ---- 资源包 ----");
L.add("resource-pack-url=");
L.add("http-port=8080");
L.add("# ---- 垃圾站 ----");
L.add("垃圾站_启用=true");
L.add("垃圾站_清理间隔秒=300");
L.add("垃圾站_保留轮数=1");
L.add("# ---- 签到盲盒奖励 ----");
L.add("签到固定金额=0");
L.add("签到最小金额=50");
L.add("签到最大金额=500");
L.add("签到给积分=true");
L.add("签到积分数量=10");
L.add("签到债券最小=1");
L.add("签到债券最大=3");
L.add("补签积分倍率=5");
L.add("奖励发放方式=债券");
writeLines(f, L);
}
// ==================== 通用工具 ====================
public List<String> getAfkWhitelist() {
List<String> list = new ArrayList<>();
File file = new File(
dataFolder, "挂机白名单.txt");
File file = new File(dataFolder, "挂机白名单.txt");
if (!file.exists()) {
try {
file.createNewFile();
try (BufferedWriter w =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(
file),
StandardCharsets
.UTF_8))) {
w.write("# 每行一个玩家名");
w.newLine();
}
FileWriter fw = new FileWriter(file);
fw.write("# 每行一个玩家名\n");
fw.flush();
fw.close();
} catch (IOException ignored) {}
return list;
}
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(
file),
StandardCharsets
.UTF_8))) {
try (BufferedReader r = new BufferedReader(
new InputStreamReader(
new FileInputStream(file),
StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()
&& !line.startsWith("#")) {
if (!line.isEmpty() && !line.startsWith("#"))
list.add(line);
}
}
} catch (IOException ignored) {}
return list;
}
// ==================== 其他 ====================
public String generateSmtpVerifyCode() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789";
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder();
java.util.Random r =
new java.util.Random();
for (int i = 0; i < 6; i++) {
sb.append(chars.charAt(
r.nextInt(chars.length())));
}
java.util.Random r = new java.util.Random();
for (int i = 0; i < 6; i++) sb.append(chars.charAt(r.nextInt(chars.length())));
smtpVerifyCode = sb.toString();
smtpVerifyExpire =
System.currentTimeMillis()
+ 300000L;
smtpVerifyExpire = System.currentTimeMillis() + 300000L;
return smtpVerifyCode;
}
public boolean checkSmtpVerifyCode(
String code) {
if (System.currentTimeMillis()
> smtpVerifyExpire) {
smtpVerifyCode = "";
return false;
}
return smtpVerifyCode
.equalsIgnoreCase(code);
public boolean checkSmtpVerifyCode(String code) {
if (System.currentTimeMillis() > smtpVerifyExpire) { smtpVerifyCode = ""; return false; }
return smtpVerifyCode.equalsIgnoreCase(code);
}
public void clearSmtpVerifyCode() {
smtpVerifyCode = "";
smtpVerifyExpire = 0;
}
public void clearSmtpVerifyCode() { smtpVerifyCode = ""; smtpVerifyExpire = 0; }
public boolean canSendSmtpVerify() { return System.currentTimeMillis() - smtpLastSendTime >= 60000L; }
public void recordSmtpSendTime() { smtpLastSendTime = System.currentTimeMillis(); }
public boolean canSendSmtpVerify() {
return System.currentTimeMillis()
- smtpLastSendTime >= 60000L;
}
// ==================== 文件工具 ====================
public void recordSmtpSendTime() {
smtpLastSendTime =
System.currentTimeMillis();
}
// ==================== 文件读写 ====================
private void loadMap(File file,
Map<String, String> map) {
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(
file),
StandardCharsets
.UTF_8))) {
private void loadMap(File file, Map<String, String> map) {
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
if (line.isEmpty() || line.startsWith("#")) continue;
int eq = line.indexOf('=');
if (eq > 0) {
map.put(
line.substring(0, eq)
.trim(),
line.substring(eq + 1)
.trim());
}
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
}
} catch (IOException e) {
System.err.println(
"[Sdf1_login] 读取失败: "
+ file.getName());
}
} catch (IOException ignored) {}
}
private void writeLines(File file,
List<String> lines) {
try (BufferedWriter w =
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(
file),
StandardCharsets
.UTF_8))) {
for (String l : lines) {
w.write(l);
w.newLine();
}
} catch (IOException e) {
System.err.println(
"[Sdf1_login] 写入失败: "
+ file.getName());
}
private void writeLines(File file, List<String> lines) {
try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
for (String l : lines) { w.write(l); w.newLine(); }
} catch (IOException ignored) {}
}
private double parseDouble(
String s, double def) {
try {
return Double.parseDouble(s.trim());
} catch (Exception e) {
return def;
}
}
private int parseInt(String s, int def) {
try {
return Integer.parseInt(s.trim());
} catch (Exception e) {
return def;
}
}
private double parseDouble(String s, double def) { try { return Double.parseDouble(s.trim()); } catch (Exception e) { return def; } }
private int parseInt(String s, int def) { try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } }
}
+10 -2
View File
@@ -74,7 +74,7 @@ public class CypayCommand
}
// ===== /cypay import =====
if (args.length == 2
/* if (args.length == 2
&& "import".equalsIgnoreCase(
args[0])) {
if (!hasAdminPerm(sender)) {
@@ -93,7 +93,7 @@ public class CypayCommand
sender.sendMessage("§a导入完成: "
+ result + " 条");
return true;
}
}*/
// ===== /cypay freeze =====
if (args.length == 2
@@ -368,6 +368,14 @@ public class CypayCommand
// ===== /cypay <player> — 查指定玩家 =====
if (args.length == 1) {
String target = args[0];
// ★ 验证玩家在 login.db 中存在 ★
if (!plugin.getDb().userExists(target)) {
sender.sendMessage("§c玩家 " + target
+ " 不存在于服务器");
return true;
}
// String target = args[0];
if (hasAdminPerm(sender)) {
int bonds = plugin.getBonds()
.getBonds(target);
@@ -60,9 +60,11 @@ public class DatabaseManager {
+ "tasks_completed TEXT DEFAULT '',"
+ "premium_uuid TEXT DEFAULT '',"
+ "register_ip TEXT DEFAULT '',"
+ "register_type TEXT DEFAULT 'manual'"
+ "register_type TEXT DEFAULT 'manual',"
+ "backcheck_dates TEXT DEFAULT ''"
+ ")");
// 安全添加 users 缺失列
safeAdd(st, "tasks_completed",
"TEXT DEFAULT ''");
+284 -275
View File
@@ -1,275 +1,284 @@
package Sdf1_login;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Base64;
import java.util.logging.Logger;
public class EmailManager {
private final ConfigManager config;
private final Logger logger;
public EmailManager(ConfigManager config) {
this.config = config;
this.logger = Logger.getLogger("Sdf1_login");
}
public boolean isConfigured() {
String host = config.getSmtp("smtp地址");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
return host != null && !host.isEmpty()
&& user != null && !user.isEmpty()
&& pass != null && !pass.isEmpty();
}
public boolean sendVerifyCode(String to,
String code) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置,"
+ "无法发送验证码");
return false;
}
logger.info("[Sdf1_login] 发送验证码到 " + to);
return sendEmailSync(to,
"[Sdf1_login] 验证码",
"您的验证码: " + code
+ "\n5分钟内有效。\n"
+ "如非本人操作请忽略。");
}
public boolean sendTempPassword(String to,
String player, String tempPwd) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置");
return false;
}
return sendEmailSync(to,
"[Sdf1_login] 临时密码",
"玩家 " + player
+ " 的临时密码: " + tempPwd
+ "\n请尽快登录并修改密码!");
}
private boolean sendEmailSync(String to,
String subject, String body) {
final boolean[] result = {false};
Thread t = new Thread(() -> {
try {
result[0] = sendEmail(
to, subject, body);
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件线程异常: "
+ e.getMessage());
}
}, "Sdf1_login-SMTP");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
logger.severe(
"[Sdf1_login] SMTP超时(15秒)");
return false;
}
return result[0];
}
private boolean sendEmail(String to,
String subject, String body) {
String host = config.getSmtp("smtp地址");
String portStr = config.getSmtp("smtp端口");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
String fromName = config.getSmtp(
"发件人名称");
int port;
try {
port = Integer.parseInt(portStr);
} catch (Exception e) {
port = 465;
}
try {
String encSubject = "=?UTF-8?B?"
+ Base64.getEncoder()
.encodeToString(
subject.getBytes("UTF-8"))
+ "?=";
String encBody = Base64.getEncoder()
.encodeToString(
body.getBytes("UTF-8"));
if (port == 465) {
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket socket = (SSLSocket)
factory.createSocket(
host, port);
socket.setSoTimeout(10000);
doSmtp(socket, host, user, pass,
to, encSubject, encBody,
fromName);
socket.close();
} else {
Socket plainSocket =
new Socket(host, port);
plainSocket.setSoTimeout(10000);
BufferedReader preReader =
new BufferedReader(
new InputStreamReader(
plainSocket
.getInputStream(),
"UTF-8"));
PrintWriter preWriter =
new PrintWriter(
plainSocket
.getOutputStream(),
true);
checkResponse(preReader, "220");
sendLine(preWriter,
"EHLO " + host);
checkResponse(preReader, "250");
sendLine(preWriter, "STARTTLS");
checkResponse(preReader, "220");
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket sslSocket = (SSLSocket)
factory.createSocket(
plainSocket, host,
port, true);
sslSocket.startHandshake();
doSmtp(sslSocket, host, user, pass,
to, encSubject, encBody,
fromName);
sslSocket.close();
}
logger.info(
"[Sdf1_login] 邮件发送成功 -> "
+ to);
return true;
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件异常: "
+ e.getClass()
.getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
logger.severe(
"[Sdf1_login] 根因: "
+ e.getCause()
.getMessage());
}
return false;
}
}
private void doSmtp(SSLSocket socket,
String host, String user, String pass,
String to, String subject, String body,
String fromName) throws Exception {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
"UTF-8"));
OutputStream rawOut =
socket.getOutputStream();
PrintWriter writer =
new PrintWriter(
new java.io.BufferedOutputStream(
rawOut), true);
checkResponse(reader, "220");
sendLine(writer, "EHLO " + host);
checkResponse(reader, "250");
sendLine(writer, "AUTH LOGIN");
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
user.getBytes("UTF-8")));
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
pass.getBytes("UTF-8")));
checkResponse(reader, "235");
sendLine(writer,
"MAIL FROM:<" + user + ">");
checkResponse(reader, "250");
sendLine(writer,
"RCPT TO:<" + to + ">");
checkResponse(reader, "250");
sendLine(writer, "DATA");
checkResponse(reader, "354");
sendLine(writer,
"From: " + fromName
+ " <" + user + ">");
sendLine(writer,
"To: <" + to + ">");
sendLine(writer,
"Reply-To: <" + user + ">");
sendLine(writer,
"Subject: " + subject);
sendLine(writer, "MIME-Version: 1.0");
sendLine(writer,
"Content-Type: text/plain; "
+ "charset=UTF-8");
sendLine(writer,
"Content-Transfer-Encoding: "
+ "base64");
sendLine(writer, "");
sendLine(writer, body);
sendLine(writer, ".");
checkResponse(reader, "250");
sendLine(writer, "QUIT");
}
private void sendLine(PrintWriter writer,
String line) {
writer.print(line + "\r\n");
writer.flush();
}
private void checkResponse(
BufferedReader reader,
String expected) throws Exception {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
if (line.length() >= 4
&& Character.isDigit(
line.charAt(0))
&& line.charAt(3) == ' ')
break;
}
String resp = sb.toString().trim();
if (!resp.startsWith(expected)) {
if (resp.startsWith("535"))
logger.severe(
"[Sdf1_login] SMTP 535 "
+ "认证失败");
throw new Exception(
"SMTP响应异常: 期望 "
+ expected
+ ",实际: " + resp);
}
}
}
package Sdf1_login;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Base64;
import java.util.logging.Logger;
public class EmailManager {
private final ConfigManager config;
private final Logger logger;
public EmailManager(ConfigManager config) {
this.config = config;
this.logger = Logger.getLogger("Sdf1_login");
}
public boolean isConfigured() {
String host = config.getSmtp("smtp地址");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
return host != null && !host.isEmpty()
&& user != null && !user.isEmpty()
&& pass != null && !pass.isEmpty();
}
public boolean sendVerifyCode(String to,
String code) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置,"
+ "无法发送验证码");
return false;
}
logger.info("[Sdf1_login] 发送验证码到 " + to);
return sendEmailSync(to,
"[Sdf1_login] 验证码",
"您的验证码: " + code
+ "\n5分钟内有效。\n"
+ "如非本人操作请忽略。");
}
public boolean sendTempPassword(String to,
String player, String tempPwd) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置");
return false;
}
return sendEmailSync(to,
"[Sdf1_login] 临时密码",
"玩家 " + player
+ " 的临时密码: " + tempPwd
+ "\n请尽快登录并修改密码!");
}
public boolean sendBody(String to, String subject, String body) {
if (!isConfigured()) {
logger.warning("[Sdf1_login] SMTP未配置");
return false;
}
return sendEmailSync(to, subject, body);
}
private boolean sendEmailSync(String to,
String subject, String body) {
final boolean[] result = {false};
Thread t = new Thread(() -> {
try {
result[0] = sendEmail(
to, subject, body);
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件线程异常: "
+ e.getMessage());
}
}, "Sdf1_login-SMTP");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
logger.severe(
"[Sdf1_login] SMTP超时(15秒)");
return false;
}
return result[0];
}
private boolean sendEmail(String to,
String subject, String body) {
String host = config.getSmtp("smtp地址");
String portStr = config.getSmtp("smtp端口");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
String fromName = config.getSmtp(
"发件人名称");
int port;
try {
port = Integer.parseInt(portStr);
} catch (Exception e) {
port = 465;
}
try {
String encSubject = "=?UTF-8?B?"
+ Base64.getEncoder()
.encodeToString(
subject.getBytes("UTF-8"))
+ "?=";
String encBody = Base64.getEncoder()
.encodeToString(
body.getBytes("UTF-8"));
if (port == 465) {
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket socket = (SSLSocket)
factory.createSocket(
host, port);
socket.setSoTimeout(10000);
doSmtp(socket, host, user, pass,
to, encSubject, encBody,
fromName);
socket.close();
} else {
Socket plainSocket =
new Socket(host, port);
plainSocket.setSoTimeout(10000);
BufferedReader preReader =
new BufferedReader(
new InputStreamReader(
plainSocket
.getInputStream(),
"UTF-8"));
PrintWriter preWriter =
new PrintWriter(
plainSocket
.getOutputStream(),
true);
checkResponse(preReader, "220");
sendLine(preWriter,
"EHLO " + host);
checkResponse(preReader, "250");
sendLine(preWriter, "STARTTLS");
checkResponse(preReader, "220");
SSLSocketFactory factory =
(SSLSocketFactory)
SSLSocketFactory
.getDefault();
SSLSocket sslSocket = (SSLSocket)
factory.createSocket(
plainSocket, host,
port, true);
sslSocket.startHandshake();
doSmtp(sslSocket, host, user, pass,
to, encSubject, encBody,
fromName);
sslSocket.close();
}
logger.info(
"[Sdf1_login] 邮件发送成功 -> "
+ to);
return true;
} catch (Exception e) {
logger.severe(
"[Sdf1_login] 邮件异常: "
+ e.getClass()
.getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
logger.severe(
"[Sdf1_login] 根因: "
+ e.getCause()
.getMessage());
}
return false;
}
}
private void doSmtp(SSLSocket socket,
String host, String user, String pass,
String to, String subject, String body,
String fromName) throws Exception {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
"UTF-8"));
OutputStream rawOut =
socket.getOutputStream();
PrintWriter writer =
new PrintWriter(
new java.io.BufferedOutputStream(
rawOut), true);
checkResponse(reader, "220");
sendLine(writer, "EHLO " + host);
checkResponse(reader, "250");
sendLine(writer, "AUTH LOGIN");
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
user.getBytes("UTF-8")));
checkResponse(reader, "334");
sendLine(writer, Base64.getEncoder()
.encodeToString(
pass.getBytes("UTF-8")));
checkResponse(reader, "235");
sendLine(writer,
"MAIL FROM:<" + user + ">");
checkResponse(reader, "250");
sendLine(writer,
"RCPT TO:<" + to + ">");
checkResponse(reader, "250");
sendLine(writer, "DATA");
checkResponse(reader, "354");
sendLine(writer,
"From: " + fromName
+ " <" + user + ">");
sendLine(writer,
"To: <" + to + ">");
sendLine(writer,
"Reply-To: <" + user + ">");
sendLine(writer,
"Subject: " + subject);
sendLine(writer, "MIME-Version: 1.0");
sendLine(writer,
"Content-Type: text/plain; "
+ "charset=UTF-8");
sendLine(writer,
"Content-Transfer-Encoding: "
+ "base64");
sendLine(writer, "");
sendLine(writer, body);
sendLine(writer, ".");
checkResponse(reader, "250");
sendLine(writer, "QUIT");
}
private void sendLine(PrintWriter writer,
String line) {
writer.print(line + "\r\n");
writer.flush();
}
private void checkResponse(
BufferedReader reader,
String expected) throws Exception {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
if (line.length() >= 4
&& Character.isDigit(
line.charAt(0))
&& line.charAt(3) == ' ')
break;
}
String resp = sb.toString().trim();
if (!resp.startsWith(expected)) {
if (resp.startsWith("535"))
logger.severe(
"[Sdf1_login] SMTP 535 "
+ "认证失败");
throw new Exception(
"SMTP响应异常: 期望 "
+ expected
+ ",实际: " + resp);
}
}
}
+245 -53
View File
@@ -16,7 +16,7 @@ import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
@@ -163,8 +163,15 @@ public class GUIManager implements Listener {
"§6§l垃圾回收站",
"§7查看和取回清理的物品"));
}
// ===== Row3(27-35): 管理功能 =====
// ★ 商店 - 所有人都能看 ★
g.setItem(27, mkItem(Material.CHEST,
"§6§l商店",
"§7浏览、购买、出售物品",
"§7货币: 债券"));
if (isAdmin(p)) {
g.setItem(29, mkItem(
Material.REDSTONE_BLOCK,
@@ -301,6 +308,10 @@ public class GUIManager implements Listener {
g.setItem(22, mkItem(Material.LIME_WOOL,
"§a§l添加菜单项"));
g.setItem(27, mkItem(Material.ENDER_CHEST,
"§6§l商店",
"§7浏览、购买、出售物品",
"§7货币: 债券"));
g.setItem(26, mkItem(Material.ARROW,
"§7返回"));
@@ -342,7 +353,7 @@ public class GUIManager implements Listener {
public void openShop(Player p) {
p.closeInventory();
p.performCommand("sdf1_login shop");
plugin.getShopManager().openShopMain(p);
}
public void openTicket(Player p) {
@@ -351,11 +362,16 @@ public class GUIManager implements Listener {
}
@EventHandler
public void onInvClose(
InventoryCloseEvent e) {
if (!(e.getPlayer() instanceof Player))
return;
if (!(e.getPlayer() instanceof Player)) return;
Player p = (Player) e.getPlayer();
Player p3 = (Player) e.getPlayer();
String t = e.getView().getTitle();
if (!(e.getPlayer() instanceof Player)) return;
Player p2 = (Player) e.getPlayer();
if (!t.equals(T_EDITOR)) return;
@@ -718,13 +734,39 @@ public class GUIManager implements Listener {
@EventHandler
public void onInvDrag(
InventoryDragEvent e) {
if (!(e.getWhoClicked()
instanceof Player)) return;
Player p = (Player) e.getWhoClicked();
if (!T_EDITOR.equals(
e.getView().getTitle())) return;
public void onInvDrag(InventoryDragEvent e) {
if (!(e.getWhoClicked() instanceof Player)) return;
String title = e.getView().getTitle();
plugin.getLogger().info("[GUI] title检查点1: [" + title + "]");
// 购物车/退款/商店/流水记录页面:禁止一切拖拽
if ("§a§l购物车".equals(title)
|| "§c§l退款中心".equals(title)
|| "§6§l商店".equals(title)
|| title.startsWith("§e§l流水记录")
|| title.startsWith("§e§l转账记录")
|| title.startsWith("§d§l余额操作")) {
e.setCancelled(true);
return;
}
// 商店分类页:禁止拖拽
if (title.startsWith("§6§l") && !title.equals(T_MAIN)
&& !title.equals(T_MY_INFO)
&& !title.equals(T_ADMIN)
&& !title.equals(T_MORE)
&& !title.equals(T_EDITOR)
&& !title.equals(T_BIOME)
&& !title.equals(T_GIFT_STAGES)
&& !title.equals(T_INVITE)
&& !title.equals(T_USER_MGMT)
&& !title.equals(T_TASK_CENTER)) {
e.setCancelled(true);
return;
}
// 编辑器:图标拖拽替换
if (!T_EDITOR.equals(title)) return;
if (e.getRawSlots().contains(12)) {
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
@@ -734,69 +776,195 @@ public class GUIManager implements Listener {
e.getOldCursor().clone();
}
}
@EventHandler
public void onInvClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player)) return;
if (!(event.getWhoClicked() instanceof Player))
return;
Player p = (Player) event.getWhoClicked();
String title = event.getView().getTitle();
int raw = event.getRawSlot();
// 我的信息 — CDK兑换 (slot 16)
// ★ 模糊匹配所有订单相关
if (title.contains("订单")
|| title.contains("购买记录")) {
event.setCancelled(true);
if (title.contains("小票配置")) {
plugin.getOrderManager()
.handleReceiptConfigClick(p, raw);
} else if (title.contains("编辑:")) {
plugin.getOrderManager()
.handleReceiptBuilderClick(p, raw);
} else if (title.contains("购买记录")) {
plugin.getOrderManager()
.handleMyOrdersClick(
p, raw, event.isLeftClick(),
event.isShiftClick());
} else if (title.contains("管理员订单面板")) {
plugin.getOrderManager()
.handleAdminClick(p, raw);
} else if (title.contains("订单管理")) {
plugin.getOrderManager()
.handleAdminOrdersClick(
p, raw, event.isLeftClick(),
event.isShiftClick());
} else if (title.contains("订单中心")) {
plugin.getOrderManager()
.handleCenterClick(p, raw);
}
return;
}
// ===== 我的信息 — CDK兑换 =====
if (T_MY_INFO.equals(title) && raw == 16) {
event.setCancelled(true);
p.closeInventory();
p.sendMessage("§e§l[CDK] §f请输入兑换码(聊天输入):");
plugin.getCDK().requestInput(p, "cdk", "");
return;
if (handleCDKClick(p, event)) return;
}
if (T_MAIN.equals(title) && raw == 32) {
event.setCancelled(true);
plugin.openMyWallet(p);
return;
}
// 余额操作界面
// 余额操作界面
if ("§d§l余额操作".equals(title)) {
event.setCancelled(true);
if (raw == 49) {
openAdmin(p);
// ===== 主菜单 =====
if (T_MAIN.equals(title)) {
if (raw == 32) {
event.setCancelled(true);
plugin.openMyWallet(p);
return;
}
if (raw == 27) {
event.setCancelled(true);
plugin.getShopManager().openShopMain(p);
return;
}
return;
}
// ===== 我的钱包 =====
if ("§6§l我的钱包".equals(title)) {
event.setCancelled(true);
if (raw == 22) { openMain(p); return; }
if (raw == 12) { plugin.openWalletTransactions(p, 7); return; }
if (raw == 13) { plugin.openWalletTransactions(p, 14); return; }
if (raw == 14) { plugin.openWalletTransfers(p); return; }
return;
}
// ===== 流水/转账记录 =====
if (title.startsWith("§e§l流水记录")
|| title.startsWith("§e§l转账记录")) {
event.setCancelled(true);
if (raw == 53) { plugin.openMyWallet(p); return; }
return;
}
// ===== 余额操作 =====
if ("§d§l余额操作".equals(title)) {
event.setCancelled(true);
if (raw == 49) { openAdmin(p); return; }
if (raw < 0 || raw >= 45) return;
ItemStack item = event.getView().getTopInventory().getItem(raw);
ItemStack item = event.getView()
.getTopInventory().getItem(raw);
if (item == null) return;
Material mat = item.getType();
if (mat != Material.PLAYER_HEAD && mat != Material.EMERALD_BLOCK) return;
if (mat != Material.PLAYER_HEAD
&& mat != Material.EMERALD_BLOCK) return;
ItemMeta im = item.getItemMeta();
if (im == null || im.getDisplayName() == null) return;
String targetName = im.getDisplayName().replaceAll("§[0-9a-fk-orA-FK-OR]", "");
Player target = plugin.getServer().getPlayerExact(targetName);
if (im == null
|| im.getDisplayName() == null) return;
String targetName = im.getDisplayName()
.replaceAll("§[0-9a-fk-orA-FK-OR]", "");
Player target = plugin.getServer()
.getPlayerExact(targetName);
if (target == null) return;
String adminTag = plugin.getConfig2().adminTag;
boolean isAdmin = p.isOp() || (!adminTag.isEmpty() && p.getScoreboardTags().contains(adminTag));
String adminTag =
plugin.getConfig2().adminTag;
boolean isAdmin = p.isOp()
|| (!adminTag.isEmpty()
&& p.getScoreboardTags()
.contains(adminTag));
if (!isAdmin) return;
boolean leftOnly = event.isLeftClick() && !event.isRightClick();
boolean rightOnly = event.isRightClick() && !event.isLeftClick();
if (leftOnly) {
event.setCancelled(true);
if (event.isLeftClick()
&& !event.isRightClick()) {
p.closeInventory();
p.sendMessage("§e§l[经济操作] §f对 §a" + targetName + " §f进行经济操作");
p.sendMessage("§e§l[经济操作] §f对 §a"
+ targetName + " §f进行经济操作");
p.sendMessage("§7输入 §a+数字 §7给钱,§c-数字 §7扣钱");
plugin.getCDK().requestInput(p, "econ", targetName);
} else if (rightOnly) {
event.setCancelled(true);
} else if (event.isRightClick()
&& !event.isLeftClick()) {
p.closeInventory();
p.sendMessage("§e§l[债券操作] §f对 §a" + targetName + " §f进行债券操作");
p.sendMessage("§e§l[债券操作] §f对 §a"
+ targetName + " §f进行债券操作");
p.sendMessage("§7输入 §a+数字 §7给债券,§c-数字 §7扣债券");
plugin.getCDK().requestInput(p, "bond", targetName);
}
return;
}
// ===== 商店主界面 =====
if ("§6§l商店".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handleMainShopClick(p, raw);
return;
}
// ===== 退款中心 → 重定向订单中心 =====
if ("§c§l退款中心".equals(title)) {
event.setCancelled(true);
plugin.getOrderManager().openOrderCenter(p);
return;
}
// ===== 购物车页面 =====
if ("§a§l购物车".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handleCartClick(
p, raw, event.isLeftClick(),
event.isRightClick());
return;
}
// ===== 打包确认 =====
if ("§6§l选择包装方式".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handlePackingConfirm(p, raw);
return;
}
// ===== 自选颜色 =====
if ("§b§l选择潜影盒颜色".equals(title)) {
event.setCancelled(true);
plugin.getShopManager().handleColorSelect(p, raw);
return;
}
// ===== 我的订单 =====
if ("§6§l我的订单".equals(title)) {
event.setCancelled(true);
plugin.getOrderManager().handleMyOrdersClick(
p, raw, event.isLeftClick(),
event.isShiftClick());
return;
}
// ===== 管理员订单面板 =====
if ("§c§l管理员订单面板".equals(title)) {
event.setCancelled(true);
plugin.getOrderManager().handleAdminClick(p, raw);
return;
}
// ===== 商店分类页 =====
ShopManager sm = plugin.getShopManager();
boolean handled = sm.handleCategoryClick(
p, title, raw,
event.isLeftClick(),
event.isShiftClick());
if (handled) {
event.setCancelled(true);
return;
}
}
public void openEditor(
public void openEditor(
Player p, int idx) {
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
@@ -1026,27 +1194,51 @@ public class GUIManager implements Listener {
* 处理"我的信息"GUI中CDK兑换按钮的点击
* 返回 true 表示已处理
*/
public boolean handleCDKClick(Player p,
InventoryClickEvent event) {
public boolean handleCDKClick(Player p, InventoryClickEvent event) {
String title = event.getView().getTitle();
int raw = event.getRawSlot();
if (!T_MY_INFO.equals(title)) return false;
// CDK兑换按钮 (slot 16)
if (raw == 16) {
event.setCancelled(true);
p.closeInventory();
p.sendMessage(
"§e§l[CDK] §f请输入兑换码"
+ "(直接输入聊天即可):");
plugin.getCDK().requestInput(
p, "cdk", "");
org.bukkit.plugin.Plugin sdf1 =
org.bukkit.Bukkit.getPluginManager().getPlugin("Sdf1");
// 情况1:插件未安装
if (sdf1 == null) {
p.sendMessage("§c§l[CDK] §f口令兑换系统尚未安装,请联系管理员");
return true;
}
// 情况2:插件已安装但未启用
if (!sdf1.isEnabled()) {
p.sendMessage("§c§l[CDK] §f口令兑换系统未启用,请联系管理员");
return true;
}
// 情况3:正常接入 — 反射调用SDF1的startListening
try {
Class<?> mainClass = sdf1.getClass();
java.lang.reflect.Method method =
mainClass.getDeclaredMethod("startListening", Player.class);
method.setAccessible(true);
method.invoke(sdf1, p);
// startListening内部已发送 "已开启口令监听 (15秒)" 提示
} catch (NoSuchMethodException e) {
p.sendMessage("§c§l[CDK] §f口令系统版本过低,请联系管理员更新");
} catch (Exception e) {
p.sendMessage("§c§l[CDK] §f兑换系统异常,请稍后再试");
}
return true;
}
return false;
}
// ===== 放入自定义图标 =====
private void doPlaceIcon(Player p,
Inventory topInv,
+110 -67
View File
@@ -1,67 +1,110 @@
package Sdf1_login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class IPGroupManager {
private final Main plugin;
private final int maxAccounts;
public IPGroupManager(Main plugin, int maxAccounts) {
this.plugin = plugin;
this.maxAccounts = maxAccounts;
}
/**
* 检查该IP是否还能注册新账号
*/
public boolean canRegister(String ip) {
if (maxAccounts <= 0) return true;
if (ip == null || ip.isEmpty()) return true;
return queryAccountsByIP(ip).size() < maxAccounts;
}
/**
* 获取该IP下已注册的账号数量
*/
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
}
/**
* 获取该IP下所有账号名
*/
public List<String> getAccounts(String ip) {
return queryAccountsByIP(ip);
}
/**
* 直接通过数据库连接查询指定IP下的所有账号
*/
private List<String> queryAccountsByIP(String ip) {
List<String> accounts = new ArrayList<>();
Connection conn = plugin.getDb().getDb();
if (conn == null) return accounts;
try {
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
accounts.add(
rs.getString("player_name"));
}
rs.close();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
return accounts;
}
}
package Sdf1_login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class IPGroupManager {
private final Main plugin;
private final int maxAccounts;
public IPGroupManager(Main plugin, int maxAccounts) {
this.plugin = plugin;
this.maxAccounts = maxAccounts;
}
public boolean canRegister(String ip) {
if (maxAccounts <= 0) return true;
if (ip == null || ip.isEmpty()) return true;
int count = getAccountCount(ip);
boolean can = count < maxAccounts;
plugin.getLogger().info(
"[IP限制] IP=" + ip
+ " 已注册=" + count
+ " 上限=" + maxAccounts
+ " 允许=" + can);
return can;
}
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
}
public List<String> getAccounts(String ip) {
return queryAccountsByIP(ip);
}
private List<String> queryAccountsByIP(String ip) {
List<String> accounts = new ArrayList<>();
Connection conn = plugin.getDb().getDb();
if (conn == null) return accounts;
try {
// 主查询:ip_address 列
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
accounts.add(
rs.getString("player_name"));
}
rs.close();
ps.close();
// 兜底:如果主列没数据,查 last_login_ip
if (accounts.isEmpty()) {
PreparedStatement ps2 = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE last_login_ip = ? "
+ "AND (ip_address IS NULL "
+ "OR ip_address = '' "
+ "OR ip_address != ?)");
ps2.setString(1, ip);
ps2.setString(2, ip);
ResultSet rs2 = ps2.executeQuery();
while (rs2.next()) {
String name = rs2.getString("player_name");
if (!accounts.contains(name))
accounts.add(name);
}
rs2.close();
ps2.close();
}
// 兜底2:查 register_ip
if (accounts.isEmpty()) {
PreparedStatement ps3 = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE register_ip = ? "
+ "AND (ip_address IS NULL "
+ "OR ip_address = '') "
+ "AND (last_login_ip IS NULL "
+ "OR last_login_ip = '' "
+ "OR last_login_ip != ?)");
ps3.setString(1, ip);
ps3.setString(2, ip);
ResultSet rs3 = ps3.executeQuery();
while (rs3.next()) {
String name = rs3.getString("player_name");
if (!accounts.contains(name))
accounts.add(name);
}
rs3.close();
ps3.close();
}
plugin.getLogger().info(
"[IP限制] 查询IP=" + ip
+ " 结果数=" + accounts.size()
+ " 账号=" + accounts);
} catch (Exception e) {
e.printStackTrace();
}
return accounts;
}
}
@@ -1,46 +1,46 @@
package Sdf1_login;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
public class InventorySerializer {
public static String toBase64(ItemStack[] items) throws IOException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(items.length);
for (ItemStack item : items) {
dataOutput.writeObject(item);
}
dataOutput.close();
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception e) {
throw new IOException("Unable to serialize item stacks.", e);
}
}
public static ItemStack[] fromBase64(String data) throws IOException {
if (data == null || data.isEmpty()) {
return new ItemStack[0];
}
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int i = 0; i < items.length; i++) {
items[i] = (ItemStack) dataInput.readObject();
}
dataInput.close();
return items;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to deserialize item stacks.", e);
}
}
}
package Sdf1_login;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
public class InventorySerializer {
public static String toBase64(ItemStack[] items) throws IOException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
dataOutput.writeInt(items.length);
for (ItemStack item : items) {
dataOutput.writeObject(item);
}
dataOutput.close();
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception e) {
throw new IOException("Unable to serialize item stacks.", e);
}
}
public static ItemStack[] fromBase64(String data) throws IOException {
if (data == null || data.isEmpty()) {
return new ItemStack[0];
}
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int i = 0; i < items.length; i++) {
items[i] = (ItemStack) dataInput.readObject();
}
dataInput.close();
return items;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to deserialize item stacks.", e);
}
}
}
+17 -14
View File
@@ -118,7 +118,16 @@ public class LoginManager {
public boolean handleLogin(Player p,
String[] args) {
String name = p.getName();
// ★ 在 handleLogin 方法开头
if (plugin.getNeedsPasswordChange().contains(name)
&& plugin.getLoggedIn().contains(name)) {
// 已登录但还没改密码,只允许执行 /sdf1_login pw
p.sendMessage("§c请先修改密码: /sdf1_login pw <旧密码> <新密码>");
return true;
}
try {
if (plugin.getLoggedIn().contains(name)) {
p.sendMessage(plugin.getConfig2()
@@ -179,27 +188,21 @@ public class LoginManager {
name, hash);
if (matchTemp) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setLoggedIn(name, true);
p.setAllowFlight(false);
p.setFlying(false);
plugin.getDb().setField(name,
"last_login_time",
plugin.getDb().setField(name, "last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
plugin.getDb().setField(name, "last_online_check",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
plugin.getNeedsPasswordChange()
.add(name);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
p.sendMessage(
"§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage(
"§7用法: /sdf1_login pw");
// ★ 确保这行存在
plugin.getNeedsPasswordChange().add(name);
p.sendMessage(plugin.getConfig2().msg("login_success"));
p.sendMessage("§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage("§7用法: /sdf1_login pw");
activateCY(p, name);
return true;
}
+246 -88
View File
@@ -5,6 +5,8 @@ import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.PlayerInteractEvent;
import java.util.Iterator;
import java.util.List;
@@ -17,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Date;
import org.bukkit.command.CommandSender;
import net.milkbowl.vault.economy.Economy;
@@ -33,8 +35,6 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerJoinEvent;
@@ -52,7 +52,6 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.inventory.CraftItemEvent;
import org.bukkit.event.player.PlayerFishEvent;
import java.util.HashMap;
@@ -120,6 +119,7 @@ public class Main extends JavaPlugin
private final Map<String, Long> ipAutoLogin =
new HashMap<>();
private static final long IP_SKIP_MS = 300000L;
private SalesStatsManager salesStats;
private final Map<UUID, Long> lastActivity =
new ConcurrentHashMap<>();
@@ -138,6 +138,11 @@ public class Main extends JavaPlugin
// 在 Main.java 字段声明区域(bondManager 附近)添加:
// 在 bondManager 字段附近添加:
private BondPrinter bondPrinter;
private OrderManager orderManager;
public OrderManager getOrderManager() {
return orderManager;
}
// 钱包流水查看目标(玩家名 → 查看目标)
private final Map<String, String> walletViewTarget =
@@ -148,7 +153,7 @@ public class Main extends JavaPlugin
TX_SDF = new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
private ShopManager shopManager; //商店
@@ -275,17 +280,23 @@ private PVPManager pvpManager;
public Set<String> getNeedsPasswordChange() {
return needsPasswordChange;
}
// public RadioManager radio;
@Override
public void onEnable() {
getDataFolder().mkdirs();
salesStats = new SalesStatsManager(this);
// ===== 1. 基础配置 =====
config = new ConfigManager(getDataFolder());
config.loadMessages();
config.loadSmtp();
config.loadSettings();
configMgr = new ConfigManager(getDataFolder());
configMgr.loadSettings();
configMgr.loadMessages();
orderManager = new OrderManager(this);
// ===== 2. 数据库 =====
db = new DatabaseManager(getDataFolder());
db.init();
@@ -299,6 +310,10 @@ private PVPManager pvpManager;
// ===== 4. GUI(依赖 menu) =====
gui = new GUIManager(this);
shopManager = new ShopManager(this);
Bukkit.getPluginManager().registerEvents(shopManager, this);
shopManager.loadCategories();
getServer().getPluginManager()
.registerEvents(gui, this);
@@ -371,6 +386,10 @@ private PVPManager pvpManager;
getCommand("menu").setExecutor(this);
if (getCommand("printer") != null) {
getCommand("printer").setExecutor(this);
getCommand("shop").setExecutor(this);
getCommand("商店").setExecutor(this);
// getCommand("testorder").setExecutor(this);
}
@@ -421,22 +440,46 @@ private PVPManager pvpManager;
cdkManager = new CDKManager(this);
userGroupManager = new UserGroupManager(this);
cypayCommand = new CypayCommand(this);
// 加载CDK文件
cdkManager.loadCDKsFromDir();
/*加载CDK文件
cdkManager.loadCDKsFromDir();*/
// cypay 命令
if (getCommand("cypay") != null) {
getCommand("cypay").setExecutor(cypayCommand);
getCommand("cypay").setTabCompleter(cypayCommand);
}
bondPrinter = new BondPrinter(this);
cdkManager.loadCDKsFromDir();
// cdkManager.loadCDKsFromDir();
//14 ====商店====
// ★ 只重载数据,不创建新实例 ★
if (shopManager != null) {
shopManager.loadCategories();
}
configMgr = new ConfigManager(getDataFolder());
configMgr.loadSettings();
orderManager = new OrderManager(this);
getLogger().info("Sdf1_login v1.0 | 就绪");
getLogger().info("[SDF1] orderManager=" + (orderManager != null ? "OK" : "NULL"));
}
public BondPrinter getBondPrinter() {
return bondPrinter;
}
public ShopManager getShopManager() {
return shopManager;
}
private ConfigManager configMgr;
public ConfigManager getConfigMgr() {
return configMgr;
}
private void setupPVPTab() {
if (getCommand("pvp") != null) {
@@ -494,6 +537,7 @@ private PVPManager pvpManager;
@Override
public void onDisable() {
if (shopManager != null) shopManager.saveAll();
// 取消未执行的删除任务
if (pendingDeleteTask != null) {
pendingDeleteTask.cancel();
@@ -2282,10 +2326,14 @@ private PVPManager pvpManager;
long time = ((Number) tx.get("time"))
.longValue();
boolean pos = amount > 0;
String sign = pos ? "+" : "";
String color = pos ? "§a" : "§c";
String typeName = walletFormatType(type);
boolean isExpense = "shop_buy".equals(type)
|| "shop_packing".equals(type);
boolean pos = isExpense ? false : (amount > 0);
String sign = pos ? "+" : (isExpense ? "-" : "");
String color = pos ? "§a" : (isExpense ? "§c" : (amount < 0 ? "§c" : ""));
// 金额取绝对值显示
int displayAmount = Math.abs(amount);
// 用 if-else 替代 switch 避免编译问题
Material mat = Material.PAPER;
@@ -2317,11 +2365,13 @@ private PVPManager pvpManager;
} else {
mat = Material.RED_WOOL;
}
String typeName = walletFormatType(type);
List<String> lore = new ArrayList<>();
lore.add("§7类型: " + typeName);
lore.add("§7金额: " + color + sign + amount
lore.add("§7金额: " + color + sign + displayAmount
+ " §6枚");
if (tgt != null && !tgt.isEmpty())
lore.add("§7对象: §e" + tgt);
if (reason != null && !reason.isEmpty())
@@ -2333,8 +2383,9 @@ private PVPManager pvpManager;
ItemStack item = new ItemStack(mat);
ItemMeta im = item.getItemMeta();
if (im != null) {
im.setDisplayName(color + sign + amount
im.setDisplayName(color + sign + displayAmount
+ " §6枚 - " + typeName);
im.setLore(lore);
item.setItemMeta(im);
}
@@ -3587,6 +3638,18 @@ private PVPManager pvpManager;
e.setCancelled(true);
}
}
/** 验证玩家是否在主库中真实存在 */
public boolean isRealPlayer(String name) {
if (name == null || name.isEmpty()) return false;
try {
Map<String, Object> user =
getDb().getUser(name);
return user != null && !user.isEmpty()
&& user.containsKey("player_name");
} catch (Exception e) {
return false;
}
}
// ===== Commands =====
@Override
@@ -3595,6 +3658,136 @@ private PVPManager pvpManager;
String[] args) {
String cmdName = cmd.getName().toLowerCase();
if (cmd.getName().equalsIgnoreCase("testorder")
&& sender instanceof Player) {
Player p = (Player) sender;
p.sendMessage("§e=== 测试订单中心 ===");
p.sendMessage("§7当前界面: §f"
+ p.getOpenInventory().getTitle());
this.getOrderManager().openOrderCenter(p);
// 延迟1秒检查实际打开了什么
Bukkit.getScheduler().runTaskLater(this, () -> {
if (p.isOnline()) {
String opened = p.getOpenInventory()
.getTitle();
p.sendMessage("§7打开后界面: §f" + opened);
this.getLogger().info("[TEST] 打开后: "
+ opened);
}
}, 20L);
return true;
}
if (cmd.getName().equalsIgnoreCase("shop")
|| cmd.getName().equals("商店")) {
if (!(sender instanceof Player)) {
sender.sendMessage("§c仅玩家可使用");
return true;
}
if (shopManager != null) {
shopManager.openShopMain((Player) sender);
} else {
sender.sendMessage("§c商店系统未加载");
}
return true;
}
// ===== 在 onCommand 方法体内,最开头加: =====
if (cmd.getName().equalsIgnoreCase("sdf1_login")
&& args.length > 0
&& args[0].equalsIgnoreCase("shop")) {
if (shopManager == null) {
sender.sendMessage("§c商店未加载");
return true;
}
return shopManager.handleCommand(sender, args);
}
// set
String sub = args[0].toLowerCase();
if (sub.equals("set")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (args.length < 3) {
sender.sendMessage("§e用法: /sdf1_login set <玩家> <临时密码>");
return true;
}
String tgt = args[1];
String tempPwd = args[2];
if (!db.userExists(tgt)) {
sender.sendMessage("§c玩家不存在");
return true;
}
String salt = (String) db.getField(tgt, "password_salt");
String hash = PasswordUtils.hash(tempPwd, salt);
db.setField(tgt, "temp_password", hash);
db.setField(tgt, "temp_pw_expire",
System.currentTimeMillis() + 300000L);
db.setField(tgt, "temp_pw_used", 0);
// ★ 立即发邮件(踢人之前)
String emailAddr = (String) db.getField(tgt, "email");
if (emailAddr != null && !emailAddr.isEmpty()) {
// 获取最近登录信息
Object lastLoginObj = db.getField(tgt, "last_login_time");
long lastLogin = lastLoginObj != null
? ((Number) lastLoginObj).longValue() : 0;
String lastIP = (String) db.getField(tgt, "last_ip");
if (lastIP == null || lastIP.isEmpty())
lastIP = "未知";
java.text.SimpleDateFormat sdf =
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String loginTimeStr = lastLogin > 0
? sdf.format(new java.util.Date(lastLogin))
: "未知时间";
final String to = emailAddr;
final String fName = tgt;
final String pwd = tempPwd;
final String ipInfo = lastIP;
final String timeInfo = loginTimeStr;
final Main self = this;
Bukkit.getScheduler().runTaskAsynchronously(this,
() -> {
// ★ 发送临时密码邮件(带登录信息)
boolean sent = email.sendTempPassword(
to, fName, pwd);
// ★ 额外发送安全通知邮件
if (sent) {
String body = "玩家 " + fName
+ ":\n\n"
+ "管理员已为您的账号设置了临时密码。\n\n"
+ "临时密码: " + pwd + "\n"
+ "过期时间: 5分钟内\n\n"
+ "最近登录记录:\n"
+ " 时间: " + timeInfo + "\n"
+ " IP地址: " + ipInfo + "\n\n"
+ "如果您本人未操作,请忽略此邮件。\n"
+ "如果您怀疑账号被盗,请立即修改密码。";
email.sendBody(to,
"【安全通知】账号临时密码已设置", body);
}
Bukkit.getScheduler().runTask(self, () -> {
if (sent) {
sender.sendMessage("§a临时密码已发送至 "
+ tgt + " 的邮箱");
}
});
});
} else {
sender.sendMessage("§7该玩家未绑定邮箱,跳过邮件通知");
}
// 踢人
Player tp = Bukkit.getPlayer(tgt);
if (tp != null && tp.isOnline())
tp.kickPlayer("§c管理员已设置了临时密码,请用新密码登录");
sender.sendMessage("§a已为 " + tgt + " 设置临时密码");
return true;
}
if (label.equalsIgnoreCase("pvp")) {
if (!(sender instanceof Player)) {
@@ -3604,10 +3797,22 @@ private PVPManager pvpManager;
Player p = (Player) sender;
return pvpManager.onCommand(p, args);
}
if (cmdName.equals("cypay")) {
return cypayCommand.onCommand(sender, cmd, label, args);
if (args.length > 0) {
String target = args[0];
if (!getDb().userExists(target)) {
sender.sendMessage("§c玩家 " + target
+ " 不存在于服务器");
return true;
}
}
return cypayCommand.onCommand(
sender, cmd, label, args);
}
// ===== /printer [玩家名] =====
if (label.equalsIgnoreCase("printer")) {
if (bondPrinter == null) {
@@ -4018,19 +4223,6 @@ private PVPManager pvpManager;
gui.openMain((Player) sender);
return true;
}
// String cmdName = cmd.getName().toLowerCase();
if (!cmdName.equals("sdf1_login")) return false;
if (args.length == 0) {
if (sender instanceof Player) {
gui.openMain((Player) sender);
} else {
sender.sendMessage("§e用法: /sdf1_login <子命令>");
}
return true;
}
String sub = args[0].toLowerCase();
// ===== del / confirm / cancel(最先处理) =====
if (sub.equals("del")) {
@@ -4179,7 +4371,10 @@ private PVPManager pvpManager;
if (getMenu() != null) {
getMenu().loadMenu();
}
if (shopManager != null) {
shopManager.loadCategories();
}
if (shopManager != null) shopManager.loadCategories();
sender.sendMessage("§a配置已重载!");
return true;
}
@@ -4303,8 +4498,12 @@ private PVPManager pvpManager;
db.setField(p2.getName(), "password_salt", newSalt);
if (!mainOk && tempOk)
db.clearTempPassword(p2.getName());
// pw 命令改密码成功后
needsPasswordChange.remove(p2.getName());
p2.sendMessage(config.msg("password_changed"));
// 清除临时密码
if (!mainOk && tempOk)
db.clearTempPassword(p2.getName());
return true;
}
@@ -4785,62 +4984,6 @@ private PVPManager pvpManager;
return true;
}
// set
if (sub.equals("set")) {
if (!isAdmin(sender)) {
sender.sendMessage("§c权限不足");
return true;
}
if (args.length < 3) {
sender.sendMessage(
"§e用法: /sdf1_login set <玩家> <临时密码>");
return true;
}
String tgt = args[1];
String tempPwd = args[2];
if (!PasswordUtils.validate(tempPwd)) {
sender.sendMessage("§c密码格式不符合要求");
return true;
}
if (!db.userExists(tgt)) {
sender.sendMessage("§c玩家不存在");
return true;
}
String salt = (String) db.getField(
tgt, "password_salt");
String hash = PasswordUtils.hash(
tempPwd, salt);
db.setField(tgt, "temp_password", hash);
db.setField(tgt, "temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
db.setField(tgt, "temp_pw_used", 0);
sender.sendMessage("§a已为 " + tgt
+ " 设置临时密码");
Player tp = Bukkit.getPlayer(tgt);
if (tp != null && tp.isOnline())
tp.kickPlayer(
"§c管理员已设置临时密码");
// 邮件通知
String emailAddr = (String) db.getField(
tgt, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String body = "玩家 " + tgt
+ ":\n您的临时密码已由管理员设置。\n"
+ "临时密码将在5分钟后过期,请尽快登录并修改密码。";
final String to = emailAddr;
final String finalBody = body;
Bukkit.getScheduler()
.runTaskAsynchronously(this,
() -> email
.sendTempPassword(
to, tgt,
finalBody));
}
return true;
}
// give - 手动发放雪球菜单
if (sub.equals("give")) {
@@ -5032,6 +5175,21 @@ private PVPManager pvpManager;
return true;
}
/**
* 供SDF1反射调用的CDK兑换
* 返回: "success:金额:余额前:余额后"
* 或 "fail:原因"
*/
public String redeemBondForExternal(
String playerName, String code) {
if (cdkManager == null)
return "fail:CDK未初始化";
return cdkManager.redeem(code, playerName);
}
// 旧的(可能缺失或拼写错误)
public SalesStatsManager getSalesStats() {
return salesStats;
}
/**
* 是否是垃圾站展示物品(带ID lore)
@@ -5096,7 +5254,7 @@ private PVPManager pvpManager;
"get", "del", "set",
"take", "add", "kick",
"ticket", "oa", "stop",
"shopadd", "shopdel", "back", "radio", "back"));
"shopadd", "shopdel", "back", "radio", "back", "shop"));
} else {
list.addAll(Arrays.asList(
"pw", "email", "sign",
File diff suppressed because it is too large Load Diff
+72 -72
View File
@@ -1,72 +1,72 @@
package Sdf1_login;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class PasswordUtils {
private static final SecureRandom RANDOM =
new SecureRandom();
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public static String hash(String password, String salt) {
try {
MessageDigest md =
MessageDigest.getInstance("SHA-256");
md.update(Base64.getDecoder().decode(salt));
byte[] hash =
md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean validate(String password) {
if (password == null) return false;
if (password.length() < 6 || password.length() > 20)
return false;
boolean hasUpper = false;
boolean hasLower = false;
boolean hasDigit = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
if (Character.isLowerCase(c)) hasLower = true;
if (Character.isDigit(c)) hasDigit = true;
}
return hasUpper && hasLower && hasDigit;
}
public static String generateTempPassword() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateInviteCode() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < 6; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateVerifyCode() {
int code = 100000 + RANDOM.nextInt(900000);
return String.valueOf(code);
}
}
package Sdf1_login;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class PasswordUtils {
private static final SecureRandom RANDOM =
new SecureRandom();
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public static String hash(String password, String salt) {
try {
MessageDigest md =
MessageDigest.getInstance("SHA-256");
md.update(Base64.getDecoder().decode(salt));
byte[] hash =
md.digest(password.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static boolean validate(String password) {
if (password == null) return false;
if (password.length() < 6 || password.length() > 20)
return false;
boolean hasUpper = false;
boolean hasLower = false;
boolean hasDigit = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
if (Character.isLowerCase(c)) hasLower = true;
if (Character.isDigit(c)) hasDigit = true;
}
return hasUpper && hasLower && hasDigit;
}
public static String generateTempPassword() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateInviteCode() {
String chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder sb = new StringBuilder(6);
for (int i = 0; i < 6; i++) {
sb.append(chars.charAt(
RANDOM.nextInt(chars.length())));
}
return sb.toString();
}
public static String generateVerifyCode() {
int code = 100000 + RANDOM.nextInt(900000);
return String.valueOf(code);
}
}
+374 -374
View File
@@ -1,374 +1,374 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Method;
import java.util.*;
public class PointsManager {
private final Main plugin;
private Object cyInstance = null;
private Method cyActivationMethod = null;
private Method cyGetShopItemsMethod = null;
private final List<Map<String, Object>> cyItems =
new ArrayList<>();
public PointsManager(Main plugin) {
this.plugin = plugin;
discoverCY();
}
private void discoverCY() {
try {
org.bukkit.plugin.Plugin p = Bukkit
.getPluginManager()
.getPlugin("CY_beibao");
if (p != null && p.isEnabled()) {
cyInstance = p;
cyGetShopItemsMethod = p.getClass()
.getMethod("onSdf1GetShopItems");
cyActivationMethod = p.getClass()
.getMethod("onSdf1Activation",
String.class,
int.class, int.class);
plugin.getLogger().info(
"[Sdf1_login] CY_beibao 联控已连接");
}
} catch (Exception ignored) {
}
}
private void refreshCYItems() {
cyItems.clear();
if (cyInstance == null
|| cyGetShopItemsMethod == null) return;
try {
Object result =
cyGetShopItemsMethod.invoke(cyInstance);
if (result == null) return;
String data = result.toString();
if (data.isEmpty()) return;
String[] items = data.split(";");
for (String itemStr : items) {
String[] p = itemStr.split("\\|");
if (p.length < 8) continue;
Map<String, Object> item =
new LinkedHashMap<>();
item.put("id", p[0]);
item.put("name", p[1]);
item.put("days",
Integer.parseInt(p[2]));
item.put("slots",
Integer.parseInt(p[3]));
item.put("price",
Integer.parseInt(p[4]));
item.put("stock",
Integer.parseInt(p[5]));
item.put("icon", p[6]);
item.put("lifetime",
"1".equals(p[7]));
cyItems.add(item);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 拉取CY商品失败: "
+ e.getMessage());
}
}
private void activateCY(String name,
int slots, int days) {
try {
if (cyInstance == null
|| cyActivationMethod == null) return;
cyActivationMethod.invoke(
cyInstance, name, slots, days);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] CY激活失败: "
+ e.getMessage());
}
}
public int getPoints(String name) {
return ((Number) plugin.getDb()
.getField(name, "points")).intValue();
}
public Inventory createShopGUI(Player p) {
int points = getPoints(p.getName());
Inventory inv = Bukkit.createInventory(
null, 54,
"§d§l积分商城 | 当前积分: " + points);
fillBg(inv);
inv.setItem(10, mkItem(Material.EMERALD,
"§a游戏币充值",
"§7消耗积分兑换游戏币",
"§7100积分 = $1000",
"",
"§e点击兑换"));
inv.setItem(12, mkItem(Material.ENDER_CHEST,
"§b背包空间",
"§7消耗积分兑换背包格子",
"§750积分 = 9格永久空间",
"",
"§e点击兑换"));
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
"§e经验瓶",
"§7消耗积分兑换经验",
"§720积分 = 10级经验",
"",
"§e点击兑换"));
if (cyInstance != null) {
inv.setItem(31, mkItem(Material.CHEST,
"§6背包商城",
"§7来自CY_beibao的商品",
"",
"§e点击查看"));
}
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
return inv;
}
public Inventory createCYShopGUI(Player p) {
refreshCYItems();
int points = getPoints(p.getName());
int size = Math.max(54,
((cyItems.size() / 9) + 1) * 9);
if (size > 54) size = 54;
Inventory inv = Bukkit.createInventory(
null, size,
"§d§lCY背包商城 | 当前积分: " + points);
fillBg(inv);
for (int i = 0; i < cyItems.size()
&& i < 45; i++) {
Map<String, Object> item = cyItems.get(i);
String name = (String) item.get("name");
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
List<String> lore = new ArrayList<>();
lore.add("§7价格: §e" + price + " 积分");
if (lifetime) {
lore.add("§7格子: §e终身 +" + slots);
} else {
lore.add("§7格子: §e+" + slots);
lore.add("§7时间: §e" + days + "天");
}
if (stock == 0) {
lore.add("§7库存: §a无限");
} else if (stock == -1) {
lore.add("§7库存: §c已下架");
} else {
lore.add("§7库存: §e" + stock);
}
lore.add("");
if (points >= price && stock != -1) {
lore.add("§e点击购买");
} else if (stock == -1) {
lore.add("§c已下架");
} else {
lore.add("§c积分不足");
}
inv.setItem(i, mkItem(Material.CHEST,
"§a" + name,
lore.toArray(new String[0])));
}
inv.setItem(size - 1,
mkItem(Material.ARROW, "§7返回"));
return inv;
}
public void addItem(String name,
int price, String cmd2) {
// 追加到商品配置文件
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.io.FileWriter fw =
new java.io.FileWriter(file, true);
fw.write(name + ":" + price
+ ":" + cmd2 + "\n");
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void removeItem(int index) {
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.util.List<String> lines =
new java.util.ArrayList<>();
java.io.BufferedReader br =
new java.io.BufferedReader(
new java.io.FileReader(
file));
String line;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()
|| line.startsWith("#")) {
lines.add(line);
continue;
}
if (i != index) {
lines.add(line);
}
i++;
}
br.close();
java.io.PrintWriter pw =
new java.io.PrintWriter(file);
for (String l : lines) pw.println(l);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void handleClick(Player p, int slot) {
int points = getPoints(p.getName());
switch (slot) {
case 10:
if (points >= 100) {
plugin.getDb().deductPoints(
p.getName(), 100);
if (plugin.getEconomy() != null) {
plugin.getEconomy()
.depositPlayer(p, 1000);
}
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 12:
if (points >= 50) {
plugin.getDb().deductPoints(
p.getName(), 50);
activateCY(p.getName(), 9, 0);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "50"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 14:
if (points >= 20) {
plugin.getDb().deductPoints(
p.getName(), 20);
p.setLevel(p.getLevel() + 10);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "20"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 31:
if (cyInstance != null) {
p.openInventory(
createCYShopGUI(p));
}
break;
}
}
public void handleCYClick(Player p, int slot) {
if (slot < 0 || slot >= cyItems.size()
|| slot >= 45) return;
Map<String, Object> item = cyItems.get(slot);
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
if (stock == -1) {
p.sendMessage("§c该商品已下架");
return;
}
int points = getPoints(p.getName());
if (points < price) {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
return;
}
plugin.getDb().deductPoints(p.getName(), price);
int actDays = lifetime ? 0 : days;
activateCY(p.getName(), slots, actDays);
String tl = lifetime
? "终身" : (days + "天");
p.sendMessage("§a购买成功!获得 "
+ tl + " +" + slots + "格 背包空间");
}
private void fillBg(Inventory inv) {
ItemStack gl = mkItem(
Material.GRAY_STAINED_GLASS_PANE, " ");
for (int i = 0; i < inv.getSize(); i++)
inv.setItem(i, gl);
}
private ItemStack mkItem(Material mat, String name,
String... lore) {
ItemStack it = new ItemStack(mat);
ItemMeta im = it.getItemMeta();
if (im != null) {
im.setDisplayName(name);
if (lore.length > 0)
im.setLore(Arrays.asList(lore));
it.setItemMeta(im);
}
return it;
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Method;
import java.util.*;
public class PointsManager {
private final Main plugin;
private Object cyInstance = null;
private Method cyActivationMethod = null;
private Method cyGetShopItemsMethod = null;
private final List<Map<String, Object>> cyItems =
new ArrayList<>();
public PointsManager(Main plugin) {
this.plugin = plugin;
discoverCY();
}
private void discoverCY() {
try {
org.bukkit.plugin.Plugin p = Bukkit
.getPluginManager()
.getPlugin("CY_beibao");
if (p != null && p.isEnabled()) {
cyInstance = p;
cyGetShopItemsMethod = p.getClass()
.getMethod("onSdf1GetShopItems");
cyActivationMethod = p.getClass()
.getMethod("onSdf1Activation",
String.class,
int.class, int.class);
plugin.getLogger().info(
"[Sdf1_login] CY_beibao 联控已连接");
}
} catch (Exception ignored) {
}
}
private void refreshCYItems() {
cyItems.clear();
if (cyInstance == null
|| cyGetShopItemsMethod == null) return;
try {
Object result =
cyGetShopItemsMethod.invoke(cyInstance);
if (result == null) return;
String data = result.toString();
if (data.isEmpty()) return;
String[] items = data.split(";");
for (String itemStr : items) {
String[] p = itemStr.split("\\|");
if (p.length < 8) continue;
Map<String, Object> item =
new LinkedHashMap<>();
item.put("id", p[0]);
item.put("name", p[1]);
item.put("days",
Integer.parseInt(p[2]));
item.put("slots",
Integer.parseInt(p[3]));
item.put("price",
Integer.parseInt(p[4]));
item.put("stock",
Integer.parseInt(p[5]));
item.put("icon", p[6]);
item.put("lifetime",
"1".equals(p[7]));
cyItems.add(item);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 拉取CY商品失败: "
+ e.getMessage());
}
}
private void activateCY(String name,
int slots, int days) {
try {
if (cyInstance == null
|| cyActivationMethod == null) return;
cyActivationMethod.invoke(
cyInstance, name, slots, days);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] CY激活失败: "
+ e.getMessage());
}
}
public int getPoints(String name) {
return ((Number) plugin.getDb()
.getField(name, "points")).intValue();
}
public Inventory createShopGUI(Player p) {
int points = getPoints(p.getName());
Inventory inv = Bukkit.createInventory(
null, 54,
"§d§l积分商城 | 当前积分: " + points);
fillBg(inv);
inv.setItem(10, mkItem(Material.EMERALD,
"§a游戏币充值",
"§7消耗积分兑换游戏币",
"§7100积分 = $1000",
"",
"§e点击兑换"));
inv.setItem(12, mkItem(Material.ENDER_CHEST,
"§b背包空间",
"§7消耗积分兑换背包格子",
"§750积分 = 9格永久空间",
"",
"§e点击兑换"));
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
"§e经验瓶",
"§7消耗积分兑换经验",
"§720积分 = 10级经验",
"",
"§e点击兑换"));
if (cyInstance != null) {
inv.setItem(31, mkItem(Material.CHEST,
"§6背包商城",
"§7来自CY_beibao的商品",
"",
"§e点击查看"));
}
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
return inv;
}
public Inventory createCYShopGUI(Player p) {
refreshCYItems();
int points = getPoints(p.getName());
int size = Math.max(54,
((cyItems.size() / 9) + 1) * 9);
if (size > 54) size = 54;
Inventory inv = Bukkit.createInventory(
null, size,
"§d§lCY背包商城 | 当前积分: " + points);
fillBg(inv);
for (int i = 0; i < cyItems.size()
&& i < 45; i++) {
Map<String, Object> item = cyItems.get(i);
String name = (String) item.get("name");
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
List<String> lore = new ArrayList<>();
lore.add("§7价格: §e" + price + " 积分");
if (lifetime) {
lore.add("§7格子: §e终身 +" + slots);
} else {
lore.add("§7格子: §e+" + slots);
lore.add("§7时间: §e" + days + "天");
}
if (stock == 0) {
lore.add("§7库存: §a无限");
} else if (stock == -1) {
lore.add("§7库存: §c已下架");
} else {
lore.add("§7库存: §e" + stock);
}
lore.add("");
if (points >= price && stock != -1) {
lore.add("§e点击购买");
} else if (stock == -1) {
lore.add("§c已下架");
} else {
lore.add("§c积分不足");
}
inv.setItem(i, mkItem(Material.CHEST,
"§a" + name,
lore.toArray(new String[0])));
}
inv.setItem(size - 1,
mkItem(Material.ARROW, "§7返回"));
return inv;
}
public void addItem(String name,
int price, String cmd2) {
// 追加到商品配置文件
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.io.FileWriter fw =
new java.io.FileWriter(file, true);
fw.write(name + ":" + price
+ ":" + cmd2 + "\n");
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void removeItem(int index) {
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.util.List<String> lines =
new java.util.ArrayList<>();
java.io.BufferedReader br =
new java.io.BufferedReader(
new java.io.FileReader(
file));
String line;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()
|| line.startsWith("#")) {
lines.add(line);
continue;
}
if (i != index) {
lines.add(line);
}
i++;
}
br.close();
java.io.PrintWriter pw =
new java.io.PrintWriter(file);
for (String l : lines) pw.println(l);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void handleClick(Player p, int slot) {
int points = getPoints(p.getName());
switch (slot) {
case 10:
if (points >= 100) {
plugin.getDb().deductPoints(
p.getName(), 100);
if (plugin.getEconomy() != null) {
plugin.getEconomy()
.depositPlayer(p, 1000);
}
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 12:
if (points >= 50) {
plugin.getDb().deductPoints(
p.getName(), 50);
activateCY(p.getName(), 9, 0);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "50"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 14:
if (points >= 20) {
plugin.getDb().deductPoints(
p.getName(), 20);
p.setLevel(p.getLevel() + 10);
p.sendMessage(plugin.getConfig2()
.msg("points_purchase_success")
.replace("{count}", "20"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 31:
if (cyInstance != null) {
p.openInventory(
createCYShopGUI(p));
}
break;
}
}
public void handleCYClick(Player p, int slot) {
if (slot < 0 || slot >= cyItems.size()
|| slot >= 45) return;
Map<String, Object> item = cyItems.get(slot);
int price = ((Number) item.get("price"))
.intValue();
int stock = ((Number) item.get("stock"))
.intValue();
int days = ((Number) item.get("days"))
.intValue();
int slots = ((Number) item.get("slots"))
.intValue();
boolean lifetime = (boolean) item
.get("lifetime");
if (stock == -1) {
p.sendMessage("§c该商品已下架");
return;
}
int points = getPoints(p.getName());
if (points < price) {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
return;
}
plugin.getDb().deductPoints(p.getName(), price);
int actDays = lifetime ? 0 : days;
activateCY(p.getName(), slots, actDays);
String tl = lifetime
? "终身" : (days + "天");
p.sendMessage("§a购买成功!获得 "
+ tl + " +" + slots + "格 背包空间");
}
private void fillBg(Inventory inv) {
ItemStack gl = mkItem(
Material.GRAY_STAINED_GLASS_PANE, " ");
for (int i = 0; i < inv.getSize(); i++)
inv.setItem(i, gl);
}
private ItemStack mkItem(Material mat, String name,
String... lore) {
ItemStack it = new ItemStack(mat);
ItemMeta im = it.getItemMeta();
if (im != null) {
im.setDisplayName(name);
if (lore.length > 0)
im.setLore(Arrays.asList(lore));
it.setItemMeta(im);
}
return it;
}
}
+78 -54
View File
@@ -15,6 +15,8 @@ import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
// 如果 Bonds 在 Sdf1_login 包下:
import Sdf1_login.BondManager;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -892,7 +894,7 @@ public class QuestTracker {
/**
* 发放阶段奖励
* 含经济内容的走 Vault API
* 含经济内容的走 Vault API或债券
*/
public void claimRewards(
Player player,
@@ -900,13 +902,20 @@ public class QuestTracker {
if (qf == null
|| qf.rewards == null
|| qf.rewards.isEmpty()) {
player.sendMessage(
"§c无可领取的奖励");
player.sendMessage("§c无可领取的奖励");
return;
}
net.milkbowl.vault.economy.Economy econ =
((Main) plugin).getEconomy();
Main main = (Main) plugin;
// ★ 读取配置:economy = Vault,bonds = 债券(默认)
boolean useEconomy = "economy".equalsIgnoreCase(
main.getConfigMgr().rewardChannel);
// Vault 经济引用(仅 useEconomy 时使用)
net.milkbowl.vault.economy.Economy econ = null;
if (useEconomy) {
econ = main.getEconomy();
}
int sentCount = 0;
@@ -916,52 +925,70 @@ public class QuestTracker {
// 检查可选奖励标记
if (hasOptionalMarker(rt)) {
if (!areAllConditionsMet(player.getName(), qf))
if (!areAllConditionsMet(player.getName(), qf)) {
player.sendMessage(
"§7[可选奖励] "
+ "需完成所有可选条件: "
+ rt);
"§7[可选奖励] 需完成所有可选条件: " + rt);
continue;
}
}
// 按 + 分割子项
String[] parts =
rt.split("[+,]");
String[] parts = rt.split("[+,]");
for (String part : parts) {
part = part.trim();
if (part.isEmpty()) continue;
// 尝试发经济
double amt =
extractEconomyAmount(part);
if (amt > 0 && econ != null) {
double before =
econ.getBalance(player);
econ.depositPlayer(
player, amt);
player.sendMessage(
"§a§l[任务奖励] §f"
+ part
+ " §7(余额: §e"
+ String.format(
"%.2f", before)
+ " §7→ §a"
+ String.format(
"%.2f",
econ.getBalance(
player))
+ "§7)");
// 通知提成系统
if (plugin instanceof Main) {
((Main) plugin)
.getCommission()
.payCommission(
player
.getName(),
amt);
// 尝试识别经济金额
double amt = extractEconomyAmount(part);
if (amt > 0) {
// ★ 根据配置分发
if (useEconomy && econ != null) {
// —— Vault 经济 ——
double before =
econ.getBalance(player);
econ.depositPlayer(player, amt);
double after =
econ.getBalance(player);
player.sendMessage(
"§a§l[任务奖励] §f" + part
+ " §7(余额: §e"
+ String.format("%.2f", before)
+ " §7→ §a"
+ String.format("%.2f", after)
+ "§7)");
if (plugin instanceof Main) {
((Main) plugin).getCommission()
.payCommission(
player.getName(), amt);
}
} else {
// —— 债券(默认) ——
int bondAmt =
(int) Math.round(amt);
BondManager bonds = main.getBonds();
if (bonds != null) {
int before = bonds.getBonds(
player.getName());
bonds.addBonds(
player.getName(),
bondAmt,
"quest_reward",
qf.displayName,
"任务系统",
"任务奖励:" + qf.displayName);
int after = bonds.getBonds(
player.getName());
player.sendMessage(
"§a§l[任务奖励] §f" + part
+ " §7(债券: §e"
+ before
+ " §7→ §a"
+ after + "§7)");
} else {
player.sendMessage(
"§c债券系统不可用");
}
}
sentCount++;
continue;
}
@@ -971,32 +998,27 @@ public class QuestTracker {
parseRewardItems(part);
if (!items.isEmpty()) {
for (ItemStack is : items) {
player.getInventory()
.addItem(is);
player.getInventory().addItem(is);
}
player.sendMessage(
"§a§l[任务奖励] §f"
+ part);
"§a§l[任务奖励] §f" + part);
sentCount++;
continue;
}
// 非经济非物品 → 仅消息
player.sendMessage(
"§a§l[任务奖励] §f"
+ part);
"§a§l[任务奖励] §f" + part);
sentCount++;
}
}
plugin.getLogger().info(
"[QuestTracker] "
+ player.getName()
+ " 领取了 \""
+ qf.displayName
+ "\" 共 " + sentCount
+ " 项奖励");
"[QuestTracker] " + player.getName()
+ " 预取了 \"" + qf.displayName
+ "\" 共 " + sentCount + "项奖励");
}
/**
* 检查奖励文本是否含可选标记
*/
@@ -1043,7 +1065,9 @@ public class QuestTracker {
|| t.contains("$")
|| t.contains("经济")
|| t.contains("余额")
|| t.contains("发钱");
|| t.contains("发钱")
|| t.contains("债券"); // ★ 新增
if (!hasEcon) return 0;
@@ -0,0 +1,62 @@
package Sdf1_login;
import org.bukkit.Material;
import java.util.UUID;
public class RefundRecord {
public final UUID playerUUID;
public final String playerName;
public final String itemId;
public final String itemName;
public final Material material;
public final Material shulkerMaterial;
public int amount;
public int totalPaid;
public final long timestamp;
// 普通购买
public RefundRecord(UUID uuid, String name,
String itemId, String itemName,
Material material, int amount,
int totalPaid, long timestamp) {
this.playerUUID = uuid;
this.playerName = name;
this.itemId = itemId;
this.itemName = itemName;
this.material = material;
this.shulkerMaterial = null;
this.amount = amount;
this.totalPaid = totalPaid;
this.timestamp = timestamp;
}
// 盒装购买
public RefundRecord(UUID uuid, String name,
String itemId, String itemName,
Material material,
Material shulkerMat,
int amount, int totalPaid,
long timestamp) {
this.playerUUID = uuid;
this.playerName = name;
this.itemId = itemId;
this.itemName = itemName;
this.material = material;
this.shulkerMaterial = shulkerMat;
this.amount = amount;
this.totalPaid = totalPaid;
this.timestamp = timestamp;
}
public boolean isExpired() {
return System.currentTimeMillis()
- timestamp > 300000L;
}
public int getSecondsLeft() {
long left = 300000L
- (System.currentTimeMillis()
- timestamp);
return (int) Math.max(0, left / 1000);
}
}
+248 -248
View File
@@ -1,248 +1,248 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
*/
public class RiskControlManager {
private final Main plugin;
// 每个玩家名 -> 连续失败次数
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
*/
public void resetFailAttempts(String playerName) {
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
}
/**
* 清理过期的冷却记录(可由定时任务调用)
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
}
/**
* 获取所有玩家的失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
*/
public class RiskControlManager {
private final Main plugin;
// 每个玩家名 -> 连续失败次数
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
*/
public void resetFailAttempts(String playerName) {
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
}
/**
* 清理过期的冷却记录(可由定时任务调用)
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
}
/**
* 获取所有玩家的失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
}
}
@@ -0,0 +1,170 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class SalesStatsManager {
private final Main plugin;
private final Map<String, Integer> sessionSales =
new ConcurrentHashMap<>();
private int sessionRevenue = 0;
private int sessionOrders = 0;
private int sessionItems = 0;
private int sessionRefunds = 0;
private int sessionRefundAmount = 0;
private long sessionStart = System.currentTimeMillis();
// 状态
private boolean waitingForEmpty = false;
private boolean hasReported = false;
private BukkitRunnable waitTask = null;
public SalesStatsManager(Main plugin) {
this.plugin = plugin;
startMonitor();
}
// ★ 每10秒检测一次
private void startMonitor() {
new BukkitRunnable() {
@Override
public void run() {
int online = Bukkit.getOnlinePlayers().size();
if (online > 0) {
// 有玩家在线 → 重置等待状态
if (waitingForEmpty) {
// 刚有人进来了,清除已汇报标记
// 下次没人时会重新汇报
}
waitingForEmpty = false;
hasReported = false;
cancelWaitTask();
} else if (!waitingForEmpty && !hasReported) {
// 没人了,且还没汇报过 → 开始1分钟倒计时
waitingForEmpty = true;
startWaitCountdown();
}
}
}.runTaskTimer(plugin, 200L, 200L);
}
// ★ 1分钟等待
private void startWaitCountdown() {
cancelWaitTask();
waitTask = new BukkitRunnable() {
@Override
public void run() {
int online = Bukkit.getOnlinePlayers().size();
if (online > 0) {
// 等待期间有人进来 → 取消,等下次没人
waitingForEmpty = false;
hasReported = false;
waitTask = null;
cancel();
return;
}
// 1分钟没人 → 汇报一次
if (!hasReported) {
hasReported = true;
report();
}
waitingForEmpty = false;
waitTask = null;
cancel();
}
};
waitTask.runTaskLater(plugin, 1200L); // 60秒
}
private void cancelWaitTask() {
if (waitTask != null) {
waitTask.cancel();
waitTask = null;
}
}
// ===== 记录 =====
public void recordSale(String itemName, int price) {
sessionSales.merge(itemName, 1, Integer::sum);
sessionRevenue += price;
sessionOrders++;
sessionItems++;
}
public void recordRefund(int amount) {
sessionRefunds++;
sessionRefundAmount += amount;
}
// ===== 汇报 =====
public void report() {
if (sessionItems == 0 && sessionRefunds == 0) {
plugin.getLogger().info("[Sales] 无数据,跳过");
return;
}
long minutes =
(System.currentTimeMillis() - sessionStart)
/ 60000;
if (minutes < 1) minutes = 1;
StringBuilder sb = new StringBuilder();
sb.append("§6§l========== 销量汇总 ==========");
sb.append("\n§7时长: §f").append(minutes).append("分钟");
sb.append("\n§7订单: §f").append(sessionOrders).append("笔");
sb.append("\n§7商品: §f").append(sessionItems).append("件");
sb.append("\n§7成交额: §6").append(sessionRevenue).append("枚");
sb.append("\n§7退款: §c").append(sessionRefunds)
.append("次(§c").append(sessionRefundAmount)
.append("枚§7)");
sb.append("\n§6§l------------------------------");
if (!sessionSales.isEmpty()) {
sb.append("\n§7销量排行:");
List<Map.Entry<String, Integer>> sorted =
new ArrayList<>(sessionSales.entrySet());
sorted.sort((a, b) ->
b.getValue() - a.getValue());
int rank = 1;
for (Map.Entry<String, Integer> e : sorted) {
if (rank > 10) break;
sb.append("\n§e").append(rank).append(". ")
.append(e.getKey())
.append(" §7x").append(e.getValue());
rank++;
}
}
sb.append("\n§6§l==============================");
String msg = sb.toString();
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.isOp()) {
for (String line : msg.split("\n"))
p.sendMessage(line);
}
}
plugin.getLogger().info("[Sales] 汇报完成");
resetSession();
}
public void resetSession() {
sessionSales.clear();
sessionRevenue = 0;
sessionOrders = 0;
sessionItems = 0;
sessionRefunds = 0;
sessionRefundAmount = 0;
sessionStart = System.currentTimeMillis();
}
}
@@ -0,0 +1,32 @@
package Sdf1_login;
import java.util.ArrayList;
import java.util.List;
public class ShopCategory {
private final String name;
private final String fileName;
private final List<ShopItem> items;
public ShopCategory(String name, String fileName) {
this.name = name;
this.fileName = fileName;
this.items = new ArrayList<>();
}
public String getName() { return name; }
public String getFileName() { return fileName; }
public List<ShopItem> getItems() { return items; }
public ShopItem getItem(String id) {
for (ShopItem item : items) {
if (item.getId().equalsIgnoreCase(id))
return item;
}
return null;
}
public void addItem(ShopItem item) {
items.add(item);
}
}
+50
View File
@@ -0,0 +1,50 @@
package Sdf1_login;
import org.bukkit.Material;
public class ShopItem {
private final String id;
private final String displayName;
private final Material material;
private final int buyPrice;
private final int sellPrice;
private int stock;
private int hourlySales;
private int totalSales;
//打包了 直接测试替换就好了。
public ShopItem(String id, String displayName,
Material material, int buyPrice,
int sellPrice, int stock,
int hourlySales, int totalSales) {
this.id = id;
this.displayName = displayName;
this.material = material;
this.buyPrice = buyPrice;
this.sellPrice = sellPrice;
this.stock = stock;
this.hourlySales = hourlySales;
this.totalSales = totalSales;
}
public int getEffectiveSellPrice() {
if (sellPrice <= 0) return 0;
return sellPrice;
}
public String getId() { return id; }
public String getDisplayName() { return displayName; }
public Material getMaterial() { return material; }
public int getBuyPrice() { return buyPrice; }
public int getSellPrice() { return sellPrice; }
public int getStock() { return stock; }
public int getHourlySales() { return hourlySales; }
public int getTotalSales() { return totalSales; }
public void setStock(int s) { this.stock = s; }
public void setHourlySales(int s) { this.hourlySales = s; }
public void setTotalSales(int s) { this.totalSales = s; }
public void addSales(int n) {
this.hourlySales += n;
this.totalSales += n;
}
}
File diff suppressed because it is too large Load Diff
+158 -158
View File
@@ -1,158 +1,158 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
public class VerificationManager {
private final Main plugin;
public VerificationManager(Main plugin) {
this.plugin = plugin;
}
public boolean isBedrockPlayer(Player p) {
String uuid = p.getUniqueId().toString();
return uuid.contains("f851ba4")
|| uuid.startsWith("00000000-0000-0000");
}
public boolean isOnlineMode() {
return Bukkit.getServer().getOnlineMode();
}
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
"Sdf1_login-PremiumVerify");
t.setDaemon(true);
t.start();
}
private void doVerify(Player player,
Consumer<Boolean> callback) {
String name = player.getName();
String uuid = player.getUniqueId().toString();
plugin.getLogger().info(
"[Sdf1_login] ====== 验证开始 ======");
plugin.getLogger().info(
"[Sdf1_login] 玩家: " + name);
plugin.getLogger().info(
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 验证异常: "
+ e.getMessage());
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
public class VerificationManager {
private final Main plugin;
public VerificationManager(Main plugin) {
this.plugin = plugin;
}
public boolean isBedrockPlayer(Player p) {
String uuid = p.getUniqueId().toString();
return uuid.contains("f851ba4")
|| uuid.startsWith("00000000-0000-0000");
}
public boolean isOnlineMode() {
return Bukkit.getServer().getOnlineMode();
}
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
"Sdf1_login-PremiumVerify");
t.setDaemon(true);
t.start();
}
private void doVerify(Player player,
Consumer<Boolean> callback) {
String name = player.getName();
String uuid = player.getUniqueId().toString();
plugin.getLogger().info(
"[Sdf1_login] ====== 验证开始 ======");
plugin.getLogger().info(
"[Sdf1_login] 玩家: " + name);
plugin.getLogger().info(
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 验证异常: "
+ e.getMessage());
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}
+10 -7
View File
@@ -60,18 +60,12 @@ commands:
deny:
description: 拒绝OA请求
usage: /deny <编号> [理由]
#sdf1debug:
# description: 调试指令
# usage: /sdf1debug <玩家> take.<阶段>
email:
description: 绑定邮箱
usage: /email <邮箱>
绑定:
description: 绑定邮箱(中文)
usage: /绑定 <邮箱>
调试:
description: 调试模式
usage: /调试
绑定邮箱:
description: 绑定邮箱
usage: /绑定邮箱
@@ -95,4 +89,13 @@ commands:
usage: /cypay
printer :
description: 打印债券流水
usage: /printer
usage: /printer
shop :
description: 游戏商人
usage: /shop
商店:
description: 打开商店
usage: /商店
#testorder:
# description: 测试订单中心
# usage: /testorder