新增3个功能:领地改名冷却、封禁名单联控、封禁广播警告
1. 领地改名冷却机制:1小时限5次,自然滚动窗口 - AreaProtection: renameLand/renameCooldownMap/getRenameCooldown/getRemainingRenames - CLI: /protect cli rename <领地名> 交互式输入 - GUI: Name Tag按钮(slot 33)显示冷却状态 - 改名更新DB+内存+触发PHP同步 2. PHP用户管理显示封禁名单(联控Java BanList) - WebManager.syncBans(): 收集Bukkit名字/IP封禁→推送到PHP - PHP: web_player_bans表 + handleSyncBans + list_bans API - admin.php: 用户行红色+禁图标标注被封禁玩家 3. 封禁事件公屏广播警告 - Main.applyBan(): 封禁后自动广播 - broadcastBanWarning(): 玩家名+IP(如IP封禁)+理由+到期时间 - loadBanWarningMessage(): 从释放警告消息.txt随机读取标语 - 支持永久封禁/限时封禁/IP封禁3种场景
This commit is contained in:
@@ -70,7 +70,7 @@ public class AreaCLIManager {
|
||||
return Component.text(text).color(color);
|
||||
}
|
||||
|
||||
private Component header(String title) {
|
||||
public static Component header(String title) {
|
||||
return Component.text("\n§6§l═══════ " + title + " ═══════\n");
|
||||
}
|
||||
|
||||
@@ -336,6 +336,19 @@ public class AreaCLIManager {
|
||||
// ★ 新增:领地公告管理
|
||||
p.sendMessage(clickableAction("📢", "领地公告(进出提示消息)",
|
||||
"/protect cli announcement " + land.name + " 1"));
|
||||
// ★ 新增:领地改名
|
||||
long[] cooldown = areaProtect.getRenameCooldown(p);
|
||||
String renameHint;
|
||||
if (cooldown != null) {
|
||||
long remainMin = cooldown[0] / 60;
|
||||
long remainSecR = cooldown[0] % 60;
|
||||
renameHint = "§c冷却中(已用" + cooldown[1] + "/" + 5 + "次,"
|
||||
+ remainMin + "分" + remainSecR + "秒后解锁)";
|
||||
} else {
|
||||
renameHint = "§a点击输入新名字(1小时限5次)";
|
||||
}
|
||||
p.sendMessage(clickableAction("✏️", "领地改名",
|
||||
"/protect cli rename " + land.name));
|
||||
|
||||
p.sendMessage(Component.empty()
|
||||
.append(Component.text("§a[◀ 返回列表]")
|
||||
|
||||
@@ -245,6 +245,19 @@ public class AreaGUIManager implements Listener {
|
||||
"§7将领地所有权转移给他人",
|
||||
"§7需要有领地所有者权限"));
|
||||
|
||||
// 领地改名(位置33)
|
||||
long[] cd = areaProtect.getRenameCooldown(p);
|
||||
String renameLore;
|
||||
if (cd != null) {
|
||||
long rm = cd[0] / 60;
|
||||
long rs = cd[0] % 60;
|
||||
renameLore = "§c冷却中(已用" + cd[1] + "/5次)\n§c" + rm + "分" + rs + "秒后解锁";
|
||||
} else {
|
||||
renameLore = "§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5\n§71小时限5次,自然滚动窗口";
|
||||
}
|
||||
inv.setItem(33, createItem(Material.NAME_TAG, "§e§l领地改名",
|
||||
renameLore, "", "§e点击输入新名字"));
|
||||
|
||||
// 返回按钮(位置48)
|
||||
inv.setItem(48, createItem(Material.ARROW, "§c§l返回领地列表", ""));
|
||||
|
||||
@@ -941,6 +954,22 @@ public class AreaGUIManager implements Listener {
|
||||
} else if (raw == 32) {
|
||||
// 过户领地
|
||||
openTransferLandPanel(p, landName);
|
||||
} else if (raw == 33) {
|
||||
// ★ 领地改名
|
||||
p.closeInventory();
|
||||
areaProtect.pendingEffectInput.put(p.getUniqueId(),
|
||||
new String[]{landName, "rename_land"});
|
||||
p.sendMessage(AreaCLIManager.header("领地改名: " + landName));
|
||||
long[] cooldownInfo = areaProtect.getRenameCooldown(p);
|
||||
if (cooldownInfo != null) {
|
||||
long rm = cooldownInfo[0] / 60;
|
||||
long rs = cooldownInfo[0] % 60;
|
||||
p.sendMessage("§c冷却中!已用" + cooldownInfo[1] + "/5次," + rm + "分" + rs + "秒后解锁");
|
||||
} else {
|
||||
p.sendMessage("§7剩余次数: §a" + areaProtect.getRemainingRenames(p) + "/5");
|
||||
}
|
||||
p.sendMessage("§e请在聊天栏输入新的领地名:");
|
||||
p.sendMessage("§7输入 §c取消 §7放弃修改");
|
||||
} else if (raw == 31) {
|
||||
// 传送 - 优先传送点,否则安全搜索中心
|
||||
p.closeInventory();
|
||||
|
||||
@@ -101,11 +101,15 @@ public class AreaProtection implements Listener {
|
||||
// ★ 添加成员输入等待:UUID → 领地名
|
||||
private final Map<java.util.UUID, String> pendingAddMemberInput = new ConcurrentHashMap<>();
|
||||
// ★ 效果管理待处理输入:UUID → [landName, inputType]
|
||||
private final Map<java.util.UUID, String[]> pendingEffectInput = new ConcurrentHashMap<>();
|
||||
public final Map<java.util.UUID, String[]> pendingEffectInput = new ConcurrentHashMap<>();
|
||||
// ★ 过户领地待输入新所有者:UUID → [landName]
|
||||
public final Map<java.util.UUID, String[]> pendingTransferInput = new ConcurrentHashMap<>();
|
||||
// ★ 用户组配置编辑待输入:UUID → [groupName, field("price"|"maxlands"|"priority")]
|
||||
public final Map<java.util.UUID, String[]> pendingGroupEditInput = new ConcurrentHashMap<>();
|
||||
// ★ 领地改名冷却:UUID → 改名时间戳列表(自然滚动窗口,1小时限5次)
|
||||
private final Map<java.util.UUID, java.util.List<Long>> renameCooldownMap = new ConcurrentHashMap<>();
|
||||
private static final int RENAME_LIMIT = 5;
|
||||
private static final long RENAME_WINDOW_MS = 3600_000L; // 1小时
|
||||
|
||||
// ★ 全局配置默认值
|
||||
private int globalCreatePricePerSqm = 10; // 每㎡创建价格
|
||||
@@ -6271,6 +6275,42 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
plugin.areaCLIManager.showAnnouncementManagement(p, args[2], annPage);
|
||||
break;
|
||||
case "rename":
|
||||
// ★ 领地改名(交互式输入)
|
||||
if (args.length < 3) {
|
||||
p.sendMessage("§c用法: /protect cli rename <领地名>");
|
||||
break;
|
||||
}
|
||||
{
|
||||
String rnLandName = args[2];
|
||||
AreaConfig rnAc = getLand(rnLandName);
|
||||
if (rnAc == null) {
|
||||
p.sendMessage("§c领地不存在: " + rnLandName);
|
||||
break;
|
||||
}
|
||||
boolean rnOwner = p.getName().equalsIgnoreCase(rnAc.owner);
|
||||
boolean rnAdmin = isAreaAdmin(p);
|
||||
if (!rnOwner && !rnAdmin) {
|
||||
p.sendMessage("§c需要领地所有者或管理员权限");
|
||||
break;
|
||||
}
|
||||
// 设置等待输入状态
|
||||
pendingEffectInput.put(p.getUniqueId(),
|
||||
new String[]{rnLandName, "rename_land"});
|
||||
p.sendMessage(AreaCLIManager.header("领地改名: " + rnLandName));
|
||||
long[] cd = getRenameCooldown(p);
|
||||
if (cd != null) {
|
||||
long rm = cd[0] / 60;
|
||||
long rs = cd[0] % 60;
|
||||
p.sendMessage(Component.text("§c冷却中!已用" + cd[1] + "/" + RENAME_LIMIT + "次,"
|
||||
+ rm + "分" + rs + "秒后解锁"));
|
||||
} else {
|
||||
p.sendMessage(Component.text("§7剩余次数: §a" + getRemainingRenames(p) + "/" + RENAME_LIMIT));
|
||||
}
|
||||
p.sendMessage(Component.text("§e请在聊天栏输入新的领地名:"));
|
||||
p.sendMessage(Component.text("§7输入 §c取消 §7放弃修改"));
|
||||
}
|
||||
break;
|
||||
case "create":
|
||||
// 跳转到创建命令
|
||||
// 直接转发
|
||||
@@ -8896,6 +8936,120 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 领地改名(带冷却检查)
|
||||
* @return null=成功, 非null=错误/冷却信息
|
||||
*/
|
||||
public String renameLand(Player p, String oldName, String newName) {
|
||||
// 1. 基本校验
|
||||
if (newName == null || newName.trim().isEmpty()) return "新名字不能为空";
|
||||
newName = newName.trim();
|
||||
if (newName.length() > 20) return "名字不能超过20个字符";
|
||||
if (newName.contains(" ") || newName.contains("§")) return "名字不能包含空格或颜色代码";
|
||||
if (newName.equalsIgnoreCase(oldName)) return "新名字和旧名字相同";
|
||||
|
||||
// 2. 重名检查
|
||||
if (getLand(newName) != null) return "已存在同名领地: " + newName;
|
||||
|
||||
// 3. 冷却检查
|
||||
long now = System.currentTimeMillis();
|
||||
java.util.List<Long> timestamps = renameCooldownMap.computeIfAbsent(
|
||||
p.getUniqueId(), k -> new java.util.ArrayList<>());
|
||||
|
||||
// 清除过期记录(超出1小时窗口的)
|
||||
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
|
||||
|
||||
if (timestamps.size() >= RENAME_LIMIT) {
|
||||
long firstInWindow = timestamps.get(0);
|
||||
long unlockTime = firstInWindow + RENAME_WINDOW_MS;
|
||||
long remainSec = (unlockTime - now) / 1000;
|
||||
long remainMin = remainSec / 60;
|
||||
long remainSecR = remainSec % 60;
|
||||
return "改名冷却中!本小时已改名" + RENAME_LIMIT + "次,"
|
||||
+ remainMin + "分" + remainSecR + "秒后解锁";
|
||||
}
|
||||
|
||||
// 4. 执行改名
|
||||
try {
|
||||
if (dbConnection == null || dbConnection.isClosed()) return "数据库未连接";
|
||||
|
||||
// 4a. 更新 area_lands 表的 name
|
||||
PreparedStatement ps = dbConnection.prepareStatement(
|
||||
"UPDATE area_lands SET name = ? WHERE name = ?");
|
||||
ps.setString(1, newName);
|
||||
ps.setString(2, oldName);
|
||||
int affected = ps.executeUpdate();
|
||||
ps.close();
|
||||
|
||||
if (affected <= 0) return "数据库中未找到领地: " + oldName;
|
||||
|
||||
// 4b. 更新 area_land_permissions 表的 land_id(如果用name关联)
|
||||
// land_id 是 area_lands 的自增 id,改名不影响 id,所以无需更新
|
||||
|
||||
// 4c. 更新内存
|
||||
AreaConfig ac = areas.remove(oldName);
|
||||
if (ac != null) {
|
||||
ac.name = newName;
|
||||
areas.put(newName, ac);
|
||||
}
|
||||
|
||||
// 4d. 更新内存中的成员权限缓存等(如果有以旧名字为key的缓存)
|
||||
// areaPlayerWhitelist 也以 landName 为 key,需同步
|
||||
java.util.Set<String> whitelist = areaPlayerWhitelist.remove(oldName);
|
||||
if (whitelist != null) {
|
||||
areaPlayerWhitelist.put(newName, whitelist);
|
||||
}
|
||||
|
||||
// 4e. 记录冷却
|
||||
timestamps.add(now);
|
||||
|
||||
// 4f. 触发PHP同步
|
||||
try {
|
||||
plugin.webManager.syncLandData();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
plugin.getLogger().info("[防护] 领地改名: " + oldName + " → " + newName + " (by " + p.getName() + ")");
|
||||
return null; // 成功
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[防护] 领地改名失败: " + e.getMessage());
|
||||
return "数据库错误: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 获取改名冷却信息(用于显示在菜单中)
|
||||
* @return [剩余秒数, 已用次数] 或 null(无冷却)
|
||||
*/
|
||||
public long[] getRenameCooldown(Player p) {
|
||||
long now = System.currentTimeMillis();
|
||||
java.util.List<Long> timestamps = renameCooldownMap.get(p.getUniqueId());
|
||||
if (timestamps == null || timestamps.isEmpty()) return null;
|
||||
|
||||
// 清除过期
|
||||
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
|
||||
|
||||
int used = timestamps.size();
|
||||
if (used == 0) return null;
|
||||
|
||||
long firstInWindow = timestamps.get(0);
|
||||
long unlockTime = firstInWindow + RENAME_WINDOW_MS;
|
||||
if (now >= unlockTime) return null; // 已解锁
|
||||
|
||||
long remainSec = (unlockTime - now) / 1000;
|
||||
return new long[]{remainSec, used};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余改名次数
|
||||
*/
|
||||
public int getRemainingRenames(Player p) {
|
||||
long now = System.currentTimeMillis();
|
||||
java.util.List<Long> timestamps = renameCooldownMap.get(p.getUniqueId());
|
||||
if (timestamps == null) return RENAME_LIMIT;
|
||||
timestamps.removeIf(t -> now - t > RENAME_WINDOW_MS);
|
||||
return Math.max(0, RENAME_LIMIT - timestamps.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 PHP 端同步过来的单个字段值直接写入内存 AreaConfig 对象
|
||||
*/
|
||||
@@ -9467,6 +9621,24 @@ public class AreaProtection implements Listener {
|
||||
}
|
||||
saveAreaToDb(land);
|
||||
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showEffectsManagement(p, landName, 3);
|
||||
} else if ("rename_land".equals(inputType)) {
|
||||
// ★ 领地改名输入处理
|
||||
if (message.trim().equals("取消") || message.trim().equals("0")) {
|
||||
p.sendMessage(Component.text("§e已取消改名,保持原样"));
|
||||
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, landName, 1);
|
||||
return true;
|
||||
}
|
||||
String newName = message.trim();
|
||||
String error = renameLand(p, landName, newName);
|
||||
if (error != null) {
|
||||
p.sendMessage(Component.text("§c改名失败: " + error));
|
||||
p.sendMessage(Component.text("§7请重新输入(或输入 §c取消§7)"));
|
||||
pendingEffectInput.put(p.getUniqueId(), new String[]{landName, "rename_land"});
|
||||
return true;
|
||||
}
|
||||
p.sendMessage(Component.text("§a✓ 领地已改名: §f" + landName + " §a→ §f" + newName));
|
||||
if (plugin.areaCLIManager != null) plugin.areaCLIManager.showLandManage(p, newName, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 刷新GUI,回到原来的subPage(必须切主线程)
|
||||
|
||||
@@ -1033,6 +1033,103 @@ public class Main extends JavaPlugin
|
||||
Bukkit.getBanList(type).addBan(target, reason,
|
||||
expireMs == null ? null : new java.util.Date(expireMs),
|
||||
sender.getName());
|
||||
|
||||
// ★ 公屏广播封禁警告
|
||||
broadcastBanWarning(target, type, reason, expireMs, sender.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 封禁事件公屏广播警告
|
||||
* 从"释放警告消息.txt"读取随机标语,结合封禁信息发送给全服
|
||||
*/
|
||||
private void broadcastBanWarning(String target, org.bukkit.BanList.Type type,
|
||||
String reason, Long expireMs, String adminName) {
|
||||
try {
|
||||
// 确定封禁类型描述
|
||||
boolean isIpBan = (type == org.bukkit.BanList.Type.IP);
|
||||
boolean isPerm = (expireMs == null);
|
||||
|
||||
// 如果是IP封禁,获取被封IP对应的在线玩家名
|
||||
String displayTarget = target;
|
||||
String ipInfo = "";
|
||||
if (isIpBan) {
|
||||
// IP封禁:target是IP,尝试查找对应玩家
|
||||
for (org.bukkit.entity.Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.getAddress() != null && p.getAddress().getAddress() != null) {
|
||||
String playerIp = p.getAddress().getAddress().getHostAddress();
|
||||
if (target.equals(playerIp)) {
|
||||
displayTarget = p.getName();
|
||||
ipInfo = ",IP: " + target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayTarget.equals(target)) {
|
||||
// 没找到在线玩家,直接用IP
|
||||
ipInfo = ",IP: " + target;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建封禁描述
|
||||
String banDesc;
|
||||
if (isIpBan) {
|
||||
banDesc = "被永久封禁IP";
|
||||
} else if (isPerm) {
|
||||
banDesc = "被终身封禁";
|
||||
} else {
|
||||
banDesc = "被封禁至" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(expireMs));
|
||||
}
|
||||
|
||||
// 读取警告标语
|
||||
String warningLine = loadBanWarningMessage();
|
||||
|
||||
// 构建消息
|
||||
String msg = "§c§l⚠ " + displayTarget + ipInfo + "因违反服务器规则" + banDesc + "。";
|
||||
if (reason != null && !reason.isEmpty() && !reason.startsWith("§")) {
|
||||
msg += "§r§7理由: " + reason + "。";
|
||||
}
|
||||
msg += "§r§c" + warningLine;
|
||||
|
||||
// 广播
|
||||
for (org.bukkit.entity.Player online : Bukkit.getOnlinePlayers()) {
|
||||
online.sendMessage(msg);
|
||||
}
|
||||
// 控制台也打印
|
||||
getLogger().info("[封禁广播] " + msg.replaceAll("§[0-9a-fk-orA-FK-OR]", ""));
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("[封禁广播] 异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从"释放警告消息.txt"读取随机标语
|
||||
*/
|
||||
private String loadBanWarningMessage() {
|
||||
try {
|
||||
java.io.File file = new java.io.File(getDataFolder(), "释放警告消息.txt");
|
||||
if (!file.exists()) {
|
||||
// 创建默认文件
|
||||
getDataFolder().mkdirs();
|
||||
java.io.FileWriter fw = new java.io.FileWriter(file);
|
||||
fw.write("请大家引以为戒,务必遵守服务器规则。\n");
|
||||
fw.write("请大家务必遵守服务器规则,切勿以身试险。\n");
|
||||
fw.write("请大家务必遵守规则,注意一言一行。\n");
|
||||
fw.close();
|
||||
}
|
||||
java.util.List<String> lines = new java.util.ArrayList<>();
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(new java.io.FileInputStream(file), java.nio.charset.StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) lines.add(line);
|
||||
}
|
||||
br.close();
|
||||
if (lines.isEmpty()) return "请大家遵守服务器规则。";
|
||||
return lines.get(new java.util.Random().nextInt(lines.size()));
|
||||
} catch (Exception e) {
|
||||
return "请大家遵守服务器规则。";
|
||||
}
|
||||
}
|
||||
|
||||
/** 临时封禁(名字或IP),由 /tempban 与 /tempbanip 共用 */
|
||||
|
||||
@@ -3343,6 +3343,9 @@ public class WebManager {
|
||||
// 4. 同步成员权限数据(area_land_permissions → web_area_permissions)
|
||||
syncPermissions(areaProtect);
|
||||
|
||||
// 5. 同步封禁名单到PHP
|
||||
syncBans();
|
||||
|
||||
// 5. ★ 同步插件管理员列表到PHP
|
||||
syncAdmins();
|
||||
|
||||
@@ -3488,6 +3491,63 @@ public class WebManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 同步封禁名单到PHP(供用户管理页面显示封禁状态)
|
||||
*/
|
||||
private void syncBans() {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
org.bukkit.BanList nameBanList = Bukkit.getBanList(org.bukkit.BanList.Type.NAME);
|
||||
org.bukkit.BanList ipBanList = Bukkit.getBanList(org.bukkit.BanList.Type.IP);
|
||||
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
|
||||
// 名字封禁
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<org.bukkit.BanEntry<?>> nameEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) nameBanList.getEntries();
|
||||
for (org.bukkit.BanEntry<?> entry : nameEntries) {
|
||||
if (!first) sb.append(",");
|
||||
first = false;
|
||||
String target = entry.getTarget();
|
||||
String reason = entry.getReason() != null ? entry.getReason() : "";
|
||||
String source = entry.getSource() != null ? entry.getSource() : "";
|
||||
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
|
||||
sb.append("{\"type\":\"name\",\"target\":\"").append(escapeJson(target))
|
||||
.append("\",\"reason\":\"").append(escapeJson(reason))
|
||||
.append("\",\"source\":\"").append(escapeJson(source))
|
||||
.append("\",\"expire\":").append(expireDate).append("}");
|
||||
}
|
||||
|
||||
// IP封禁
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<org.bukkit.BanEntry<?>> ipEntries = (Set<org.bukkit.BanEntry<?>>)(Set<?>) ipBanList.getEntries();
|
||||
for (org.bukkit.BanEntry<?> entry : ipEntries) {
|
||||
if (!first) sb.append(",");
|
||||
first = false;
|
||||
String target = entry.getTarget();
|
||||
String reason = entry.getReason() != null ? entry.getReason() : "";
|
||||
String source = entry.getSource() != null ? entry.getSource() : "";
|
||||
long expireDate = entry.getExpiration() != null ? entry.getExpiration().getTime() : 0;
|
||||
sb.append("{\"type\":\"ip\",\"target\":\"").append(escapeJson(target))
|
||||
.append("\",\"reason\":\"").append(escapeJson(reason))
|
||||
.append("\",\"source\":\"").append(escapeJson(source))
|
||||
.append("\",\"expire\":").append(expireDate).append("}");
|
||||
}
|
||||
|
||||
sb.append("]");
|
||||
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put("action", "sync_bans");
|
||||
params.put("secret", secretKey);
|
||||
params.put("bans", sb.toString());
|
||||
String resp = httpGet("api/sync.php", params);
|
||||
plugin.getLogger().fine("[防护-sync] 封禁名单同步: " + resp);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[防护-sync] 封禁名单同步异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 从PHP拉取用户组配置到Java本地
|
||||
* PHP管理后台创建的用户组通过此方法同步到Java
|
||||
|
||||
Reference in New Issue
Block a user