第十批:邀请存量数据自愈(回填绑定时间+重算月度计数)、本月邀请头颅明细GUI、修复Commission跨库查询失效(bond_transaction在bond.db)
Check Source / check (push) Successful in 5s
Auto Release / build (push) Successful in 2m34s
Simple Test / test (push) Successful in 2s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-12 17:44:37 +08:00
parent 21c61e9e5c
commit 4eec0c9a39
5 changed files with 284 additions and 7 deletions
+10
View File
@@ -53,6 +53,16 @@ public class BondManager {
initDB();
}
/**
* ★ 第十批(2026-10-12):暴露 bond.db 连接。
* bond_transaction 在 bond.db(财务债券/),不在 login.db——
* 跨库查询必须走这里(CommissionManager 日上限/翻倍统计、
* InviteManager 存量绑定时间回填均依赖)。
*/
public Connection getConnection() {
return db;
}
// ======================== 数据库初始化 ========================
private void initDB() {
@@ -239,7 +239,7 @@ public class CommissionManager {
.atStartOfDay(ZoneId.systemDefault())
.toInstant().toEpochMilli();
try {
PreparedStatement ps = main.getDb()
PreparedStatement ps = main.getBonds()
.getConnection().prepareStatement(
"SELECT COALESCE(SUM(amount),0) "
+ "FROM bond_transaction "
@@ -266,7 +266,7 @@ public class CommissionManager {
.atStartOfDay(ZoneId.systemDefault())
.toInstant().toEpochMilli();
try {
PreparedStatement ps = main.getDb()
PreparedStatement ps = main.getBonds()
.getConnection().prepareStatement(
"SELECT COUNT(*) "
+ "FROM bond_transaction "
+70 -5
View File
@@ -9,6 +9,7 @@ import org.bukkit.event.inventory.*;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.persistence.PersistentDataType;
@@ -43,6 +44,8 @@ public class GUIManager implements Listener {
"§6§l管理员面板";
public static final String T_INVITE =
"§6§l邀请数据";
public static final String T_MONTHLY_INVITE =
"§6§l本月邀请";
public static final String T_USER_MGMT =
"§6§l用户管理";
public static final String T_USER_GROUP =
@@ -2419,10 +2422,9 @@ public class GUIManager implements Listener {
if (code == null || code.isEmpty()) {
code = plugin.getInvite().generateCode(p);
}
int monthly = ((Number) plugin.getDb()
.getField(p.getName(),
"monthly_invite_count"))
.intValue();
// ★ 第十批:计数改派生口径(invited_at 回填后与头颅明细同源、跨月自愈)
int monthly = plugin.getInvite()
.getMonthlyInviteCount(p.getName());
g.setItem(10, mkItem(Material.PAPER,
"§b§l我的邀请码",
"§7" + code,
@@ -2430,7 +2432,9 @@ public class GUIManager implements Listener {
"§e点击刷新邀请码"));
g.setItem(12, mkItem(Material.PLAYER_HEAD,
"§a本月邀请",
"§7已邀请 §e" + monthly + " §7人"));
"§7已邀请 §e" + monthly + " §7人",
"",
"§e点击查看邀请明细"));
// ★ 第九批:已绑定邀请人则显示「邀请成功时间+邀请人」,
// 否则显示「输入邀请码」按钮
String invitedBy = (String) plugin.getDb()
@@ -2470,6 +2474,67 @@ public class GUIManager implements Listener {
p.openInventory(g);
}
/**
* ★ 第十批(2026-10-12):「本月邀请」明细 GUI——
* 本月绑定我邀请码的玩家,一人一个真实玩家头颅。
* 标题 T_MONTHLY_INVITE 已加入 Main.onInvClick 排除列表,
* 点击全部由该分支自行 cancel(防拿走头颅)。
*/
public void openMonthlyInvite(Player p) {
Inventory g = Bukkit.createInventory(
null, 54, T_MONTHLY_INVITE);
fillBg(g);
List<Map<String, Object>> list = plugin.getInvite()
.getMonthlyInvitedPlayers(p.getName());
if (list.isEmpty()) {
g.setItem(22, mkItem(Material.BARRIER,
"§c本月暂无邀请记录",
"§7邀请好友绑定你的邀请码后",
"§7这里会展示他们的头颅"));
} else {
// 内容区 0-44 共 45 格;超出 45 人时留 1 格提示
int limit = Math.min(list.size(),
list.size() > 45 ? 44 : 45);
for (int i = 0; i < limit; i++) {
Map<String, Object> row = list.get(i);
String name = String.valueOf(row.get("name"));
long at = ((Number) row.get("at")).longValue();
ItemStack head = new ItemStack(
Material.PLAYER_HEAD);
SkullMeta sm = (SkullMeta)
head.getItemMeta();
if (sm != null) {
// 在线优先用精确档案;离线按玩家名解析真实皮肤
Player target =
Bukkit.getPlayerExact(name);
if (target != null) {
sm.setOwningPlayer(target);
} else {
sm.setOwner(name);
}
sm.setDisplayName("§e§l" + name);
sm.setLore(Arrays.asList(
"§7绑定时间: §e"
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm")
.format(new java.util.Date(at)),
"",
"§7已计入你的本月邀请"));
head.setItemMeta(sm);
}
g.setItem(i, head);
}
if (list.size() > 45) {
g.setItem(44, mkItem(Material.PAPER,
"§e还有 " + (list.size() - 44)
+ " 人未显示"));
}
}
g.setItem(49, mkItem(Material.ARROW,
"§7返回", "§7返回「邀请数据」"));
p.openInventory(g);
}
private String getInviteRate(int count) {
if (count > 15) return "60%";
+187
View File
@@ -89,4 +89,191 @@ public class InviteManager {
}
return result;
}
// ==================== 第十批(2026-10-12)存量自愈 + 派生口径 ====================
/** 本月 1 号 00:00(本地时区)毫秒时间戳 */
private long monthStartTime() {
Calendar c = Calendar.getInstance();
c.set(Calendar.DAY_OF_MONTH, 1);
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
return c.getTimeInMillis();
}
private static String str(Object o) {
return o == null ? "" : o.toString();
}
private static long lng(Object o) {
return (o instanceof Number)
? ((Number) o).longValue() : 0L;
}
/**
* ★ 第十批(2026-10-12):存量邀请数据自愈,
* onEnable 启动时调用一次(幂等)。
*
* <p>① 回填 invited_at=0 的存量绑定时间——绑定功能自项目
* 初始化(2026-05)就存在但没记时间,用户 GUI 看到
* 「绑定时间: 未知」。回填优先级:</p>
* <ol>
* <li>bond_transaction type='invitee_reward'
* (第九批起被邀请人收款,time=绑定时刻,最精确)</li>
* <li>bond_transaction type='invite_reward' 且
* target_player=被邀请人(第八批起邀请人收款)</li>
* <li>注册时间(早期绑定大多发生在注册前后,最佳代理)</li>
* <li>最后登录时间 → 当前时间</li>
* </ol>
*
* <p>② 按回填后的口径重算 monthly_invite_count / invite_month
* ——修复存量绑定不计入导致的「已邀请 0 人」。</p>
*/
public void reconcileInviteData() {
try {
List<Map<String, Object>> users =
plugin.getDb().getAllUsers();
long now = System.currentTimeMillis();
int fixedAt = 0;
// ---- ① 回填存量绑定时间 ----
for (Map<String, Object> u : users) {
String name = str(u.get("player_name"));
if (name.isEmpty()) continue;
if (str(u.get("invited_by")).isEmpty()) continue;
if (lng(u.get("invited_at")) > 0) continue;
long ts = lookupBondTx(
"player_name", name,
"invitee_reward");
if (ts <= 0) ts = lookupBondTx(
"target_player", name,
"invite_reward");
if (ts <= 0) ts = lng(u.get("register_time"));
if (ts <= 0) ts = lng(u.get("last_login_time"));
if (ts <= 0) ts = now;
plugin.getDb().setField(
name, "invited_at", ts);
u.put("invited_at", ts);
fixedAt++;
}
// ---- ② 重算月度计数(仅维护邀请人,与 GUI 派生口径同源) ----
long monthStart = monthStartTime();
Map<String, Integer> counter = new HashMap<>();
for (Map<String, Object> u : users) {
String inviter = str(u.get("invited_by"));
if (inviter.isEmpty()) continue;
if (lng(u.get("invited_at")) >= monthStart) {
counter.merge(inviter, 1, Integer::sum);
}
}
String curMonth = new SimpleDateFormat("yyyy-MM")
.format(new Date());
int fixedCnt = 0;
for (Map<String, Object> u : users) {
String name = str(u.get("player_name"));
if (name.isEmpty()) continue;
// 只维护邀请人(有邀请码的玩家)
if (str(u.get("invite_code")).isEmpty()) {
continue;
}
int want = counter.getOrDefault(name, 0);
if (lng(u.get("monthly_invite_count")) != want) {
plugin.getDb().setField(
name, "monthly_invite_count",
want);
fixedCnt++;
}
if (!curMonth.equals(
str(u.get("invite_month")))) {
plugin.getDb().setField(
name, "invite_month", curMonth);
}
}
if (fixedAt > 0 || fixedCnt > 0) {
plugin.getLogger().info(
"[Invite] 存量自愈: 回填绑定时间 "
+ fixedAt + " 条, 重算月度计数 "
+ fixedCnt + " 人");
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Invite] 存量邀请数据自愈失败: "
+ e.getMessage());
}
}
/**
* 在 bond.db 的 bond_transaction 里查该玩家某类流水的最早时间
* (跨库:必须走 BondManager.getConnection(),
* bond_transaction 不在 login.db)
*/
private long lookupBondTx(String col, String player,
String type) {
try {
java.sql.PreparedStatement ps = plugin.getBonds()
.getConnection().prepareStatement(
"SELECT MIN(time) "
+ "FROM bond_transaction "
+ "WHERE type=? AND "
+ col + "=?");
ps.setString(1, type);
ps.setString(2, player);
java.sql.ResultSet rs = ps.executeQuery();
long t = rs.next() ? rs.getLong(1) : 0L;
rs.close();
ps.close();
// 兼容以秒为单位的历史行
if (t > 0 && t < 1000000000000L) t *= 1000L;
return t;
} catch (Exception e) {
return 0L;
}
}
/**
* ★ 第十批:派生口径的「本月邀请人数」——
* 按 invited_at >= 本月 1 号实时统计(GUI 显示用,
* 跨月自愈,不依赖增量列 monthly_invite_count)。
*/
public int getMonthlyInviteCount(String inviter) {
if (inviter == null || inviter.isEmpty()) return 0;
long monthStart = monthStartTime();
int n = 0;
for (Map<String, Object> u
: plugin.getDb().getAllUsers()) {
if (!inviter.equals(
str(u.get("invited_by")))) continue;
if (lng(u.get("invited_at")) >= monthStart) n++;
}
return n;
}
/**
* ★ 第十批:本月绑定该玩家邀请码的玩家列表(头颅明细 GUI)。
*
* @return 每项含 name(被邀请人)与 at(绑定时间戳),
* 按绑定时间倒序
*/
public List<Map<String, Object>> getMonthlyInvitedPlayers(
String inviter) {
List<Map<String, Object>> result =
new ArrayList<>();
if (inviter == null || inviter.isEmpty()) return result;
long monthStart = monthStartTime();
for (Map<String, Object> u
: plugin.getDb().getAllUsers()) {
if (!inviter.equals(
str(u.get("invited_by")))) continue;
long ts = lng(u.get("invited_at"));
if (ts < monthStart) continue;
Map<String, Object> row = new LinkedHashMap<>();
row.put("name", str(u.get("player_name")));
row.put("at", ts);
result.add(row);
}
result.sort((a, b) -> Long.compare(
lng(b.get("at")), lng(a.get("at"))));
return result;
}
}
+15
View File
@@ -623,6 +623,9 @@ public class Main extends JavaPlugin
webManager.registerTransactionListener(bondManager);
}
// ★ 第十批:存量邀请数据自愈(回填 invited_at + 重算月度计数)
invite.reconcileInviteData();
cdkManager = new CDKManager(this);
userGroupManager = new UserGroupManager(this);
cypayCommand = new CypayCommand(this);
@@ -6604,6 +6607,8 @@ public class Main extends JavaPlugin
&& !title.equals(GUIManager.T_MY_INFO)
&& !title.equals(GUIManager.T_INVITE)
&& !title.equals(
GUIManager.T_MONTHLY_INVITE)
&& !title.equals(
GUIManager.T_TASK_CENTER)
&& !title.equals(
GUIManager.T_GIFT_STAGES)
@@ -7255,6 +7260,9 @@ public class Main extends JavaPlugin
invite.generateCode(p);
p.sendMessage("§e邀请码: " + code);
gui.openInvite(p);
} else if (slot == 12) {
// ★ 第十批:「本月邀请」→ 头颅明细 GUI
gui.openMonthlyInvite(p);
} else if (slot == 16) {
// ★ 第九批:已绑定邀请人则拦截,不再进入输入
Object boundInvite = db.getField(
@@ -7275,6 +7283,13 @@ public class Main extends JavaPlugin
return;
}
// ==================== 本月邀请明细(第十批) ====================
if (title.equals(GUIManager.T_MONTHLY_INVITE)) {
e.setCancelled(true);
if (slot == 49) gui.openInvite(p);
return;
}
// ==================== 用户管理 ====================
if (title.equals(GUIManager.T_USER_MGMT)) {
e.setCancelled(true);