fix: PHP数据库锁修复 + 用户组添加/移除错误处理

1. initTables()统一创建所有核心表(web_user_groups, web_user_group_members等)
2. handleAddGroupMember改用playerExists()本地验证替代异步validatePlayerViaJava()
3. handleRemoveGroupMember增加错误处理和affected行数检查
4. 移除validation_callback/visitor中的CREATE TABLE IF NOT EXISTS
This commit is contained in:
ypsdf1
2026-07-03 13:41:22 +08:00
parent 4927aaed01
commit 591d71ede0
3 changed files with 255 additions and 75 deletions
+3 -3
View File
@@ -2450,10 +2450,10 @@ async function adminTransferLand(name, currentOwner) {
const d = await res.json();
if (d.success && d.pending) {
glassAlert(d.message || '已提交改主请求,等待Java端验证');
// 启动轮询,每5秒查询状态,最多等待1.05分钟
// 启动轮询,每3秒查询状态,最多等待15秒
let elapsed = 0;
const maxWait = 65000; // 1.05分钟 = 65秒
const interval = 5000; // 5秒轮询一次
const maxWait = 15000; // 15秒
const interval = 3000; // 3秒轮询一次
const poll = async () => {
try {
const pollRes = await fetch('api/land_api.php?action=get_owner_change_status&name=' + encodeURIComponent(name) + '&secret=sdf1_web_comm_2026_ypshidifu');
+115 -51
View File
@@ -948,7 +948,7 @@ function handleGetOwnerChangeStatus($db, $get) {
if ($rowTime && $status === 'pending') {
$createdAt = (int)($rowTime['created_at'] ?? 0);
$now = time();
if ($now - $createdAt > 65) {
if ($now - $createdAt > 15) {
// 超时,自动回滚
$stmtRollback = $db->prepare("UPDATE web_admin_changes SET status = 'failed', acknowledged = 1, acked_at = :now WHERE change_type = 'owner_change' AND target_name = :name AND status = 'pending'");
$stmtRollback->bindValue(':now', $now, SQLITE3_INTEGER);
@@ -1226,20 +1226,93 @@ function handleValidationCallback($db, $data) {
return;
}
// ★ 先读取完整记录(含request_type和request_data),再更新状态
$stmt0 = $db->prepare("SELECT player_name, request_type, request_data FROM pending_player_validations WHERE id = :id");
$stmt0->bindValue(':id', $id, SQLITE3_INTEGER);
$rs0 = $stmt0->execute();
$row = $rs0->fetchArray(SQLITE3_ASSOC);
$player = $row ? $row['player_name'] : '?';
$reqType = $row ? $row['request_type'] : '';
$reqData = $row ? json_decode($row['request_data'] ?? '{}', true) : [];
$stmt = $db->prepare("UPDATE pending_player_validations SET status = :status, validated_at = :now WHERE id = :id");
$stmt->bindValue(':status', $status, SQLITE3_TEXT);
$stmt->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
// 获取玩家名用于日志
$stmt2 = $db->prepare("SELECT player_name FROM pending_player_validations WHERE id = :id");
$stmt2->bindValue(':id', $id, SQLITE3_INTEGER);
$rs = $stmt2->execute();
$row = $rs->fetchArray(SQLITE3_ASSOC);
$player = $row ? $row['player_name'] : '?';
debugLog("validation_callback: 玩家 {$player} 验证结果={$status}, type={$reqType}");
// ★ 验证通过时,自动执行挂起的操作
if ($status === 'valid') {
// --- add_group_member: 自动将玩家加入用户组 ---
if ($reqType === 'add_group_member' && !empty($reqData['group'])) {
$group = $reqData['group'];
$now = time();
$expiry = (int)($reqData['expiry_time'] ?? 0);
// ★ 如果没指定expiry_time,根据用户组的duration_minutes计算
if ($expiry <= 0) {
try {
$stmtCfg = $db->prepare("SELECT duration_minutes FROM web_user_groups WHERE group_name = :group");
$stmtCfg->bindValue(':group', $group, SQLITE3_TEXT);
$rsCfg = $stmtCfg->execute();
$cfgRow = $rsCfg->fetchArray(SQLITE3_ASSOC);
if ($cfgRow && (int)$cfgRow['duration_minutes'] > 0) {
$expiry = $now + (int)$cfgRow['duration_minutes'] * 60;
}
} catch (\Throwable $e) { /* 表可能不存在 */ }
}
try {
$stmt3 = $db->prepare("INSERT OR REPLACE INTO web_user_group_members
(player_name, group_name, added_by, added_time, expiry_time)
VALUES (:player, :group, 'admin', :added, :expiry)");
$stmt3->bindValue(':player', $player, SQLITE3_TEXT);
$stmt3->bindValue(':group', $group, SQLITE3_TEXT);
$stmt3->bindValue(':added', $now, SQLITE3_INTEGER);
$stmt3->bindValue(':expiry', $expiry, SQLITE3_INTEGER);
$stmt3->execute();
// 写入变更队列,通知Java端同步
$stmt4 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('group_change', :id, :name, :data, :time)");
$stmt4->bindValue(':id', $group, SQLITE3_TEXT);
$stmt4->bindValue(':name', $group, SQLITE3_TEXT);
$stmt4->bindValue(':data', json_encode(['action' => 'add_member', 'group_name' => $group, 'player' => $player]), SQLITE3_TEXT);
$stmt4->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt4->execute();
} catch (\Throwable $e) { /* 静默 */ }
debugLog("validation_callback: 自动执行add_group_member", ['player' => $player, 'group' => $group]);
}
// --- add_visitor: 自动将玩家加入领地访客列表 ---
if ($reqType === 'add_visitor' && !empty($reqData['land'])) {
$landName = $reqData['land'];
$now = time();
try {
$stmt5 = $db->prepare("INSERT OR IGNORE INTO web_area_visitors (land_name, player_name, added_by, added_time)
VALUES (:land, :player, 'admin', :time)");
$stmt5->bindValue(':land', $landName, SQLITE3_TEXT);
$stmt5->bindValue(':player', $player, SQLITE3_TEXT);
$stmt5->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt5->execute();
// 写入变更队列,通知Java端
$stmt6 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('visitor_change', :id, :name, :data, :time)");
$stmt6->bindValue(':id', $landName, SQLITE3_TEXT);
$stmt6->bindValue(':name', $landName, SQLITE3_TEXT);
$stmt6->bindValue(':data', json_encode(['action' => 'add_visitor', 'player' => $player]), SQLITE3_TEXT);
$stmt6->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt6->execute();
} catch (\Throwable $e) { /* 静默 */ }
debugLog("validation_callback: 自动执行add_visitor", ['player' => $player, 'land' => $landName]);
}
}
debugLog("validation_callback: 玩家 {$player} 验证结果={$status}");
echo json_encode(['success' => true]);
}
@@ -1952,14 +2025,7 @@ function handleDeleteUserGroup($db, $name) {
function handleListGroupMembers($db, $group) {
if (empty($group)) { echo json_encode(['success' => false, 'error' => 'missing group']); 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)
)");
try {
$stmt = $db->prepare("SELECT * FROM web_user_group_members WHERE group_name = :group");
$stmt->bindValue(':group', $group, SQLITE3_TEXT);
$rs = $stmt->execute();
@@ -1968,6 +2034,9 @@ function handleListGroupMembers($db, $group) {
$members[] = $row;
}
echo json_encode(['success' => true, 'members' => $members]);
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => '数据库错误: ' . $e->getMessage()]);
}
}
function handleAddGroupMember($db, $data) {
@@ -1982,29 +2051,30 @@ function handleAddGroupMember($db, $data) {
echo json_encode(['success' => false, 'error' => "玩家名格式不正确,仅支持英文字母、数字和下划线(3-16位)"]);
return;
}
// ★ 校验玩家是否存在
$playerValid = validatePlayerViaJava($db, $player, 'add_group_member', ['group' => $group]);
if ($playerValid === null) {
// 异步验证中 → 稍后重试(不直接写DB,等Java验证)
echo json_encode(['success' => true, 'pending' => true, 'message' => "玩家 {$player} 的验证请求已提交,系统正在验证,请稍候再试"]);
return;
}
// ★ 管理面板操作:直接用本地playerExists验证(无需异步等待Java)
$playerValid = playerExists($db, $player);
if (!$playerValid) {
echo json_encode(['success' => false, 'error' => "玩家 {$player} 尚未注册,请确认玩家名是否正确"]);
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)
)");
$addedBy = $data['added_by'] ?? 'admin';
$now = time();
$expiry = (int)($data['expiry_time'] ?? 0);
// ★ 如果没指定expiry_time,根据用户组的duration_minutes计算
if ($expiry <= 0) {
try {
$stmtCfg = $db->prepare("SELECT duration_minutes FROM web_user_groups WHERE group_name = :group");
$stmtCfg->bindValue(':group', $group, SQLITE3_TEXT);
$rsCfg = $stmtCfg->execute();
$cfgRow = $rsCfg->fetchArray(SQLITE3_ASSOC);
if ($cfgRow && (int)$cfgRow['duration_minutes'] > 0) {
$expiry = $now + (int)$cfgRow['duration_minutes'] * 60;
}
} catch (\Throwable $e) { /* 表可能不存在 */ }
}
try {
$stmt = $db->prepare("INSERT OR REPLACE INTO web_user_group_members
(player_name, group_name, added_by, added_time, expiry_time)
VALUES (:player, :group, :addedby, :added, :expiry)");
@@ -2014,19 +2084,13 @@ function handleAddGroupMember($db, $data) {
$stmt->bindValue(':added', $now, SQLITE3_INTEGER);
$stmt->bindValue(':expiry', $expiry, SQLITE3_INTEGER);
$stmt->execute();
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => '数据库错误: ' . $e->getMessage()]);
return;
}
// ★ 写入变更队列,通知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 '{}',
created_at INTEGER DEFAULT 0,
acknowledged INTEGER DEFAULT 0,
acked_at INTEGER DEFAULT 0
)");
$stmt2 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('group_change', :id, :name, :data, :time)");
$stmt2->bindValue(':id', $group, SQLITE3_TEXT);
@@ -2046,23 +2110,19 @@ function handleRemoveGroupMember($db, $data) {
echo json_encode(['success' => false, 'error' => 'missing player or group']);
return;
}
try {
$stmt = $db->prepare("DELETE 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);
$stmt->execute();
$affected = $db->changes();
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => '数据库错误: ' . $e->getMessage()]);
return;
}
// ★ 写入变更队列,通知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 '{}',
created_at INTEGER DEFAULT 0,
acknowledged INTEGER DEFAULT 0,
acked_at INTEGER DEFAULT 0
)");
$stmt2 = $db->prepare("INSERT INTO web_admin_changes (change_type, target_id, target_name, change_data, created_at)
VALUES ('group_change', :id, :name, :data, :time)");
$stmt2->bindValue(':id', $group, SQLITE3_TEXT);
@@ -2072,7 +2132,11 @@ function handleRemoveGroupMember($db, $data) {
$stmt2->execute();
} catch (\Throwable $e) { /* 静默 */ }
if ($affected > 0) {
echo json_encode(['success' => true, 'message' => "{$player} 已移出用户组 {$group}"]);
} else {
echo json_encode(['success' => false, 'error' => "玩家 {$player} 不在用户组 {$group} 中"]);
}
}
function handleGetPlayerGroups($db, $player) {
+116
View File
@@ -469,6 +469,122 @@ function initTables(SQLite3 $db) {
$db->exec("CREATE INDEX IF NOT EXISTS idx_web_tickets_assigned ON web_tickets(assigned_to)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_web_ticket_replies_ticket ON web_ticket_replies(ticket_id)");
// ★ 用户组相关表(从handler函数中提取到这里统一创建,避免并发请求锁竞争)
$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
)");
// 用户组成员表
$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)
)");
// 管理员变更队列
$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 '{}',
created_at INTEGER DEFAULT 0,
acknowledged INTEGER DEFAULT 0,
acked_at INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending'
)");
// 待验证玩家表
$db->exec("CREATE TABLE IF NOT EXISTS pending_player_validations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_name TEXT NOT NULL,
request_type TEXT DEFAULT 'general',
request_data TEXT DEFAULT '{}',
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
validated_at INTEGER DEFAULT 0
)");
// 续费请求表
$db->exec("CREATE TABLE IF NOT EXISTS web_group_renew (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_name TEXT NOT NULL,
group_name TEXT NOT NULL,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
req_id TEXT DEFAULT '',
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
processed_at INTEGER DEFAULT 0,
remark TEXT DEFAULT ''
)");
// 过户请求表
$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,
cooldown_until INTEGER DEFAULT 0
)");
// 领地表
$db->exec("CREATE TABLE IF NOT EXISTS web_area_lands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
owner TEXT DEFAULT '',
world TEXT DEFAULT '',
x1 INTEGER DEFAULT 0,
z1 INTEGER DEFAULT 0,
x2 INTEGER DEFAULT 0,
z2 INTEGER DEFAULT 0,
y1 INTEGER DEFAULT 0,
y2 INTEGER DEFAULT 0,
area_size INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
admin_changed INTEGER DEFAULT 0,
deny_pvp INTEGER DEFAULT 0,
deny_fall_damage INTEGER DEFAULT 0,
deny_hunger INTEGER DEFAULT 0,
deny_all_damage INTEGER DEFAULT 0,
deny_fire_spread INTEGER DEFAULT 0,
deny_explosion INTEGER DEFAULT 0,
deny_mob_grief INTEGER DEFAULT 0,
deny_block_place INTEGER DEFAULT 0,
deny_block_break INTEGER DEFAULT 0
)");
// 领地权限表
$db->exec("CREATE TABLE IF NOT EXISTS web_area_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
land_id INTEGER NOT NULL,
player_name TEXT NOT NULL,
role TEXT DEFAULT 'visitor',
permissions TEXT DEFAULT '',
granted_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');