feat: 领地过户功能 + 管理面板改主修复

1. 领地过户: 添加web_land_transfers冷却表,handleTransferLand/handleCancelTransfer/handleTransferStatus
2. 管理面板改主: 移除异步验证,直接写入web_admin_changes让Java端执行(管理员操作可信任)
3. admin.php: 过户按钮+adminTransferLand JS函数+confirm二次确认
4. 玩家名正则统一为3-16位
This commit is contained in:
ypsdf1
2026-06-29 11:48:51 +08:00
parent 3925dea586
commit 2237f6c855
2 changed files with 297 additions and 40 deletions
+35 -5
View File
@@ -2331,6 +2331,7 @@ async function loadLands(el) {
<td>${size} 格²</td>
<td>
<button onclick="changeLandOwner('${escAdmHtml(l.name)}','${escAdmHtml(l.owner)}')" style="padding:2px 8px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">改主</button>
<button onclick="adminTransferLand('${escAdmHtml(l.name)}','${escAdmHtml(l.owner)}')" style="padding:2px 8px;background:#ff9800;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">过户</button>
<button onclick="deleteLand('${escAdmHtml(l.name)}')" style="padding:2px 8px;background:#f44336;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">删除</button>
</td>
</tr>`;
@@ -2414,13 +2415,13 @@ async function saveLandConfig() {
}
async function changeLandOwner(name, currentOwner) {
const newOwner = await glassPrompt('领地 [' + name + '] 新所有者', currentOwner||'', '仅允许英文字母、数字和下划线,2-16位');
const newOwner = await glassPrompt('领地 [' + name + '] 新所有者', currentOwner||'', '仅允许英文字母、数字和下划线,3-16位');
if (newOwner === null || newOwner === false || newOwner === undefined) return;
const trimmed = newOwner.trim();
if (trimmed === '' || trimmed === currentOwner) return;
// 前端验证:Minecraft玩家名格式
if (!/^[a-zA-Z0-9_]{2,16}$/.test(trimmed)) {
glassAlert('玩家名格式无效:仅允许英文字母、数字和下划线,2-16位');
if (!/^[a-zA-Z0-9_]{3,16}$/.test(trimmed)) {
glassAlert('玩家名格式无效:仅允许英文字母、数字和下划线,3-16位');
return;
}
fetch('api/land_api.php?action=update_land_owner', {
@@ -2428,8 +2429,37 @@ async function changeLandOwner(name, currentOwner) {
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(trimmed) + '&secret=sdf1_web_comm_2026_ypshidifu'
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else glassAlert('失败: ' + (d.error||''));
if (d.success) {
glassAlert(d.message || '改主成功');
loadLands(document.getElementById('C'));
} else {
glassAlert('失败: ' + (d.error||''));
}
});
}
// ★ 管理面板过户(带冷却机制)
async function adminTransferLand(name, currentOwner) {
const newOwner = await glassPrompt('过户领地 [' + name + ']\n新所有者(过户后有1分钟冷却,期间可取消)', currentOwner||'', '仅允许英文字母、数字和下划线,3-16位');
if (newOwner === null || newOwner === false || newOwner === undefined) return;
const trimmed = newOwner.trim();
if (trimmed === '' || trimmed === currentOwner) return;
if (!/^[a-zA-Z0-9_]{3,16}$/.test(trimmed)) {
glassAlert('玩家名格式无效:仅允许英文字母、数字和下划线,3-16位');
return;
}
if (!await glassConfirm('确认将领地 [' + name + '] 过户给 ' + trimmed + ' ?\n\n过户后1分钟内可以取消')) return;
fetch('api/land_api.php?action=transfer_land', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'land=' + encodeURIComponent(name) + '&new_owner=' + encodeURIComponent(trimmed) + '&secret=sdf1_web_comm_2026_ypshidifu'
}).then(r => r.json()).then(d => {
if (d.success) {
glassAlert(d.message || '过户成功');
loadLands(document.getElementById('C'));
} else {
glassAlert('失败: ' + (d.error||''));
}
});
}
+262 -35
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'];
$playerActions = ['my_lands', 'land_detail', 'add_visitor', 'remove_visitor', 'list_visitors', 'land_shop', 'buy_permission', 'transfer_land', 'cancel_transfer', 'transfer_status'];
// ★ 玩家端领地字段更新(效果管理、开关等)
$playerFieldActions = ['update_land_field'];
// ★ 玩家端权限操作action
@@ -251,6 +251,29 @@ try {
handleLandShop($db, $_GET['name'] ?? '');
break;
// ===== 玩家端:过户领地 =====
case 'transfer_land':
if ($method !== 'POST') {
echo json_encode(['success' => false, 'error' => 'POST only']);
exit;
}
handleTransferLand($db, $playerName, $_POST);
break;
// ===== 玩家端:取消过户 =====
case 'cancel_transfer':
if ($method !== 'POST') {
echo json_encode(['success' => false, 'error' => 'POST only']);
exit;
}
handleCancelTransfer($db, $playerName, $_POST);
break;
// ===== 玩家端:查询过户状态 =====
case 'transfer_status':
handleTransferStatus($db, $playerName, $_GET['land'] ?? '');
break;
// ===== 玩家端:获取访客权限 =====
case 'get_visitor_perm':
handleGetVisitorPerm($db, $playerName, $_GET['land'] ?? '');
@@ -510,6 +533,18 @@ function initLandTables($db) {
created_at INTEGER DEFAULT 0,
synced INTEGER DEFAULT 0
)");
// ★ 领地过户冷却表
$db->exec("CREATE TABLE IF NOT EXISTS web_land_transfers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
land_name TEXT NOT NULL,
old_owner TEXT NOT NULL,
new_owner TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
completed_at INTEGER DEFAULT 0,
expires_at INTEGER DEFAULT 0
)");
}
function handleSyncLands($db, $post) {
@@ -742,7 +777,6 @@ function handleUpdateLandOwner($db, $post) {
return;
}
// ★ 验证新所有者
if (empty($owner)) {
echo json_encode(['success' => false, 'error' => '新所有者不能为空']);
return;
@@ -751,56 +785,49 @@ function handleUpdateLandOwner($db, $post) {
echo json_encode(['success' => false, 'error' => '玩家名格式不正确,仅支持英文字母、数字和下划线(3-16位)']);
return;
}
// ★ 校验玩家是否存在于login.db(异步验证:写入pending表,Java定时器拉取验证)
$ownerValid = validatePlayerViaJava($db, $owner, 'update_owner', ['land' => $name]);
if ($ownerValid === null) {
echo json_encode(['success' => true, 'pending' => true, 'message' => "玩家 {$owner} 的验证请求已提交,系统将在1-2分钟内自动完成验证"]);
return;
}
if (!$ownerValid) {
echo json_encode(['success' => false, 'error' => "玩家 {$owner} 尚未注册,请确认玩家名是否正确"]);
return;
}
// 获取旧所有者
$stmt = $db->prepare("SELECT owner FROM web_area_lands WHERE name = :name");
// ★ 管理面板改主:不走异步验证,直接写入变更队列让Java端执行
$stmt = $db->prepare("SELECT id, owner FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$oldOwner = $row ? ($row['owner'] ?? '') : '';
if (!$row) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
$landId = (int)$row['id'];
$oldOwner = $row['owner'] ?? '';
if ($oldOwner === $owner) {
echo json_encode(['success' => false, 'error' => '新旧所有者相同,无需更改']);
return;
}
// 更新所有者
// 更新PHP本地副本
$stmt2 = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
$stmt2->bindValue(':owner', $owner, SQLITE3_TEXT);
$stmt2->bindValue(':name', $name, SQLITE3_TEXT);
$stmt2->execute();
// 记录变更(用于同步到Java)
if ($oldOwner !== $owner) {
$landId = (int)($post['id'] ?? 0);
$stmt3 = $db->prepare("INSERT INTO web_land_owner_changes (land_id, land_name, old_owner, new_owner, created_at) VALUES (:lid, :name, :old, :new, :now)");
$stmt3->bindValue(':lid', $landId, SQLITE3_INTEGER);
$stmt3->bindValue(':name', $name, SQLITE3_TEXT);
$stmt3->bindValue(':old', $oldOwner, SQLITE3_TEXT);
$stmt3->bindValue(':new', $owner, SQLITE3_TEXT);
$stmt3->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt3->execute();
// 记录变更
$stmt3 = $db->prepare("INSERT INTO web_land_owner_changes (land_id, land_name, old_owner, new_owner, created_at) VALUES (:lid, :name, :old, :new, :now)");
$stmt3->bindValue(':lid', $landId, SQLITE3_INTEGER);
$stmt3->bindValue(':name', $name, SQLITE3_TEXT);
$stmt3->bindValue(':old', $oldOwner, SQLITE3_TEXT);
$stmt3->bindValue(':new', $owner, SQLITE3_TEXT);
$stmt3->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt3->execute();
// 同时添加到变更队列
$stmt4 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('owner_change', :id, :name, :data, :now)");
$stmt4->bindValue(':id', (int)($post['id'] ?? 0), SQLITE3_INTEGER);
$stmt4->bindValue(':name', $name, SQLITE3_TEXT);
$stmt4->bindValue(':data', json_encode(['old_owner' => $oldOwner, 'new_owner' => $owner]), SQLITE3_TEXT);
$stmt4->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt4->execute();
}
// ★ 写入web_admin_changes让Java端pollAdminChanges拉取并执行
$stmt4 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('owner_change', :id, :name, :data, :now)");
$stmt4->bindValue(':id', $landId, SQLITE3_INTEGER);
$stmt4->bindValue(':name', $name, SQLITE3_TEXT);
$stmt4->bindValue(':data', json_encode(['old_owner' => $oldOwner, 'new_owner' => $owner, 'source' => 'admin_panel']), SQLITE3_TEXT);
$stmt4->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt4->execute();
echo json_encode(['success' => true]);
debugLog("handleUpdateLandOwner: 管理面板改主 {$name}: {$oldOwner} → {$owner}");
echo json_encode(['success' => true, 'message' => "领地 [{$name}] 所有者已从 {$oldOwner} 变更为 {$owner}"]);
}
function handleDeleteShopItem($db, $post) {
@@ -1891,6 +1918,206 @@ function handleGetPlayerGroups($db, $player) {
echo json_encode(['success' => true, 'groups' => $groups]);
}
// ==================== 领地过户功能 ====================
/**
* 过户领地:领地主将自己的领地转让给其他玩家
* 冷却1分钟,期间如果领地主取消或重新设置权限则取消转让
*/
function handleTransferLand($db, $playerName, $post) {
$landName = $post['land'] ?? '';
$newOwner = $post['new_owner'] ?? '';
if (empty($landName) || empty($newOwner)) {
echo json_encode(['success' => false, 'error' => '参数不完整']);
return;
}
if (!preg_match('/^[a-zA-Z0-9_]{3,16}$/', $newOwner)) {
echo json_encode(['success' => false, 'error' => '玩家名格式不正确,仅支持英文字母、数字和下划线(3-16位)']);
return;
}
if (strtolower($newOwner) === strtolower($playerName)) {
echo json_encode(['success' => false, 'error' => '不能将领地转让给自己']);
return;
}
// 验证领地存在且操作者是领地主
$stmt = $db->prepare("SELECT id, owner FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $landName, SQLITE3_TEXT);
$rs = $stmt->execute();
$row = $rs->fetchArray(SQLITE3_ASSOC);
if (!$row) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
if (strtolower($row['owner'] ?? '') !== strtolower($playerName)) {
echo json_encode(['success' => false, 'error' => '只有领地主才能转让领地']);
return;
}
// 检查是否已有进行中的过户(冷却中)
$stmt2 = $db->prepare("SELECT id, status, expires_at FROM web_land_transfers WHERE land_name = :land AND status = 'pending' ORDER BY id DESC LIMIT 1");
$stmt2->bindValue(':land', $landName, SQLITE3_TEXT);
$rs2 = $stmt2->execute();
$pending = $rs2->fetchArray(SQLITE3_ASSOC);
if ($pending) {
if ($pending['expires_at'] > time()) {
$remain = $pending['expires_at'] - time();
echo json_encode(['success' => false, 'error' => "该领地正在过户冷却中,还需 {$remain} 秒"]);
return;
}
// 已过期,清理
$db->prepare("UPDATE web_land_transfers SET status = 'expired' WHERE id = :id")->bindValue(':id', (int)$pending['id'], SQLITE3_INTEGER);
$db->query("UPDATE web_land_transfers SET status = 'expired' WHERE id = " . (int)$pending['id']);
}
// 异步验证新所有者
$ownerValid = validatePlayerViaJava($db, $newOwner, 'transfer_land', ['land' => $landName, 'from' => $playerName]);
if ($ownerValid === null) {
echo json_encode(['success' => true, 'pending' => true, 'message' => "玩家 {$newOwner} 的验证请求已提交,系统将在1-2分钟内自动完成验证"]);
return;
}
if (!$ownerValid) {
echo json_encode(['success' => false, 'error' => "玩家 {$newOwner} 尚未注册,请确认玩家名是否正确"]);
return;
}
// 执行过户
$now = time();
$cooldown = 60; // 1分钟冷却
// 写入过户记录
$stmt3 = $db->prepare("INSERT INTO web_land_transfers (land_name, old_owner, new_owner, status, created_at, completed_at, expires_at) VALUES (:land, :old, :new, 'cooldown', :now, :now, :expires)");
$stmt3->bindValue(':land', $landName, SQLITE3_TEXT);
$stmt3->bindValue(':old', $playerName, SQLITE3_TEXT);
$stmt3->bindValue(':new', $newOwner, SQLITE3_TEXT);
$stmt3->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt3->bindValue(':expires', $now + $cooldown, SQLITE3_INTEGER);
$stmt3->execute();
// 更新领地所有者
$landId = (int)$row['id'];
$stmtUpd = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
$stmtUpd->bindValue(':owner', $newOwner, SQLITE3_TEXT);
$stmtUpd->bindValue(':name', $landName, SQLITE3_TEXT);
$stmtUpd->execute();
// 记录变更
$stmt4 = $db->prepare("INSERT INTO web_land_owner_changes (land_id, land_name, old_owner, new_owner, created_at) VALUES (:lid, :name, :old, :new, :now)");
$stmt4->bindValue(':lid', $landId, SQLITE3_INTEGER);
$stmt4->bindValue(':name', $landName, SQLITE3_TEXT);
$stmt4->bindValue(':old', $playerName, SQLITE3_TEXT);
$stmt4->bindValue(':new', $newOwner, SQLITE3_TEXT);
$stmt4->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt4->execute();
// 写入web_admin_changes让Java端执行
$stmt5 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('owner_change', :id, :name, :data, :now)");
$stmt5->bindValue(':id', $landId, SQLITE3_INTEGER);
$stmt5->bindValue(':name', $landName, SQLITE3_TEXT);
$stmt5->bindValue(':data', json_encode(['old_owner' => $playerName, 'new_owner' => $newOwner, 'source' => 'player_transfer', 'cooldown' => $cooldown]), SQLITE3_TEXT);
$stmt5->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt5->execute();
debugLog("handleTransferLand: {$playerName} 过户 {$landName} → {$newOwner}, 冷却{$cooldown}秒");
echo json_encode(['success' => true, 'message' => "领地 [{$landName}] 已转让给 {$newOwner},冷却 {$cooldown} 秒。冷却期间取消操作可撤回转让", 'cooldown' => $cooldown]);
}
/**
* 取消过户:冷却期间领地主可撤回转让
*/
function handleCancelTransfer($db, $playerName, $post) {
$landName = $post['land'] ?? '';
if (empty($landName)) {
echo json_encode(['success' => false, 'error' => '参数不完整']);
return;
}
// 查找进行中的过户
$stmt = $db->prepare("SELECT id, old_owner, new_owner, expires_at FROM web_land_transfers WHERE land_name = :land AND status = 'cooldown' ORDER BY id DESC LIMIT 1");
$stmt->bindValue(':land', $landName, SQLITE3_TEXT);
$rs = $stmt->execute();
$row = $rs->fetchArray(SQLITE3_ASSOC);
if (!$row) {
echo json_encode(['success' => false, 'error' => '没有进行中的过户']);
return;
}
if (time() > $row['expires_at']) {
echo json_encode(['success' => false, 'error' => '冷却期已过,无法取消']);
return;
}
if (strtolower($row['old_owner'] ?? '') !== strtolower($playerName)) {
echo json_encode(['success' => false, 'error' => '只有原领地主才能取消过户']);
return;
}
// 回退领地所有者
$oldOwner = $row['old_owner'];
$newOwner = $row['new_owner'];
$stmt2 = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
$stmt2->bindValue(':owner', $oldOwner, SQLITE3_TEXT);
$stmt2->bindValue(':name', $landName, SQLITE3_TEXT);
$stmt2->execute();
// 标记过户为已取消
$db->query("UPDATE web_land_transfers SET status = 'cancelled' WHERE id = " . (int)$row['id']);
// 记录回退变更
$stmt3 = $db->prepare("SELECT id FROM web_area_lands WHERE name = :name");
$stmt3->bindValue(':name', $landName, SQLITE3_TEXT);
$r3 = $stmt3->execute();
$r3Row = $r3->fetchArray(SQLITE3_ASSOC);
$landId = $r3Row ? (int)$r3Row['id'] : 0;
$stmt4 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at) VALUES ('owner_change', :id, :name, :data, :now)");
$stmt4->bindValue(':id', $landId, SQLITE3_INTEGER);
$stmt4->bindValue(':name', $landName, SQLITE3_TEXT);
$stmt4->bindValue(':data', json_encode(['old_owner' => $newOwner, 'new_owner' => $oldOwner, 'source' => 'transfer_cancelled']), SQLITE3_TEXT);
$stmt4->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt4->execute();
debugLog("handleCancelTransfer: {$playerName} 取消过户 {$landName}: {$newOwner} → {$oldOwner}");
echo json_encode(['success' => true, 'message' => "过户已取消,领地 [{$landName}] 已恢复为 {$oldOwner} 的所有"]);
}
/**
* 查询过户状态
*/
function handleTransferStatus($db, $playerName, $landName) {
if (empty($landName)) {
echo json_encode(['success' => false, 'error' => '缺少land参数']);
return;
}
$stmt = $db->prepare("SELECT * FROM web_land_transfers WHERE land_name = :land AND status IN ('pending', 'cooldown') ORDER BY id DESC LIMIT 1");
$stmt->bindValue(':land', $landName, SQLITE3_TEXT);
$rs = $stmt->execute();
$row = $rs->fetchArray(SQLITE3_ASSOC);
if (!$row) {
echo json_encode(['success' => true, 'transfer' => null]);
return;
}
$now = time();
$expires = (int)$row['expires_at'];
$remain = max(0, $expires - $now);
$status = $remain > 0 ? $row['status'] : 'expired';
echo json_encode(['success' => true, 'transfer' => [
'status' => $status,
'old_owner' => $row['old_owner'],
'new_owner' => $row['new_owner'],
'remaining' => $remain,
'expires_at' => $expires,
]]);
}
function validateSecret($secret) {
if (empty($secret)) return false;
$validSecrets = [