feat: 领地管理增强 - 毛玻璃改主弹窗 + pending队列 + 同步状态查询 + 玩家前端UI

This commit is contained in:
2026-07-01 01:20:15 +08:00
parent 6d68c4a05b
commit 1bc97f6980
4 changed files with 513 additions and 27 deletions
+58
View File
@@ -2681,6 +2681,32 @@ function showAddUserGroup() {
<input id="ugMaxLands" type="number" value="-1" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
<div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=跟随默认)</label>
<input id="ugHomeLimit" type="number" value="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
<div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">有效期(分钟, 0=永久)</label>
<input id="ugDuration" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
<div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">加入价格(债券, 0=免费)</label>
<input id="ugJoinPrice" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
<div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">续费价格(债券, 0=免费)</label>
<input id="ugRenewPrice" type="number" value="0" min="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
</div>
<div style="margin-bottom:10px">
<label style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);cursor:pointer">
<input type="checkbox" id="ugAutoRenew" style="accent-color:#4caf50;width:14px;height:14px">
启用自动续费(到期时从债券余额扣除续费价格自动延长)
</label>
</div>
<button onclick="doAddUserGroup()" style="width:100%;padding:8px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer">创建用户组</button>
</div>`;
showModal('新建用户组', '', null, html);
@@ -2693,6 +2719,11 @@ async function doAddUserGroup() {
const priority = document.getElementById('ugPriority')?.value || '0';
const price = document.getElementById('ugPrice')?.value || '-1';
const maxLands = document.getElementById('ugMaxLands')?.value || '-1';
const homeLimit = document.getElementById('ugHomeLimit')?.value || '0';
const joinPrice = document.getElementById('ugJoinPrice')?.value || '0';
const renewPrice = document.getElementById('ugRenewPrice')?.value || '0';
const duration = document.getElementById('ugDuration')?.value || '0';
const autoRenew = document.getElementById('ugAutoRenew')?.checked ? '1' : '0';
if (!name || !/^[a-zA-Z0-9_]{2,20}$/.test(name)) {
glassAlert('组ID仅允许英文字母、数字和下划线,2-20位');
@@ -2703,6 +2734,8 @@ async function doAddUserGroup() {
const res = await apiCall('update_user_group', {
name, display_name: displayName, display_color: color,
priority, land_price_per_sqm: price, max_lands: maxLands,
home_limit: homeLimit, join_price: joinPrice, renew_price: renewPrice,
duration_minutes: duration, auto_renew: autoRenew,
default_perms: '{}'
}, 'POST');
if (res.success) {
@@ -2746,6 +2779,24 @@ async function showEditUserGroup(groupName) {
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">最大领地数</label>
<input id="eugMaxLands" type="number" value="${g.max_lands}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
</div>`;
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=默认)</label>
<input id="eugHomeLimit" type="number" value="${g.home_limit || 0}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">有效期(分钟, 0=永久)</label>
<input id="eugDuration" type="number" value="${g.duration_minutes || 0}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
</div>`;
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">加入价格(债券)</label>
<input id="eugJoinPrice" type="number" value="${g.join_price || 0}" min="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">续费价格(债券)</label>
<input id="eugRenewPrice" type="number" value="${g.renew_price || 0}" min="0" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
</div>`;
html += `<div style="margin-bottom:12px">
<label style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);cursor:pointer">
<input type="checkbox" id="eugAutoRenew" ${g.auto_renew == 1 ? 'checked' : ''} style="accent-color:#4caf50;width:14px;height:14px">
启用自动续费
</label>
</div>`;
// 默认权限列表
html += `<div style="margin-bottom:12px"><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:6px">默认权限</label>`;
html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:4px">';
@@ -2772,6 +2823,11 @@ async function doEditUserGroup(groupName) {
const priority = document.getElementById('eugPriority')?.value || '0';
const price = document.getElementById('eugPrice')?.value || '-1';
const maxLands = document.getElementById('eugMaxLands')?.value || '-1';
const homeLimit = document.getElementById('eugHomeLimit')?.value || '0';
const duration = document.getElementById('eugDuration')?.value || '0';
const joinPrice = document.getElementById('eugJoinPrice')?.value || '0';
const renewPrice = document.getElementById('eugRenewPrice')?.value || '0';
const autoRenew = document.getElementById('eugAutoRenew')?.checked ? '1' : '0';
// 收集权限
const perms = {};
@@ -2783,6 +2839,8 @@ async function doEditUserGroup(groupName) {
const res = await apiCall('update_user_group', {
name: groupName, display_name: displayName, display_color: color,
priority, land_price_per_sqm: price, max_lands: maxLands,
home_limit: homeLimit, duration_minutes: duration,
join_price: joinPrice, renew_price: renewPrice, auto_renew: autoRenew,
default_perms: JSON.stringify(perms)
}, 'POST');
if (res.success) {
+240 -4
View File
@@ -94,7 +94,7 @@ try {
// 管理面板action:支持admin_token或secret
$adminActions = ['list_lands', 'list_shop', 'get_config', 'update_config', 'delete_land', 'update_land_owner', 'delete_shop_item', 'list_user_groups', 'get_user_group', 'update_user_group', 'delete_user_group', 'list_group_members', 'add_group_member', 'remove_group_member', 'get_player_groups'];
// 玩家端action:需要token
$playerActions = ['my_lands', 'land_detail', 'add_visitor', 'remove_visitor', 'list_visitors', 'land_shop', 'buy_permission', 'transfer_land', 'cancel_transfer', 'transfer_status'];
$playerActions = ['my_lands', 'land_detail', 'add_visitor', 'remove_visitor', 'list_visitors', 'land_shop', 'buy_permission', 'transfer_land', 'cancel_transfer', 'transfer_status', 'renew_group', 'list_available_groups', 'buy_group'];
// ★ 玩家端领地字段更新(效果管理、开关等)
$playerFieldActions = ['update_land_field'];
// ★ 玩家端权限操作action
@@ -276,6 +276,21 @@ try {
handleTransferStatus($db, $playerName, $_GET['land'] ?? '');
break;
// ===== 玩家端:查看可购买的用户组 =====
case 'list_available_groups':
handleListAvailableGroups($db);
break;
// ===== 玩家端:付费加入用户组 =====
case 'buy_group':
handleBuyGroup($db, $playerName, $_POST + $_GET);
break;
// ===== 玩家端:续费用户组 =====
case 'renew_group':
handleRenewGroup($db, $playerName, $_POST + $_GET);
break;
// ===== 玩家端:获取访客权限 =====
case 'get_visitor_perm':
handleGetVisitorPerm($db, $playerName, $_GET['land'] ?? '');
@@ -1755,9 +1770,33 @@ function handleListUserGroups($db) {
priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1,
home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
default_perms TEXT DEFAULT '{}',
synced_at INTEGER DEFAULT 0
)");
// 迁移:添加新列
$columns = [];
$rs = $db->query("PRAGMA table_info(web_user_groups)");
while ($row = $rs->fetchArray(SQLITE3_ASSOC)) {
$columns[] = $row['name'];
}
$migrations = [
'home_limit' => 'ALTER TABLE web_user_groups ADD COLUMN home_limit INTEGER DEFAULT 0',
'join_price' => 'ALTER TABLE web_user_groups ADD COLUMN join_price INTEGER DEFAULT 0',
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
];
foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) {
try { $db->exec($sql); } catch (\Throwable $e) { /* 已存在 */ }
}
}
$rs = $db->query("SELECT * FROM web_user_groups ORDER BY priority DESC");
$groups = [];
while ($row = $rs->fetchArray(SQLITE3_ASSOC)) {
@@ -1788,6 +1827,11 @@ function handleUpdateUserGroup($db, $data) {
priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1,
home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
default_perms TEXT DEFAULT '{}',
synced_at INTEGER DEFAULT 0
)");
@@ -1798,13 +1842,18 @@ function handleUpdateUserGroup($db, $data) {
$priority = (int)($data['priority'] ?? 0);
$pricePerSqm = (int)($data['land_price_per_sqm'] ?? -1);
$maxLands = (int)($data['max_lands'] ?? -1);
$homeLimit = (int)($data['home_limit'] ?? 0);
$joinPrice = (int)($data['join_price'] ?? 0);
$renewPrice = (int)($data['renew_price'] ?? 0);
$durationMinutes = (int)($data['duration_minutes'] ?? 0);
$autoRenew = (int)($data['auto_renew'] ?? 0);
$defaultPerms = $data['default_perms'] ?? '{}';
$stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups
(group_name, display_name, display_color, display_emoji, priority,
land_price_per_sqm, max_lands, default_perms, synced_at)
land_price_per_sqm, max_lands, home_limit, join_price, auto_renew, renew_price, duration_minutes, default_perms, synced_at)
VALUES (:name, :display, :color, :emoji, :priority,
:price, :maxlands, :perms, :synced)");
:price, :maxlands, :homeLimit, :joinPrice, :autoRenew, :renewPrice, :duration, :perms, :synced)");
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->bindValue(':display', $displayName, SQLITE3_TEXT);
$stmt->bindValue(':color', $displayColor, SQLITE3_TEXT);
@@ -1812,6 +1861,11 @@ function handleUpdateUserGroup($db, $data) {
$stmt->bindValue(':priority', $priority, SQLITE3_INTEGER);
$stmt->bindValue(':price', $pricePerSqm, SQLITE3_INTEGER);
$stmt->bindValue(':maxlands', $maxLands, SQLITE3_INTEGER);
$stmt->bindValue(':homeLimit', $homeLimit, SQLITE3_INTEGER);
$stmt->bindValue(':joinPrice', $joinPrice, SQLITE3_INTEGER);
$stmt->bindValue(':autoRenew', $autoRenew, SQLITE3_INTEGER);
$stmt->bindValue(':renewPrice', $renewPrice, SQLITE3_INTEGER);
$stmt->bindValue(':duration', $durationMinutes, SQLITE3_INTEGER);
$stmt->bindValue(':perms', $defaultPerms, SQLITE3_TEXT);
$stmt->bindValue(':synced', time(), SQLITE3_INTEGER);
$stmt->execute();
@@ -2004,7 +2058,8 @@ function handleGetPlayerGroups($db, $player) {
expiry_time INTEGER DEFAULT 0,
PRIMARY KEY(player_name, group_name)
)");
$stmt = $db->prepare("SELECT m.*, g.display_name, g.display_color, g.priority
$stmt = $db->prepare("SELECT m.*, g.display_name, g.display_color, g.priority,
g.join_price, g.renew_price, g.auto_renew, g.duration_minutes, g.home_limit
FROM web_user_group_members m
LEFT JOIN web_user_groups g ON m.group_name = g.group_name
WHERE m.player_name = :player");
@@ -2017,6 +2072,187 @@ function handleGetPlayerGroups($db, $player) {
echo json_encode(['success' => true, 'groups' => $groups]);
}
/**
* 玩家端:查看可购买的用户组
*/
function handleListAvailableGroups($db) {
$db->exec("CREATE TABLE IF NOT EXISTS web_user_groups (
group_name TEXT PRIMARY KEY,
display_name TEXT DEFAULT '',
display_color TEXT DEFAULT '§f',
display_emoji TEXT DEFAULT '',
priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1,
home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
default_perms TEXT DEFAULT '{}',
synced_at INTEGER DEFAULT 0
)");
// 返回开放付费加入的用户组(join_price > 0)
$rs = $db->query("SELECT group_name, display_name, display_color, display_emoji,
join_price, renew_price, duration_minutes
FROM web_user_groups WHERE join_price > 0 ORDER BY priority DESC");
$groups = [];
while ($row = $rs->fetchArray(SQLITE3_ASSOC)) {
$groups[] = $row;
}
echo json_encode(['success' => true, 'groups' => $groups], JSON_UNESCAPED_UNICODE);
}
/**
* 玩家端:付费加入用户组(写入web_admin_changes,Java轮询拉取执行)
*/
function handleBuyGroup($db, $player, $data) {
$group = $data['group'] ?? $data['group_name'] ?? '';
if (empty($player) || empty($group)) {
echo json_encode(['success' => false, 'error' => '缺少玩家名或用户组名']);
return;
}
// 检查用户组是否存在且开放付费加入
$stmt = $db->prepare("SELECT * FROM web_user_groups WHERE group_name = :group");
$stmt->bindValue(':group', $group, SQLITE3_TEXT);
$rs = $stmt->execute();
$cfg = $rs->fetchArray(SQLITE3_ASSOC);
if (!$cfg) {
echo json_encode(['success' => false, 'error' => '用户组不存在: ' . $group]);
return;
}
if ((int)$cfg['join_price'] <= 0) {
echo json_encode(['success' => false, 'error' => '该用户组不开放付费加入']);
return;
}
if ((int)$cfg['duration_minutes'] <= 0) {
echo json_encode(['success' => false, 'error' => '该用户组配置异常(有效时长未设置)']);
return;
}
// 检查该玩家是否已在组内
$db->exec("CREATE TABLE IF NOT EXISTS web_user_group_members (
player_name TEXT NOT NULL,
group_name TEXT NOT NULL,
added_by TEXT DEFAULT 'system',
added_time INTEGER DEFAULT 0,
expiry_time INTEGER DEFAULT 0,
PRIMARY KEY(player_name, group_name)
)");
$stmt2 = $db->prepare("SELECT * FROM web_user_group_members WHERE player_name = :player AND group_name = :group");
$stmt2->bindValue(':player', $player, SQLITE3_TEXT);
$stmt2->bindValue(':group', $group, SQLITE3_TEXT);
$rs2 = $stmt2->execute();
$member = $rs2->fetchArray(SQLITE3_ASSOC);
if ($member) {
echo json_encode(['success' => false, 'error' => '你已在该用户组中']);
return;
}
// 写入 web_admin_changes 等待Java拉取执行
try {
$db->exec("CREATE TABLE IF NOT EXISTS web_admin_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
change_type TEXT NOT NULL,
target_id TEXT DEFAULT '',
target_name TEXT DEFAULT '',
change_data TEXT DEFAULT '{}',
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
processed_at INTEGER DEFAULT 0
)");
$now = time();
$stmt3 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('group_buy', :id, :name, :data, :time)");
$stmt3->bindValue(':id', $group, SQLITE3_TEXT);
$stmt3->bindValue(':name', $player, SQLITE3_TEXT);
$stmt3->bindValue(':data', json_encode([
'action' => 'buy',
'group_name' => $group,
'player' => $player,
'join_price' => (int)$cfg['join_price'],
'duration_minutes' => (int)$cfg['duration_minutes']
], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt3->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt3->execute();
echo json_encode(['success' => true, 'message' => '购买请求已提交,请在游戏中确认扣费']);
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => '购买请求写入失败: ' . $e->getMessage()]);
}
}
/**
* 玩家端:续费用户组(写入web_admin_changes,Java轮询拉取执行)
*/
function handleRenewGroup($db, $player, $data) {
$group = $data['group'] ?? $data['group_name'] ?? '';
if (empty($player) || empty($group)) {
echo json_encode(['success' => false, 'error' => '缺少玩家名或用户组名']);
return;
}
// 检查该玩家是否在该组内
$db->exec("CREATE TABLE IF NOT EXISTS web_user_group_members (
player_name TEXT NOT NULL,
group_name TEXT NOT NULL,
added_by TEXT DEFAULT 'system',
added_time INTEGER DEFAULT 0,
expiry_time INTEGER DEFAULT 0,
PRIMARY KEY(player_name, group_name)
)");
$stmt = $db->prepare("SELECT * FROM web_user_group_members WHERE player_name = :player AND group_name = :group");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':group', $group, SQLITE3_TEXT);
$rs = $stmt->execute();
$member = $rs->fetchArray(SQLITE3_ASSOC);
if (!$member) {
echo json_encode(['success' => false, 'error' => '你不在该用户组中']);
return;
}
// 获取用户组配置
$stmt2 = $db->prepare("SELECT * FROM web_user_groups WHERE group_name = :group");
$stmt2->bindValue(':group', $group, SQLITE3_TEXT);
$rs2 = $stmt2->execute();
$cfg = $rs2->fetchArray(SQLITE3_ASSOC);
if (!$cfg || (int)$cfg['renew_price'] <= 0) {
echo json_encode(['success' => false, 'error' => '该用户组不支持续费']);
return;
}
// 写入 web_admin_changes 等待Java拉取执行
try {
$db->exec("CREATE TABLE IF NOT EXISTS web_admin_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
change_type TEXT NOT NULL,
target_id TEXT DEFAULT '',
target_name TEXT DEFAULT '',
change_data TEXT DEFAULT '{}',
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
processed_at INTEGER DEFAULT 0
)");
$now = time();
$stmt3 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('group_renew', :id, :name, :data, :time)");
$stmt3->bindValue(':id', $group, SQLITE3_TEXT);
$stmt3->bindValue(':name', $player, SQLITE3_TEXT);
$stmt3->bindValue(':data', json_encode([
'action' => 'renew',
'group_name' => $group,
'player' => $player,
'renew_price' => (int)$cfg['renew_price'],
'duration_minutes' => (int)$cfg['duration_minutes']
], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt3->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt3->execute();
echo json_encode(['success' => true, 'message' => '续费请求已提交,请在游戏中确认扣费']);
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => '续费请求写入失败: ' . $e->getMessage()]);
}
}
// ==================== 领地过户功能 ====================
/**
+41 -23
View File
@@ -146,7 +146,8 @@ function checkPlayerRegistered() {
}
try {
$db = getDB();
$stmt = $db->prepare("SELECT player_name, password_hash, salt, email, register_time, last_login_time, points, gift_stage, total_online_time, ip_address FROM users WHERE LOWER(player_name)=LOWER(:name) LIMIT 1");
// ★ 只查players_name,因为users表可能只有这两个字段
$stmt = $db->prepare("SELECT player_name, register_time, last_login_time, email, points, gift_stage, total_online_time FROM users WHERE LOWER(player_name)=LOWER(:name) LIMIT 1");
$stmt->bindValue(':name', mb_strtolower($player, 'UTF-8'), SQLITE3_TEXT);
$res = $stmt->execute();
$row = $res->fetchArray(SQLITE3_ASSOC);
@@ -157,15 +158,15 @@ function checkPlayerRegistered() {
$userInfo = [
'registered' => true,
'player_name' => $row['player_name'],
'password_hash' => $row['password_hash'] ?? '',
'salt' => $row['salt'] ?? '',
'password_hash' => '', // users表无密码字段,留空
'salt' => '',
'email' => $row['email'] ?? null,
'register_time' => $row['register_time'] ?? 0,
'last_login_time' => $row['last_login_time'] ?? 0,
'points' => $row['points'] ?? 0,
'gift_stage' => $row['gift_stage'] ?? 0,
'total_online_time' => $row['total_online_time'] ?? 0,
'ip_address' => $row['ip_address'] ?? null,
'ip_address' => null,
];
success($userInfo);
} catch (\Throwable $e) {
@@ -775,16 +776,42 @@ function syncUserGroups() {
priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1,
home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
default_perms TEXT DEFAULT '{}',
synced_at INTEGER DEFAULT 0
)");
// 迁移:添加新列(如果缺失)
$columns = [];
$rs = $db->query("PRAGMA table_info(web_user_groups)");
while ($row = $rs->fetchArray(SQLITE3_ASSOC)) {
$columns[] = $row['name'];
}
$migrations = [
'home_limit' => 'ALTER TABLE web_user_groups ADD COLUMN home_limit INTEGER DEFAULT 0',
'join_price' => 'ALTER TABLE web_user_groups ADD COLUMN join_price INTEGER DEFAULT 0',
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
];
foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) {
try { $db->exec($sql); } catch (\Throwable $e) { /* 已存在 */ }
}
}
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups
(group_name, display_name, display_color, display_emoji, priority,
land_price_per_sqm, max_lands, default_perms, synced_at)
land_price_per_sqm, max_lands, home_limit, join_price, auto_renew,
renew_price, duration_minutes, default_perms, synced_at)
VALUES (:name, :display, :color, :emoji, :priority,
:price, :maxlands, :perms, :synced)");
:price, :maxlands, :homelimit, :joinprice, :autorenew,
:renewprice, :duration, :perms, :synced)");
$count = 0;
foreach ($groups as $g) {
$stmt->bindValue(':name', $g['group_name'] ?? '', SQLITE3_TEXT);
@@ -794,6 +821,11 @@ function syncUserGroups() {
$stmt->bindValue(':priority', (int)($g['priority'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':price', (int)($g['land_price_per_sqm'] ?? -1), SQLITE3_INTEGER);
$stmt->bindValue(':maxlands', (int)($g['max_lands'] ?? -1), SQLITE3_INTEGER);
$stmt->bindValue(':homelimit', (int)($g['home_limit'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':joinprice', (int)($g['join_price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':autorenew', (int)($g['auto_renew'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':renewprice', (int)($g['renew_price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':duration', (int)($g['duration_minutes'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':perms', $g['default_perms'] ?? '{}', SQLITE3_TEXT);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
@@ -995,24 +1027,10 @@ function syncTransactions() {
// ===== 插件推送注册数据 =====
function syncLogin() {
// ★ 支持两种认证:token 或 SECRET_KEY(防止token注册超时导致同步失败)
$token = getParam('token');
// ★ 纯secret认证(不依赖token,避免token过期/已使用导致同步失败)
$secret = getParam('secret');
if ($token) {
$tokenInfo = validateToken($token);
if (!$tokenInfo || ($tokenInfo['purpose'] !== 'admin' && $tokenInfo['purpose'] !== 'all' && $tokenInfo['purpose'] !== 'sync')) {
// token无效时,检查是否携带了secret
if ($secret && $secret === SECRET_KEY) {
// secret有效,继续执行
} else {
error('同步需要管理权限token或SECRET_KEY');
}
}
} elseif ($secret) {
if ($secret !== SECRET_KEY) error('密钥验证失败', 403);
} else {
error('同步需要token或SECRET_KEY');
if (!$secret || $secret !== SECRET_KEY) {
error('同步需要SECRET_KEY验证', 403);
}
$users = getParam('users');
+174
View File
@@ -273,6 +273,7 @@ if ($currentVersion !== $BUILD_VERSION) {
<div class="sidebar-item" data-page="lands" onclick="switchPage('lands')">🏡 领地管理</div>
<div class="sidebar-item" data-page="cdk" onclick="switchPage('cdk')">🎁 CDK兑换</div>
<div class="sidebar-item" data-page="balance" onclick="switchPage('balance')">💰 余额查询</div>
<div class="sidebar-item" data-page="groups" onclick="switchPage('groups')">👥 用户组</div>
<div class="sidebar-item" data-page="ticket" onclick="switchPage('ticket')">📋 工单系统</div>
</div>
@@ -1038,6 +1039,7 @@ if ($currentVersion !== $BUILD_VERSION) {
else if (page === 'lands') renderLands(c);
else if (page === 'cdk') renderCDK(c);
else if (page === 'balance') renderBalance(c);
else if (page === 'groups') renderGroups(c);
else if (page === 'account') renderAccount(c);
else if (page === 'ticket') renderTicket(c);
}
@@ -1402,6 +1404,178 @@ if ($currentVersion !== $BUILD_VERSION) {
}
}
// ===== 用户组 =====
function renderGroups(el) {
if (!currentPlayer || (!AUTHENTICATED && NEED_PASSWORD)) {
el.innerHTML = '<div class="card"><h2>👥 用户组</h2><div style="text-align:center;padding:40px 20px"><div style="font-size:48px;margin-bottom:16px">🔒</div><p style="color:var(--dim);font-size:14px;margin-bottom:20px">请先登录游戏</p></div></div>';
return;
}
el.innerHTML = `
<div class="card">
<h2>👥 用户组</h2>
<div id="myGroupsResult" style="margin-top:12px"><div style="color:var(--dim);font-size:13px">加载中...</div></div>
</div>
<div class="card">
<h2>🛒 可购买的用户组</h2>
<div id="availableGroupsResult" style="margin-top:12px"><div style="color:var(--dim);font-size:13px">加载中...</div></div>
</div>`;
loadMyGroups();
loadAvailableGroups();
}
async function loadMyGroups() {
const div = document.getElementById('myGroupsResult');
try {
const res = await api('land_api.php', {action: 'get_player_groups', player: currentPlayer, token: TOKEN});
if (!res.success) { div.innerHTML = `<div style="color:var(--red);font-size:13px">${res.error || '加载失败'}</div>`; return; }
const groups = res.groups || [];
if (groups.length === 0) {
div.innerHTML = '<div style="color:var(--dim);font-size:13px;padding:12px 0">你还没有加入任何用户组</div>';
return;
}
let html = '';
groups.forEach(g => {
const now = Math.floor(Date.now() / 1000);
const expiry = parseInt(g.expiry_time) || 0;
let statusHtml = '';
let colorStyle = g.display_color ? g.display_color.replace(/§/g, '') : '';
// 将MC颜色代码转为CSS
const mcColors = {'0':'#000','1':'#00a','2':'#0a0','3':'#0aa','4':'#a00','5':'#a0a','6':'#fa0','7':'#aaa','8':'#555','9':'#55f','a':'#5f5','b':'#5ff','c':'#f55','d':'#f5f','e':'#ff5','f':'#fff'};
const mcBold = {'l':'font-weight:bold;'};
let cssColor = '';
let cssBold = '';
for (let i = 0; i < colorStyle.length; i++) {
if (mcColors[colorStyle[i]]) cssColor = mcColors[colorStyle[i]];
if (colorStyle[i] === 'l') cssBold = 'font-weight:bold;';
}
const displayName = g.display_name || g.group_name;
const displayNameHtml = `<span style="color:${cssColor};${cssBold}">${displayName}</span>`;
// 格式化有效期
const durMin = parseInt(g.duration_minutes) || 0;
let durStr = '';
if (durMin > 0) {
durStr = durMin >= 1440 ? '有效期:' + Math.floor(durMin/1440) + '天' : '有效期:' + Math.floor(durMin/60) + '小时' + (durMin%60 ? ' '+(durMin%60)+'分钟' : '');
} else {
durStr = '永久有效';
}
const homeLimit = g.home_limit != null ? g.home_limit : '默认';
const landPrice = parseInt(g.land_price_per_sqm) !== -1 ? g.land_price_per_sqm : -1;
if (expiry > 0) {
const left = expiry - now;
if (left <= 0) {
statusHtml = '<span style="color:var(--red);font-size:12px">已过期</span>';
} else {
const days = Math.floor(left / 86400);
const hours = Math.floor((left % 86400) / 3600);
const mins = Math.floor((left % 3600) / 60);
let timeStr = '';
if (days > 0) timeStr += days + '天';
if (hours > 0) timeStr += hours + '小时';
timeStr += mins + '分钟';
statusHtml = `<span style="color:var(--green);font-size:12px">剩余 ${timeStr}</span>`;
if (g.auto_renew == 1) statusHtml += ' <span style="color:var(--purple);font-size:11px">[自动续费]</span>';
}
} else {
statusHtml = '<span style="color:var(--dim);font-size:12px">永久</span>';
}
let renewBtn = '';
if (expiry > 0 && expiry - now < 86400 && parseInt(g.renew_price) > 0) {
renewBtn = ` <button class="btn btn-blue" style="font-size:11px;padding:3px 8px;margin-left:8px" onclick="doRenewGroup('${g.group_name}')">续费 ${g.renew_price}张</button>`;
}
html += `<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);gap:8px;flex-wrap:wrap">
<div>
${displayNameHtml}
<div style="font-size:11px;color:var(--dim);margin-top:4px">
${durStr} · Home:${homeLimit} · 领地${landPrice === -1 ? '默认' : '('+landPrice+'张/格'+String.fromCharCode(178)+')'} · 加入${parseInt(g.join_price)>0?(g.join_price+'张'):((g.auto_renew==1?'可续费':'免费'))}
</div>
</div>
<div style="display:flex;align-items:center;gap:8px">${statusHtml}${renewBtn}</div>
</div>`;
});
div.innerHTML = html;
} catch(e) {
div.innerHTML = `<div style="color:var(--red);font-size:13px">加载失败: ${e.message}</div>`;
}
}
async function loadAvailableGroups() {
const div = document.getElementById('availableGroupsResult');
window._availGroupsMap = window._availGroupsMap || {};
try {
const res = await api('land_api.php', {action: 'list_available_groups', token: TOKEN});
if (!res.success) { div.innerHTML = `<div style="color:var(--red);font-size:13px">${res.error || '加载失败'}</div>`; return; }
const groups = res.groups || [];
// Cache for buy dialog display
groups.forEach(g => { window._availGroupsMap[g.group_name] = g; });
if (groups.length === 0) {
div.innerHTML = '<div style="color:var(--dim);font-size:13px;padding:12px 0">暂无可购买的用户组</div>';
return;
}
let html = '<div class="grid">';
groups.forEach(g => {
const mcColors = {'0':'#000','1':'#00a','2':'#0a0','3':'#0aa','4':'#a00','5':'#a0a','6':'#fa0','7':'#aaa','8':'#555','9':'#55f','a':'#5f5','b':'#5ff','c':'#f55','d':'#f5f','e':'#ff5','f':'#fff'};
const colorCode = (g.display_color || '§f').replace(/§/g, '');
let cssColor = '#fff';
for (let i = 0; i < colorCode.length; i++) { if (mcColors[colorCode[i]]) cssColor = mcColors[colorCode[i]]; }
const dur = parseInt(g.duration_minutes) || 0;
let durStr = dur >= 1440 ? Math.floor(dur/1440) + '天' : dur >= 60 ? Math.floor(dur/60) + '小时' : dur + '分钟';
html += `<div class="item-card" style="border-color:var(--border)">
<div style="display:flex;justify-content:space-between;align-items:start">
<div class="name" style="color:${cssColor}">${g.display_emoji || ''} ${g.display_name || g.group_name}</div>
<div class="price" style="color:var(--green);font-weight:600">${g.join_price}张</div>
</div>
<div style="color:var(--dim);font-size:12px;margin-top:6px">有效期: ${durStr} · Home:${g.home_limit || 0} · 续费:${g.renew_price}张</div>
<div style="margin-top:10px"><button class="btn btn-blue" style="width:100%" onclick="doBuyGroup('${g.group_name}')">购买加入</button></div>
</div>`;
});
html += '</div>';
div.innerHTML = html;
} catch(e) {
div.innerHTML = `<div style="color:var(--red);font-size:13px">加载失败: ${e.message}</div>`;
}
}
async function doBuyGroup(groupName) {
const g = window._availGroupsMap?.[groupName];
const price = g ? g.join_price + ' 张债券' : '(价格未知)';
const dur = g ? (g.duration_minutes >= 1440 ? Math.floor(g.duration_minutes/1440) + '天' : Math.floor(g.duration_minutes/60) + '小时') : '未知';
if (!confirm(`确认付费加入用户组「${g ? (g.display_name || groupName) : groupName}」?\n价格: ${price}\n有效期: ${dur}\n将从债券余额扣费。`)) return;
try {
const res = await api('land_api.php', {action: 'buy_group', group: groupName, player: currentPlayer, token: TOKEN});
if (res.success) {
alert(res.message || '加入成功');
loadMyGroups();
loadAvailableGroups();
} else {
alert(res.error || '加入失败');
}
} catch(e) {
alert('请求失败: ' + e.message);
}
}
async function doRenewGroup(groupName) {
const g = window._availGroupsMap?.[groupName];
const price = g ? (parseInt(g.renew_price) > 0 ? g.renew_price + ' 张债券' : '免费') : '未知';
const dur = g && g.duration_minutes ? (g.duration_minutes >= 1440 ? Math.floor(g.duration_minutes/1440) + '天' : Math.floor(g.duration_minutes/60) + '小时') : '由用户组配置决定';
if (!confirm(`确认续费用户组「${g ? (g.display_name || groupName) : groupName}」?\n续费价格: ${price}\n延长有效期: ${dur}\n将从债券余额扣费。`)) return;
try {
const res = await api('land_api.php', {action: 'renew_group', group: groupName, player: currentPlayer, token: TOKEN});
if (res.success) {
alert(res.message || '续费请求已提交');
loadMyGroups();
} else {
alert(res.error || '续费失败');
}
} catch(e) {
alert('请求失败: ' + e.message);
}
}
// ===== 账号 =====
function renderAccount(el) {
if (IS_PREVIEW || (!AUTHENTICATED && NEED_PASSWORD)) {