第八批:雪球菜单入口接入SN系统(换载体转移SN)、邀请码一次性持久落库并按新规则发债券、代码层删除提成规则.txt改由邀请好友赢债券/规则.md控制
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-12 15:51:38 +08:00
parent c214abeafa
commit df56e2c293
5 changed files with 251 additions and 192 deletions
+31 -4
View File
@@ -639,6 +639,18 @@ public class ChatInputManager {
reset(p);
return;
}
// ★ 第八批(2026-10-12):邀请码一次性——已绑定过邀请人的
// 禁止重复使用(invited_by 持久落库,重启后依然拦截)
Object bound = db.getField(
myName, "invited_by");
if (bound != null
&& !bound.toString().isEmpty()) {
p.sendMessage(
"§c你已绑定过邀请人(" + bound
+ "),邀请码仅能使用一次");
reset(p);
return;
}
boolean found = false;
for (Map<String, Object> u :
db.getAllUsers()) {
@@ -650,17 +662,32 @@ public class ChatInputManager {
u.get("player_name");
db.setField(myName,
"invited_by", inv);
db.addPoints(inv, 50);
// ★ 第八批(2026-10-12):绑定奖励改按
// 邀请好友赢债券/规则.md 发债券;
// 旧 addPoints(inv,50) 积分随提成规则体系废弃
int bondGive = mainPlugin
.getCommission()
.payInviteBond(inv, myName);
p.sendMessage(
"§a邀请成功!邀请人: "
+ inv);
Player ip =
Bukkit.getPlayer(inv);
Bukkit.getPlayerExact(inv);
if (ip != null
&& ip.isOnline())
&& ip.isOnline()) {
String extra = "";
if (bondGive > 0) {
extra = "§7,你获得 §e"
+ bondGive
+ " §a债券";
} else if (bondGive == 0) {
extra = "§7(今日邀请债券已达上限)";
}
ip.sendMessage("§a"
+ myName
+ " 使用了您的邀请码");
+ " 使用了您的邀请码"
+ extra);
}
found = true;
break;
}
+138 -161
View File
@@ -1,18 +1,33 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.logging.Logger;
/**
* 邀请好友赢债券奖励(配置:邀请好友赢债券/规则.md)。
*
* <p>★ 2026-10-12 第八批(用户决策):旧「提成规则.txt」(邀请人数:提成百分比)
* 体系整体废弃——其 payCommission 因 Economy 移除早已是无调用死代码,
* 签到链路奖励计算 (int)(1*rate) 恒为 0 从未生效(「邀请好友无效」根因)。
* 代码层面删除旧 txt(启动时清理而非 SFTP 手删——否则 writeDefault
* 会再次释放,写文件逻辑已一并移除),奖励改由新 md 规则控制:
* 绑定邀请码成功时立即结算债券,受「单次邀请奖励上限」「单日邀请奖励上限」
* 双重限制(当日累计按 bond_transaction type='invite_reward' 汇总,自然日重置)。</p>
*/
public class CommissionManager {
private final Main main;
private final Logger logger;
private final List<double[]> rules =
new ArrayList<>();
/** 单次邀请奖励(债券数,绑定成功立即结算) */
private volatile int perInviteReward = 10;
/** 单日邀请奖励上限(债券数,自然日重置) */
private volatile int dailyInviteCap = 100;
public CommissionManager(Main main) {
this.main = main;
@@ -21,189 +36,151 @@ public class CommissionManager {
}
public void loadRules() {
rules.clear();
File f = new File(
main.getDataFolder(),
// ★ 代码层面删除旧提成规则.txt(手删会被再次释放,故写文件
// 逻辑已移除,这里负责清理存量文件)
File legacy = new File(main.getDataFolder(),
"提成规则.txt");
if (!f.exists()) {
writeDefault(f);
if (legacy.exists() && legacy.delete()) {
logger.info("[Commission] 已删除旧版 提成规则.txt"
+ "(改由 邀请好友赢债券/规则.md 控制)");
}
try (BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
File f = new File(main.getDataFolder(),
"邀请好友赢债券/规则.md");
if (!f.exists()) {
try {
main.saveResource(
"邀请好友赢债券/规则.md", false);
} catch (Exception e) {
logger.warning("[Commission] 规则文件释放失败: "
+ e.getMessage());
}
}
if (!f.exists()) {
logger.warning("[Commission] 规则文件缺失,使用默认值"
+ " 单次=" + perInviteReward
+ " 单日=" + dailyInviteCap);
return;
}
boolean inCfg = false;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
String t = line.trim();
if (t.startsWith("##") && t.contains("配置区")
&& t.contains("开始")) {
inCfg = true;
continue;
// 格式: 邀请人数:提成百分比
// 如 "5:15" = 邀请5人提成15%
String[] parts =
line.split(":");
if (parts.length >= 2) {
try {
int count = Integer.parseInt(
parts[0].trim());
double rate = Double
.parseDouble(
parts[1]
.trim()) / 100.0;
rules.add(new double[]{
count, rate});
} catch (Exception ignored) {
}
}
if (t.startsWith("##") && t.contains("配置区")
&& t.contains("结束")) {
break;
}
if (!inCfg || !t.startsWith("|")) continue;
String[] cols = t.split("\\|", -1);
if (cols.length < 4) continue; // | 键 | 解释 | 值 |
String key = cols[1].trim();
String val = cols[cols.length - 2].trim()
.replace("`", "");
if ("单次邀请奖励上限".equals(key)) {
perInviteReward = parseIntSafe(val,
perInviteReward);
} else if ("单日邀请奖励上限".equals(key)) {
dailyInviteCap = parseIntSafe(val,
dailyInviteCap);
}
}
// 按人数排序(从大到小)
rules.sort((a, b) ->
Double.compare(b[0], a[0]));
logger.info(
"[Commission] 加载 "
+ rules.size() + " 条提成规则");
logger.info("[Commission] 邀请债券规则: 单次="
+ perInviteReward + " 单日=" + dailyInviteCap);
} catch (Exception e) {
logger.warning(
"[Commission] 加载失败: "
+ e.getMessage());
logger.warning("[Commission] 规则解析失败: "
+ e.getMessage());
}
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# 邀请提成规则");
pw.println("# 格式: 邀请人数:提成百分比");
pw.println("# 邀请10人提成20%");
pw.println("10:20");
pw.println("# 邀请5人提成15%");
pw.println("5:15");
pw.println("# 邀请1人提成10%");
pw.println("1:10");
} catch (IOException ignored) {
}
}
/**
* 获取邀请人的提成比例
* 根据已邀请人数匹配最高规则
*/
public double getCommissionRate(
String inviter) {
int inviteCount = getInviteCount(
inviter);
for (double[] rule : rules) {
if (inviteCount >= (int) rule[0]) {
return rule[1];
}
}
return 0;
}
/**
* 获取邀请人数
*/
public int getInviteCount(
String inviter) {
private static int parseIntSafe(String s, int def) {
try {
Object val =
main.getDb().getField(
inviter,
"monthly_invite_count");
if (val instanceof Number) {
return ((Number) val).intValue();
}
Object total =
main.getDb().getField(
inviter,
"total_invite_count");
if (total instanceof Number) {
return ((Number) total).intValue();
}
} catch (Exception ignored) {
return (int) Double.parseDouble(s.trim());
} catch (Exception e) {
return def;
}
return 0;
}
/** 单次邀请奖励(债券) */
public int getPerInviteReward() {
return perInviteReward;
}
/** 单日邀请奖励上限(债券) */
public int getDailyInviteCap() {
return dailyInviteCap;
}
/**
* 发放提成
* 被邀请人获得奖励时,邀请人获得提成
* 邀请绑定成功 → 给邀请人发债券。
* 规则见 邀请好友赢债券/规则.md:每次按单次上限结算,
* 当日累计不超单日上限。
*
* @param inviter 邀请人(收款方)
* @param invitee 被邀请人(绑定者)
* @return &gt;0 实发债券数;0 = 今日已达上限或规则为0;
* -1 = 异常/冻结未发
*/
public void payCommission(
String invitee,
double originalAmount) {
// 查找邀请人
String inviter = getInviter(invitee);
if (inviter == null
|| inviter.isEmpty()) return;
double rate = getCommissionRate(
inviter);
if (rate <= 0) return;
double commission =
originalAmount * rate;
if (commission <= 0) return;
// Economy已移除,佣金提成功能不可用
// 邀请人可通过债券系统获取奖励
// 通知邀请人
Player inviterP =
main.getServer()
.getPlayer(inviter);
if (inviterP != null
&& inviterP.isOnline()) {
inviterP.sendMessage(
"§a§l[邀请提成] §f"
+ invitee + " 完成任务,"
+ "你获得 §e"
+ String.format("%.2f",
commission)
+ " §f游戏币提成 ("
+ (int)(rate * 100) + "%)");
public int payInviteBond(String inviter, String invitee) {
if (inviter == null || inviter.isEmpty()) return -1;
if (perInviteReward <= 0) return 0;
int used = inviteBondToday(inviter);
int room = dailyInviteCap - used;
if (room <= 0) {
logger.info("[Commission] " + inviter
+ " 今日邀请债券已达上限(" + dailyInviteCap + ")");
return 0;
}
logger.info(
"[Commission] 提成: "
+ inviter + " ← "
+ invitee + " | "
+ String.format("%.2f",
originalAmount)
+ " × "
+ (int)(rate * 100) + "% = "
+ String.format("%.2f",
commission));
int give = Math.min(perInviteReward, room);
int after = main.getBonds().addBonds(inviter, give,
"invite_reward", invitee, "system",
"邀请好友绑定奖励(" + invitee + ")");
if (after < 0) return -1; // 冻结期未入账
logger.info("[Commission] 邀请债券: " + inviter + " ← "
+ invitee + " | +" + give + "(今日累计 "
+ (used + give) + "/" + dailyInviteCap + ")");
return give;
}
/**
* 获取邀请人
*/
private String getInviter(String player) {
/** 今日 0 点起该玩家已发放的邀请债券总数 */
private int inviteBondToday(String inviter) {
long dayStart = LocalDate.now()
.atStartOfDay(ZoneId.systemDefault())
.toInstant().toEpochMilli();
try {
Object val =
main.getDb().getField(
player,
"invited_by");
if (val != null
&& !val.toString()
.isEmpty()) {
return val.toString();
}
} catch (Exception ignored) {
PreparedStatement ps = main.getDb()
.getConnection().prepareStatement(
"SELECT COALESCE(SUM(amount),0) "
+ "FROM bond_transaction "
+ "WHERE player_name=? "
+ "AND type='invite_reward' "
+ "AND time>=?");
ps.setString(1, inviter);
ps.setLong(2, dayStart);
ResultSet rs = ps.executeQuery();
int v = rs.next() ? rs.getInt(1) : 0;
rs.close();
ps.close();
return v;
} catch (Exception e) {
logger.warning("[Commission] 查询当日邀请债券失败: "
+ e.getMessage());
return 0;
}
return null;
}
/**
* 判断金额是否包含经济内容
*/
public boolean hasEconomyContent(
String text) {
public boolean hasEconomyContent(String text) {
String t = text.toLowerCase();
return t.contains("游戏币")
|| t.contains("金币")
+59 -2
View File
@@ -1670,6 +1670,34 @@ public class GUIManager implements Listener {
return;
}
// ★ 第八批(2026-10-12):图标槽接入 SN 系统(此前完全没拦)——
// ① 背包已有自定义菜单物品 → 拒绝重复载体;
// ② 读旧雪球 SN 作换载体复用(登记不动、不触发冷静期);
// ③ 无旧 SN 则先过申领管控(冷静期/1人1个),拦在动背包之前。
SnManager snm = plugin.getSnManager();
for (ItemStack pre : p.getInventory().getContents()) {
if (isCustomMenuTrigger(pre)) {
p.sendMessage("§c[菜单] 你已拥有菜单物品,请先清除当前图标再更换");
return;
}
}
String reuseSn = null;
if (snm != null) {
for (ItemStack pre : p.getInventory().getContents()) {
if (pre != null
&& pre.getType() == Material.SNOWBALL
&& isMenuTriggerItem(pre)) {
reuseSn = snm.readSn(pre);
break;
}
}
}
if (reuseSn == null && snm != null
&& snm.isBlocked(p, SnManager.TYPE_MENU)) {
p.sendMessage("§c[SN] 菜单物品管控中(冷静期或已有有效登记),暂不能更换图标");
return;
}
int amount = item.getAmount();
removeSnowballFromInventory(p); //没收雪球菜单
// 1. 从来源移除
@@ -1740,6 +1768,23 @@ public class GUIManager implements Listener {
menu.setItemMeta(mm);
}
// 4.5 ★ 第八批:自定义菜单物品打 SN 副本——
// 有旧雪球 SN 则转移复用;没有则新申领(入口已过管控,
// 此处失败按 DB 异常放行不打标,与 Main.snTag 同款容错)
String menuSn = reuseSn;
if (menuSn == null && snm != null) {
menuSn = snm.applySnQuiet(p,
SnManager.TYPE_MENU, "自定义菜单图标");
}
if (menuSn != null && snm != null) {
snm.writeSn(menu, menuSn, SnManager.TYPE_MENU);
if (reuseSn != null) {
snm.logSn(menuSn, SnManager.TYPE_MENU, "xfer",
p.getName(),
"菜单载体转换:雪球->自定义图标");
}
}
// 5. 发放菜单物品
int slot = p.getInventory()
.firstEmpty();
@@ -1766,6 +1811,18 @@ public class GUIManager implements Listener {
String dbIcon = plugin.getDb()
.getMenuIcon(p.getName());
// ★ 第八批:没收前先读自定义物品的 SN,发雪球时转移复用
// (登记不动,避免旧登记把新雪球卡在「1人1个」/冷静期)
String reuseSn = null;
if (plugin.getSnManager() != null) {
for (ItemStack it : p.getInventory().getContents()) {
if (isCustomMenuTrigger(it)) {
reuseSn = plugin.getSnManager().readSn(it);
break;
}
}
}
// 1. 没收自定义菜单物品
removeCustomMenuFromInventory(p);
@@ -1776,8 +1833,8 @@ public class GUIManager implements Listener {
// 3. 强制清空slot 31
topInv.setItem(31, null);
// 4. 发雪球
plugin.giveMenuSnowball(p);
// 4. 发雪球(复用旧 SN 转移登记)
plugin.giveMenuSnowball(p, reuseSn);
// 5. 还原件
if (dbIcon != null && !dbIcon.isEmpty()) {
+5 -23
View File
@@ -47,29 +47,11 @@ public class InviteManager {
invitedBy, "monthly_invite_count",
count + 1);
double rate = 0.10;
if (count + 1 > 15) {
rate = 0.60;
} else if (count + 1 > 10) {
rate = 0.35;
} else if (count + 1 > 5) {
rate = 0.15;
}
int bonus = (int) (1 * rate);
if (bonus > 0) {
plugin.getDb().addPoints(invitedBy, bonus);
Player inviterPlayer = Bukkit
.getPlayerExact(invitedBy);
if (inviterPlayer != null
&& inviterPlayer.isOnline()) {
inviterPlayer.sendMessage(
plugin.getConfig2()
.msg("invite_referral_bonus")
.replace("{points}",
String.valueOf(bonus)));
}
}
// ★ 第八批(2026-10-12):旧「签到给邀请人积分提成」废弃——
// 原 bonus=(int)(1*rate) 恒为 0(rate 最大 0.60,截断后为 0),
// 奖励从未实际发放,即用户反馈「邀请好友无效」的根因;
// 邀请奖励统一在绑定邀请码时按 邀请好友赢债券/规则.md 发债券
// (CommissionManager.payInviteBond)。此处只保留月度邀请计数。
}
public List<Map<String, Object>> getAllInviteData() {
+18 -2
View File
@@ -3205,6 +3205,14 @@ public class Main extends JavaPlugin
"sdf1_menu";
public void giveMenuSnowball(Player p) {
giveMenuSnowball(p, null);
}
/**
* @param reuseSn 复用的旧 SN(自定义图标≉雪球互换时转移登记,
* 不重新申领、不触发冷静期);null = 正常申领
*/
public void giveMenuSnowball(Player p, String reuseSn) {
String name = p.getName();
try {
// getLogger().info("[菜单] ★调用 giveMenuSnowball: " + name);
@@ -3231,7 +3239,8 @@ public class Main extends JavaPlugin
}
// ★ SN 管控:1人同种类仅限1个,旧SN未注销/冷静期未过不再发放
if (snManager != null
// ★ 复用旧 SN 换载体时不拦——登记本来就是本人的
if (reuseSn == null && snManager != null
&& snManager.isBlocked(p, SnManager.TYPE_MENU)) {
return;
}
@@ -3299,7 +3308,14 @@ public class Main extends JavaPlugin
im.setLore(lore);
snow.setItemMeta(im);
}
if (!snTag(p, snow, SnManager.TYPE_MENU, "雪球菜单")) {
if (reuseSn != null && snManager != null) {
// ★ 图标≉雪球互换:SN 转移到新载体,登记不动(不触发冷静期)
snManager.writeSn(snow, reuseSn,
SnManager.TYPE_MENU);
snManager.logSn(reuseSn, SnManager.TYPE_MENU,
"xfer", p.getName(),
"菜单载体转换:自定义图标->雪球");
} else if (!snTag(p, snow, SnManager.TYPE_MENU, "雪球菜单")) {
return;
}
int slot = p.getInventory()