diff --git a/src/main/java/Sdf1_login/ChatInputManager.java b/src/main/java/Sdf1_login/ChatInputManager.java index 0a52a76..b24a560 100644 --- a/src/main/java/Sdf1_login/ChatInputManager.java +++ b/src/main/java/Sdf1_login/ChatInputManager.java @@ -662,15 +662,35 @@ public class ChatInputManager { u.get("player_name"); db.setField(myName, "invited_by", inv); + // ★ 第九批:绑定时间落库(供邀请GUI显示 + // 「邀请成功时间+邀请人」) + db.setField(myName, "invited_at", + System.currentTimeMillis()); + // ★ 第九批(配置懒更新):玩家绑定邀请码时 + // 自动重载 规则.md,实时刷新奖励信息 + // (不再依赖启动加载或定时热重载) + mainPlugin.getCommission().loadRules(); + // ★ 第九批:绑定即计入邀请人「本月邀请」 + // (修复旧逻辑在签到才计数导致恒 0 人) + mainPlugin.getInvite().onInviteeBind( + inv, myName); // ★ 第八批(2026-10-12):绑定奖励改按 // 邀请好友赢债券/规则.md 发债券; // 旧 addPoints(inv,50) 积分随提成规则体系废弃 + // ★ 第九批:奖励单向改双向——邀请人 + 被邀请人 int bondGive = mainPlugin .getCommission() .payInviteBond(inv, myName); - p.sendMessage( - "§a邀请成功!邀请人: " - + inv); + int bondGet = mainPlugin + .getCommission() + .payInviteeBond(myName, inv); + String bMsg = + "§a邀请成功!邀请人: " + inv; + if (bondGet > 0) { + bMsg += "§7,你获得 §e" + bondGet + + " §a债券"; + } + p.sendMessage(bMsg); Player ip = Bukkit.getPlayerExact(inv); if (ip != null diff --git a/src/main/java/Sdf1_login/CommissionManager.java b/src/main/java/Sdf1_login/CommissionManager.java index 75cdee4..6a7f25c 100644 --- a/src/main/java/Sdf1_login/CommissionManager.java +++ b/src/main/java/Sdf1_login/CommissionManager.java @@ -7,27 +7,38 @@ import java.sql.ResultSet; import java.time.LocalDate; import java.time.ZoneId; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 邀请好友赢债券奖励(配置:邀请好友赢债券/规则.md)。 * - *

★ 2026-10-12 第八批(用户决策):旧「提成规则.txt」(邀请人数:提成百分比) - * 体系整体废弃——其 payCommission 因 Economy 移除早已是无调用死代码, - * 签到链路奖励计算 (int)(1*rate) 恒为 0 从未生效(「邀请好友无效」根因)。 - * 代码层面删除旧 txt(启动时清理而非 SFTP 手删——否则 writeDefault - * 会再次释放,写文件逻辑已一并移除),奖励改由新 md 规则控制: - * 绑定邀请码成功时立即结算债券,受「单次邀请奖励上限」「单日邀请奖励上限」 - * 双重限制(当日累计按 bond_transaction type='invite_reward' 汇总,自然日重置)。

+ *

★ 2026-10-12 第八批(用户决策):旧「提成规则.txt」体系整体废弃, + * 奖励改由新 md 规则控制,绑定邀请码成功时立即结算债券。

+ * + *

★ 2026-10-12 第九批(用户需求)三点改造: + * ① 解析键改用新 md 4 键(旧「单次邀请奖励上限」→「邀请人单次奖励」), + * 新增「被邀请人单次奖励」「单日邀请翻倍额度」; + * ② 奖励单向改双向——payInviteBond 给邀请人、payInviteeBond 给被邀请人; + * 翻倍额度作用于邀请人(每邀请 X 人,奖金翻 Y 倍); + * ③ 支持懒更新——玩家绑定邀请码时调用 loadRules() 实时重载规则, + * 不再依赖启动时一次性加载或定时热重载。

*/ public class CommissionManager { private final Main main; private final Logger logger; - /** 单次邀请奖励(债券数,绑定成功立即结算) */ + /** 邀请人单次奖励(债券数,绑定成功立即结算) */ private volatile int perInviteReward = 10; - /** 单日邀请奖励上限(债券数,自然日重置) */ + /** 被邀请人单次奖励(债券数,绑定成功立即结算) */ + private volatile int perInviteeReward = 5; + /** 单日邀请奖励上限(债券数,自然日重置,仅针对邀请人) */ private volatile int dailyInviteCap = 100; + /** 单日邀请翻倍额度:每邀请 X 人(>0 生效,0=关闭翻倍) */ + private volatile int doubleEveryX = 0; + /** 单日邀请翻倍额度:奖金翻 Y 倍(翻 Y 倍 = 每满 X 人基数加 Y 倍) */ + private volatile int doubleByY = 0; public CommissionManager(Main main) { this.main = main; @@ -35,6 +46,9 @@ public class CommissionManager { loadRules(); } + /** + * 重载规则(★ 第九批:绑定邀请码时懒调用,实时刷新奖励信息)。 + */ public void loadRules() { // ★ 代码层面删除旧提成规则.txt(手删会被再次释放,故写文件 // 逻辑已移除,这里负责清理存量文件) @@ -58,7 +72,8 @@ public class CommissionManager { } if (!f.exists()) { logger.warning("[Commission] 规则文件缺失,使用默认值" - + " 单次=" + perInviteReward + + " 邀请人=" + perInviteReward + + " 被邀请人=" + perInviteeReward + " 单日=" + dailyInviteCap); return; } @@ -85,22 +100,52 @@ public class CommissionManager { String key = cols[1].trim(); String val = cols[cols.length - 2].trim() .replace("`", ""); - if ("单次邀请奖励上限".equals(key)) { + // ★ 第九批:适配新 md 4 键 + if ("邀请人单次奖励".equals(key)) { perInviteReward = parseIntSafe(val, perInviteReward); + } else if ("被邀请人单次奖励".equals(key)) { + perInviteeReward = parseIntSafe(val, + perInviteeReward); } else if ("单日邀请奖励上限".equals(key)) { dailyInviteCap = parseIntSafe(val, dailyInviteCap); + } else if ("单日邀请翻倍额度".equals(key)) { + parseDoubleRule(val); } } - logger.info("[Commission] 邀请债券规则: 单次=" - + perInviteReward + " 单日=" + dailyInviteCap); + logger.info("[Commission] 邀请债券规则: 邀请人=" + + perInviteReward + + " 被邀请人=" + perInviteeReward + + " 单日=" + dailyInviteCap + + " 翻倍=" + (doubleEveryX > 0 + ? ("每" + doubleEveryX + "人翻" + + doubleByY + "倍") + : "关闭")); } catch (Exception e) { logger.warning("[Commission] 规则解析失败: " + e.getMessage()); } } + /** 解析「每邀请X人,奖金翻Y倍」格式 */ + private void parseDoubleRule(String v) { + // 默认关闭 + doubleEveryX = 0; + doubleByY = 0; + if (v == null) return; + Matcher m = Pattern.compile( + "每邀请\\s*(\\d+)\\s*人\\s*[,,]\\s*奖金翻\\s*(\\d+)\\s*倍") + .matcher(v); + if (m.find()) { + doubleEveryX = parseIntSafe(m.group(1), 0); + doubleByY = parseIntSafe(m.group(2), 0); + } else { + logger.warning("[Commission] 翻倍额度格式无法解析: " + + v + "(示例:每邀请2人,奖金翻1倍)"); + } + } + private static int parseIntSafe(String s, int def) { try { return (int) Double.parseDouble(s.trim()); @@ -109,19 +154,24 @@ public class CommissionManager { } } - /** 单次邀请奖励(债券) */ + /** 邀请人单次奖励(债券) */ public int getPerInviteReward() { return perInviteReward; } + /** 被邀请人单次奖励(债券) */ + public int getPerInviteeReward() { + return perInviteeReward; + } + /** 单日邀请奖励上限(债券) */ public int getDailyInviteCap() { return dailyInviteCap; } /** - * 邀请绑定成功 → 给邀请人发债券。 - * 规则见 邀请好友赢债券/规则.md:每次按单次上限结算, + * 邀请绑定成功 → 给邀请人发债券(★ 第九批:含翻倍额度)。 + * 规则见 邀请好友赢债券/规则.md:每次按单次上限结算(每满 X 人翻 Y 倍), * 当日累计不超单日上限。 * * @param inviter 邀请人(收款方) @@ -139,18 +189,51 @@ public class CommissionManager { + " 今日邀请债券已达上限(" + dailyInviteCap + ")"); return 0; } - int give = Math.min(perInviteReward, room); + // ★ 第九批:单日邀请翻倍额度——每邀请 X 人,奖金翻 Y 倍 + // nToday = 今日已发放的邀请奖励笔数(≈已成功邀请人数) + // mult = 1 + (nToday / X) * Y(整数除法,每满 X 人加 Y 倍) + int nToday = inviteBondCountToday(inviter); + int mult = 1; + if (doubleEveryX > 0) { + mult = 1 + (nToday / doubleEveryX) * doubleByY; + if (mult < 1) mult = 1; + } + int base = perInviteReward * mult; + int give = Math.min(base, room); int after = main.getBonds().addBonds(inviter, give, "invite_reward", invitee, "system", "邀请好友绑定奖励(" + invitee + ")"); if (after < 0) return -1; // 冻结期未入账 logger.info("[Commission] 邀请债券: " + inviter + " ← " - + invitee + " | +" + give + "(今日累计 " + + invitee + " | +" + give + "(基数" + + perInviteReward + "×" + mult + ",今日累计 " + (used + give) + "/" + dailyInviteCap + ")"); return give; } - /** 今日 0 点起该玩家已发放的邀请债券总数 */ + /** + * 邀请绑定成功 → 给被邀请人发债券(★ 第九批:双向奖励)。 + * 被邀请人奖励为固定单次数额,不受单日上限约束(上限仅针对邀请人)。 + * + * @param invitee 被邀请人(绑定者,收款方) + * @param inviter 邀请人(被邀请人的邀请来源) + * @return >0 实发债券数;0 = 规则为0;-1 = 异常/冻结未发 + */ + public int payInviteeBond(String invitee, String inviter) { + if (invitee == null || invitee.isEmpty()) return -1; + if (perInviteeReward <= 0) return 0; + // 用独立 type='invitee_reward',避免污染邀请人日上限统计 + int after = main.getBonds().addBonds(invitee, + perInviteeReward, "invitee_reward", inviter, + "system", + "被邀请人绑定奖励(邀请人:" + inviter + ")"); + if (after < 0) return -1; // 冻结期未入账 + logger.info("[Commission] 被邀请人债券: " + invitee + + " ← " + inviter + " | +" + perInviteeReward); + return perInviteeReward; + } + + /** 今日 0 点起该玩家已发放的邀请债券总数(金额) */ private int inviteBondToday(String inviter) { long dayStart = LocalDate.now() .atStartOfDay(ZoneId.systemDefault()) @@ -177,6 +260,33 @@ public class CommissionManager { } } + /** 今日 0 点起该玩家已成功邀请人数(发奖笔数,用于翻倍档位) */ + private int inviteBondCountToday(String inviter) { + long dayStart = LocalDate.now() + .atStartOfDay(ZoneId.systemDefault()) + .toInstant().toEpochMilli(); + try { + PreparedStatement ps = main.getDb() + .getConnection().prepareStatement( + "SELECT COUNT(*) " + + "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; + } + } + /** * 判断金额是否包含经济内容 */ diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 172d589..f95e3c1 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -51,6 +51,7 @@ public class DatabaseManager { + "gift_claimed TEXT DEFAULT ''," + "invite_code TEXT DEFAULT ''," + "invited_by TEXT DEFAULT ''," + + "invited_at INTEGER DEFAULT 0," + "monthly_invite_count INTEGER DEFAULT 0," + "invite_month TEXT DEFAULT ''," + "points INTEGER DEFAULT 0," @@ -92,6 +93,10 @@ public class DatabaseManager { "TEXT DEFAULT ''"); safeAdd(st, "last_login_ip", "TEXT DEFAULT ''"); + // ★ 第九批:邀请绑定时间(毫秒时间戳), + // 供「我的邀请码」GUI 显示邀请成功时间 + safeAdd(st, "invited_at", + "INTEGER DEFAULT 0"); // UI偏好:1=CLI(默认), 0=GUI safeAdd(st, "ui_mode", "INTEGER DEFAULT 1"); diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 7b4d458..920dc7d 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -2431,11 +2431,31 @@ public class GUIManager implements Listener { g.setItem(12, mkItem(Material.PLAYER_HEAD, "§a本月邀请", "§7已邀请 §e" + monthly + " §7人")); - g.setItem(16, mkItem(Material.BOOK, - "§e输入邀请码", - "§7绑定他人的邀请码", - "", - "§e点击输入")); + // ★ 第九批:已绑定邀请人则显示「邀请成功时间+邀请人」, + // 否则显示「输入邀请码」按钮 + String invitedBy = (String) plugin.getDb() + .getField(p.getName(), "invited_by"); + if (invitedBy != null && !invitedBy.isEmpty()) { + Object tsObj = plugin.getDb().getField( + p.getName(), "invited_at"); + long ts = (tsObj instanceof Number) + ? ((Number) tsObj).longValue() : 0L; + String timeStr = ts > 0 + ? new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm") + .format(new java.util.Date(ts)) + : "未知"; + g.setItem(16, mkItem(Material.WRITTEN_BOOK, + "§a邀请已绑定", + "§7邀请人: §e" + invitedBy, + "§7绑定时间: §e" + timeStr)); + } else { + g.setItem(16, mkItem(Material.BOOK, + "§e输入邀请码", + "§7绑定他人的邀请码", + "", + "§e点击输入")); + } String ugLabel = "§7(玩家)"; UserGroupManager.UserGroupConfig ugCfg = plugin.getUserGroup().getHighestGroup(p.getName()); diff --git a/src/main/java/Sdf1_login/InviteManager.java b/src/main/java/Sdf1_login/InviteManager.java index b81ab36..359b288 100644 --- a/src/main/java/Sdf1_login/InviteManager.java +++ b/src/main/java/Sdf1_login/InviteManager.java @@ -21,37 +21,51 @@ public class InviteManager { return code; } - public void onInviteeCheckIn(String inviteeName) { - String invitedBy = (String) plugin.getDb() - .getField(inviteeName, "invited_by"); - if (invitedBy == null || invitedBy.isEmpty()) - return; + /** + * ★ 第九批(2026-10-12):绑定邀请码成功时立即计入邀请人 + * 「本月邀请」计数(带月度重置)。 + * + *

旧逻辑把计数放在 onInviteeCheckIn(被邀请人签到)里, + * 导致「绑定成功但未签到」时 GUI 恒显示「已邀请 0 人」(用户反馈 + * 的计数 bug 根因)。现改为绑定即计数——绑定才是「邀请成功」 + * 的真正时点,与债券发放同一时机,语义一致。

+ * + * @param inviter 邀请人(被计数方) + * @param invitee 被邀请人(绑定者) + */ + public void onInviteeBind(String inviter, String invitee) { + if (inviter == null || inviter.isEmpty()) return; String month = new SimpleDateFormat("yyyy-MM") .format(new Date()); String lastMonth = (String) plugin.getDb() - .getField(invitedBy, "invite_month"); + .getField(inviter, "invite_month"); if (!month.equals(lastMonth)) { plugin.getDb().setField( - invitedBy, "invite_month", month); + inviter, "invite_month", month); plugin.getDb().setField( - invitedBy, "monthly_invite_count", 0); + inviter, "monthly_invite_count", 0); } int count = ((Number) plugin.getDb() - .getField(invitedBy, + .getField(inviter, "monthly_invite_count")) .intValue(); plugin.getDb().setField( - invitedBy, "monthly_invite_count", + inviter, "monthly_invite_count", count + 1); + } - // ★ 第八批(2026-10-12):旧「签到给邀请人积分提成」废弃—— - // 原 bonus=(int)(1*rate) 恒为 0(rate 最大 0.60,截断后为 0), - // 奖励从未实际发放,即用户反馈「邀请好友无效」的根因; - // 邀请奖励统一在绑定邀请码时按 邀请好友赢债券/规则.md 发债券 - // (CommissionManager.payInviteBond)。此处只保留月度邀请计数。 + /** + * ★ 第九批:签到链路不再重复计数——「本月邀请」计数已在 + * 绑定邀请码时由 onInviteeBind 完成(见上)。若此处再 +1, + * 同一被邀请人每签到一次都会重复累加,造成计数虚高。 + * 方法保留以兼容既有三处签到调用点(Main.dailySign 等), + * 但已清空计数逻辑。 + */ + public void onInviteeCheckIn(String inviteeName) { + // 计数已迁移至 onInviteeBind(绑定时),签到不再计数 } public List> getAllInviteData() { diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index 7534cc1..19d5f04 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -7256,11 +7256,21 @@ public class Main extends JavaPlugin p.sendMessage("§e邀请码: " + code); gui.openInvite(p); } else if (slot == 16) { - chatInput.getState(p).type = - ChatInputManager.InputType - .INVITE_INPUT_CODE; - p.closeInventory(); - p.sendMessage("§e请输入邀请码:"); + // ★ 第九批:已绑定邀请人则拦截,不再进入输入 + Object boundInvite = db.getField( + p.getName(), "invited_by"); + if (boundInvite != null && !boundInvite + .toString().isEmpty()) { + p.sendMessage("§c你已绑定邀请人(" + + boundInvite + + "),邀请码仅能使用一次"); + } else { + chatInput.getState(p).type = + ChatInputManager.InputType + .INVITE_INPUT_CODE; + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } } return; } diff --git a/src/main/resources/邀请好友赢债券/规则.md b/src/main/resources/邀请好友赢债券/规则.md index 0feb8a0..78fe7e5 100644 --- a/src/main/resources/邀请好友赢债券/规则.md +++ b/src/main/resources/邀请好友赢债券/规则.md @@ -1,20 +1,23 @@ # 邀好友赢债券活动配置 ## ----配置区开始---- - -| 键 | 解释说明 | 值 | -|:---|:---|:---:| -| 单次邀请奖励上限 | 单个玩家每次成功邀请一位好友时,**立即结算**可获得的债券数量上限。支持小数或整数,填写纯数字即可,单位默认为"个"。适用于"马上邀、马上结账"的场景。 | `10` | -| 单日邀请奖励上限 | 单个玩家在**一天内**通过邀请好友累计可领取的债券最大总数。超出后当日不再发放奖励,次日自动重置。适合控制薅羊毛行为。 | `100` | +| 项目 | 解释 | 值 | +| :---: | :---: | :---: | +| 邀请人单次奖励 | 邀请人每次成功邀请一位好友后,立即结算获得的债券数量。数值填写为纯数字,单位默认为"个"。适用于"马上邀、马上结账"的场景。 | 10 | +| 被邀请人单次奖励 | 被邀请人在注册并首次上线后,立即结算获得的债券数量。数值填写为纯数字,用于吸引新玩家加入。 | 5 | +| 单日邀请奖励上限 | 单个邀请人在**一天内**通过邀请好友累计可获得的债券总数上限。达到上限后当日不再发放奖励,次日自动重置。适合控制薅羊毛行为。 | 100 | +| 单日邀请翻倍额度 | 拉了多少个人以后,给邀请人的奖励。参数值为:"每邀请X人,奖金翻Y倍" | 每邀请2人,奖金翻1倍 | ## ----配置区域结束---- -## 邀好友赢债券活动说明书 -- 什么是邀好友赢债券:玩家通过邀请新玩家加入服务器,完成任务后获得债券奖励,激励玩家拉新。
-- 两种上限的区别: - - **单次邀请奖励上限**:每次邀请成功结算时的单次奖励额度,不会累积 - - **单日邀请奖励上限**:当天所有邀请累计的总奖励上限,达到后当天不再发奖 +## 邀好友赢债券说明书 +- 什么是邀好友赢债券:玩家邀请新玩家加入服务器,双方完成任务后分别获得债券奖励,激励玩家拉新。
+- 三种配置的区别: + - **邀请人单次奖励**:每次成功邀请后,邀请人立即获得的债券数,不累积 + - **被邀请人单次奖励**:被邀请人注册并首次上线后,自己立即获得的债券数 + - **单日邀请奖励上限**:邀请人一天内累计获得的总奖励上限(仅针对邀请人),达到后当日停止发放,次日重置 - 计算公式示例: - - 单次邀请奖励上限 = 5,单人当天邀请 3 人 → 累计获得 5 × 3 = 15 债券 - - 单日邀请奖励上限 = 30,单人当天邀请 7 人 → 理论应得 5 × 7 = 35,但被单日上限卡住,实际获得 30 债券,当日不再发放 + - 邀请人单次奖励 = 10,单人当天邀请 3 人 → 累计获得 10 × 3 = **30 债券**(未超单日上限) + - 邀请人单次奖励 = 10,单人当天邀请 11 人 → 理论应得 10 × 11 = 110,但被单日上限卡住,实际获得 **100 债券**,当日不再发放 + - 被邀请人单次奖励 = 5,新玩家注册首次上线 → 立即获得 **5 债券**