第九批:邀请系统计数修复+绑定显示邀请人+双向奖励+配置懒更新
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m46s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

1. 计数修复:monthly_invite_count 改为绑定邀请码时由 onInviteeBind 计入
   (旧逻辑在被邀请人签到才计数,导致绑定成功但未签到时 GUI 恒显示已邀请0人);
   onInviteeCheckIn 清空计数逻辑避免重复累加。

2. B端显示:新增 invited_at 列(毫秒时间戳落库),邀请数据GUI slot16
   已绑定时改显示「邀请成功时间(yyyy-MM-dd HH:mm)+邀请人名字」,
   未绑定才显示输入邀请码按钮;点击已绑定则拦截。

3. 双向奖励:CommissionManager 适配新规则md 4键(邀请人单次奖励/
   被邀请人单次奖励/单日邀请奖励上限/单日邀请翻倍额度);
   payInviteBond 给邀请人(含每邀请X人翻Y倍)、payInviteeBond 给被邀请人。

4. 配置懒更新:玩家绑定邀请码时调用 loadRules() 实时重载规则md,
   不再依赖启动加载或定时热重载。
This commit is contained in:
youpaishidifu
2026-10-12 17:00:14 +08:00
parent df56e2c293
commit 21c61e9e5c
7 changed files with 241 additions and 59 deletions
+23 -3
View File
@@ -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
+129 -19
View File
@@ -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)。
*
* <p>★ 2026-10-12 第八批(用户决策):旧「提成规则.txt」(邀请人数:提成百分比)
* 体系整体废弃——其 payCommission 因 Economy 移除早已是无调用死代码,
* 签到链路奖励计算 (int)(1*rate) 恒为 0 从未生效(「邀请好友无效」根因)。
* 代码层面删除旧 txt(启动时清理而非 SFTP 手删——否则 writeDefault
* 会再次释放,写文件逻辑已一并移除),奖励改由新 md 规则控制:
* 绑定邀请码成功时立即结算债券,受「单次邀请奖励上限」「单日邀请奖励上限」
* 双重限制(当日累计按 bond_transaction type='invite_reward' 汇总,自然日重置)。</p>
* <p>★ 2026-10-12 第八批(用户决策):旧「提成规则.txt」体系整体废弃,
* 奖励改由新 md 规则控制,绑定邀请码成功时立即结算债券。</p>
*
* <p>★ 2026-10-12 第九批(用户需求)三点改造:
* ① 解析键改用新 md 4 键(旧「单次邀请奖励上限」→「邀请人单次奖励」),
* 新增「被邀请人单次奖励」「单日邀请翻倍额度」;
* ② 奖励单向改<b>双向</b>——payInviteBond 给邀请人、payInviteeBond 给被邀请人;
* 翻倍额度作用于邀请人(每邀请 X 人,奖金翻 Y 倍);
* ③ 支持<b>懒更新</b>——玩家绑定邀请码时调用 loadRules() 实时重载规则,
* 不再依赖启动时一次性加载或定时热重载。</p>
*/
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:每次按单次上限结算,
* 邀请绑定成功 → 给<b>邀请人</b>发债券(★ 第九批:含翻倍额度)。
* 规则见 邀请好友赢债券/规则.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 点起该玩家已发放的邀请债券总数 */
/**
* 邀请绑定成功 → 给<b>被邀请人</b>发债券(★ 第九批:双向奖励)。
* 被邀请人奖励为固定单次数额,不受单日上限约束(上限仅针对邀请人)。
*
* @param invitee 被邀请人(绑定者,收款方)
* @param inviter 邀请人(被邀请人的邀请来源)
* @return &gt;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;
}
}
/**
* 判断金额是否包含经济内容
*/
@@ -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");
+25 -5
View File
@@ -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());
+29 -15
View File
@@ -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):绑定邀请码成功时立即计入邀请人
* 「本月邀请」计数(带月度重置)。
*
* <p>旧逻辑把计数放在 onInviteeCheckIn(被邀请人签到)里,
* 导致「绑定成功但未签到」时 GUI 恒显示「已邀请 0 人」(用户反馈
* 的计数 bug 根因)。现改为绑定即计数——绑定才是「邀请成功」
* 的真正时点,与债券发放同一时机,语义一致。</p>
*
* @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<Map<String, Object>> getAllInviteData() {
+15 -5
View File
@@ -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;
}
@@ -1,20 +1,23 @@
# 邀好友赢债券活动配置
## ----配置区开始----
| 键 | 解释说明 | 值 |
|:---|:---|:---:|
| 单次邀请奖励上限 | 单个玩家每次成功邀请一位好友时,**立即结算**可获得的债券数量上限。支持小数或整数,填写纯数字即可,单位默认为"个"。适用于"马上邀、马上结账"的场景。 | `10` |
| 单日邀请奖励上限 | 单个玩家在**一天内**通过邀请好友累计可领取的债券最大总数。超出后当日不再发放奖励,次日自动重置。适合控制薅羊毛行为。 | `100` |
| 项目 | 解释 | 值 |
| :---: | :---: | :---: |
| 邀请人单次奖励 | 邀请人每次成功邀请一位好友后,立即结算获得的债券数量。数值填写为纯数字,单位默认为"个"。适用于"马上邀、马上结账"的场景。 | 10 |
| 被邀请人单次奖励 | 被邀请人在注册并首次上线后,立即结算获得的债券数量。数值填写为纯数字,用于吸引新玩家加入。 | 5 |
| 单日邀请奖励上限 | 单个邀请人在**一天内**通过邀请好友累计可获得的债券总数上限。达到上限后当日不再发放奖励,次日自动重置。适合控制薅羊毛行为。 | 100 |
| 单日邀请翻倍额度 | 拉了多少个人以后,给邀请人的奖励。参数值为:"每邀请X人,奖金翻Y倍" | 每邀请2人,奖金翻1倍 |
## ----配置区域结束----
## 邀好友赢债券活动说明书
- 什么是邀好友赢债券:玩家通过邀请新玩家加入服务器,完成任务后获得债券奖励,激励玩家拉新。<br>
- 两种上限的区别:
- **单次邀请奖励上限**:每次邀请成功结算时的单次奖励额度,不会累积
- **单日邀请奖励上限**:当天所有邀请累计的总奖励上限,达到后当天不再发奖
## 邀好友赢债券说明书
- 什么是邀好友赢债券:玩家邀请新玩家加入服务器,双方完成任务后分别获得债券奖励,激励玩家拉新。<br>
- 三种配置的区别:
- **邀请人单次奖励**:每次成功邀请后,邀请人立即获得的债券数,不累积
- **被邀请人单次奖励**:被邀请人注册并首次上线后,自己立即获得的债券数
- **单日邀请奖励上限**:邀请人一天内累计获得的总奖励上限(仅针对邀请人),达到后当日停止发放,次日重置
- 计算公式示例:
- 单次邀请奖励上限 = 5,单人当天邀请 3 人 → 累计获得 5 × 3 = 15 债券
- 单日邀请奖励上限 = 30,单人当天邀请 7 人 → 理论应得 5 × 7 = 35,但被单日上限卡住,实际获得 30 债券,当日不再发放
- 邀请人单次奖励 = 10,单人当天邀请 3 人 → 累计获得 10 × 3 = **30 债券**(未超单日上限)
- 邀请人单次奖励 = 10,单人当天邀请 11 人 → 理论应得 10 × 11 = 110,但被单日上限卡住,实际获得 **100 债券**,当日不再发放
- 被邀请人单次奖励 = 5,新玩家注册首次上线 → 立即获得 **5 债券**