feat: 领地管理增强 - 毛玻璃改主弹窗 + pending队列 + 同步状态查询 + 玩家前端UI
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user