领地系统大重构:全局配置+数据入库+新权限+传送点+安全删除+容器扩展

- 全局配置改为area_config表(替代txt文件)
- area_lands扩展到61列,新增10个权限布尔+5个传送点坐标
- 新增投掷物/发光/红石/门禁/音频/拴绳/农作物/剪毛/投喂等权限
- /protect settp/tp传送点系统
- 删除1分钟延迟+二次确认
- 安全传送判定(空气+实体+边界3格)
- 容器权限覆盖所有UI类型
- .gitignore清理敏感文件
@
This commit is contained in:
ypsdf1
2026-06-23 16:51:30 +08:00
parent b50bfd4379
commit 98f9cb6410
16 changed files with 709 additions and 1651 deletions
+11
View File
@@ -5,3 +5,14 @@
*.db-journal
*.db-wal
*.db-shm
upload_debug.php
upload_via_http.php
upload_debug.ps1
debug_admin_api.php
debug_online.php
debug_region.php
debug_region_search.php
self_update.php
version_check.php
test_sync.php
db/debug_ip_v2.php
+8 -8
View File
@@ -2247,9 +2247,9 @@ async function loadLands(el) {
el.innerHTML = '<div style="text-align:center;padding:40px;color:var(--dim)">加载中...</div>';
try {
const [landsRes, shopRes, configRes] = await Promise.all([
fetch('api/land_api.php?action=list_lands').then(r => r.json()),
fetch('api/land_api.php?action=list_shop').then(r => r.json()),
fetch('api/land_api.php?action=get_config').then(r => r.json())
fetch('api/land_api.php?action=list_lands&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json()),
fetch('api/land_api.php?action=list_shop&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json()),
fetch('api/land_api.php?action=get_config&secret=sdf1_web_comm_2026_ypshidifu').then(r => r.json())
]);
let html = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">';
@@ -2368,12 +2368,12 @@ async function saveLandConfig() {
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=land_price_per_sqm&value=' + encodeURIComponent(price)
body: 'key=land_price_per_sqm&value=' + encodeURIComponent(price) + '&secret=sdf1_web_comm_2026_ypshidifu'
});
await fetch('api/land_api.php?action=update_config', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'key=max_lands&value=' + encodeURIComponent(maxLands)
body: 'key=max_lands&value=' + encodeURIComponent(maxLands) + '&secret=sdf1_web_comm_2026_ypshidifu'
});
document.querySelector('.modal-close')?.click();
loadLands(document.getElementById('C'));
@@ -2388,7 +2388,7 @@ function changeLandOwner(name, currentOwner) {
fetch('api/land_api.php?action=update_land_owner', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(newOwner)
body: 'name=' + encodeURIComponent(name) + '&owner=' + encodeURIComponent(newOwner) + '&secret=sdf1_web_comm_2026_ypshidifu'
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
@@ -2400,7 +2400,7 @@ function deleteLand(name) {
fetch('api/land_api.php?action=delete_land', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=' + encodeURIComponent(name)
body: 'name=' + encodeURIComponent(name) + '&secret=sdf1_web_comm_2026_ypshidifu'
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
@@ -2412,7 +2412,7 @@ function deleteShopItem(id) {
fetch('api/land_api.php?action=delete_shop_item', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'id=' + id
body: 'id=' + id + '&secret=sdf1_web_comm_2026_ypshidifu'
}).then(r => r.json()).then(d => {
if (d.success) loadLands(document.getElementById('C'));
else alert('失败: ' + (d.error||''));
+325 -3
View File
@@ -1,8 +1,8 @@
<?php
/**
* 领地系统API - 接收Java端领地数据 + 管理面板
* 领地系统API - 接收Java端领地数据 + 管理面板 + 玩家端
* POST: Java端推送领地数据
* GET: admin面板读取领地数据
* GET: admin/玩家面板读取领地数据
*/
header('Content-Type: application/json; charset=utf-8');
error_reporting(E_ERROR | E_PARSE);
@@ -20,11 +20,61 @@ try {
$method = $_SERVER['REQUEST_METHOD'];
// ===== 验证 =====
if ($method === 'POST') {
// 三种认证方式:
// 1. Java端推送: POST + secret
// 2. 管理面板: GET/POST + admin_token (admin.php已登录的session)
// 3. 玩家端: GET + player_token
$playerName = '';
$isAdmin = false;
// 同步类action只接受secret验证(Java端推送)
$syncActions = ['sync_lands', 'sync_shop', 'sync_permissions'];
// 管理面板action:支持admin_token或secret
$adminActions = ['list_lands', 'list_shop', 'get_config', 'update_config', 'delete_land', 'update_land_owner', 'delete_shop_item'];
// 玩家端action:需要token
$playerActions = ['my_lands', 'land_detail', 'add_visitor', 'remove_visitor', 'list_visitors', 'land_shop', 'buy_permission'];
if (in_array($action, $syncActions)) {
// 同步类:必须有secret
if (!validateSecret($secret)) {
echo json_encode(['success' => false, 'error' => 'invalid secret']);
exit;
}
} elseif (in_array($action, $adminActions)) {
// 管理面板:验证admin_token或secret
$adminToken = $_GET['admin_token'] ?? $_POST['admin_token'] ?? '';
if (!empty($adminToken)) {
// 用admin_token验证(admin.php的登录态)
$adminInfo = validateTokenSilent($adminToken);
if (!$adminInfo) {
echo json_encode(['success' => false, 'error' => '管理员认证失败', 'needLogin' => true]);
exit;
}
$isAdmin = true;
} elseif (!validateSecret($secret)) {
// 没有admin_token,尝试secret
echo json_encode(['success' => false, 'error' => '需要管理员认证']);
exit;
}
} elseif (in_array($action, $playerActions)) {
// 玩家端:需要token
$token = $_GET['token'] ?? '';
if (empty($token)) {
echo json_encode(['success' => false, 'error' => '需要登录', 'needLogin' => true]);
exit;
}
$tokenInfo = validateTokenSilent($token);
if (!$tokenInfo) {
echo json_encode(['success' => false, 'error' => 'token无效或已过期', 'needLogin' => true]);
exit;
}
$playerName = is_array($tokenInfo) ? $tokenInfo['player'] : $tokenInfo;
} else {
// 未知action:默认放行(部分公开GET读取)
if ($method === 'POST' && !validateSecret($secret)) {
echo json_encode(['success' => false, 'error' => 'invalid secret']);
exit;
}
}
$db = getDB();
@@ -43,6 +93,11 @@ try {
handleSyncShop($db, $_POST);
break;
// ===== Java端推送访客权限数据 =====
case 'sync_permissions':
handleSyncPermissions($db, $_POST);
break;
// ===== 管理面板:获取所有领地 =====
case 'list_lands':
if ($method !== 'GET') {
@@ -86,6 +141,46 @@ try {
handleDeleteShopItem($db, $_POST);
break;
// ===== 玩家端:我拥有的领地 =====
case 'my_lands':
handleMyLands($db, $playerName);
break;
// ===== 玩家端:领地详情(含访客列表)=====
case 'land_detail':
handleLandDetail($db, $playerName, $_GET['name'] ?? '');
break;
// ===== 玩家端:添加访客 =====
case 'add_visitor':
if ($method !== 'GET' && $method !== 'POST') {
echo json_encode(['success' => false, 'error' => 'GET/POST only']);
exit;
}
$req = $method === 'POST' ? $_POST : $_GET;
handleAddVisitor($db, $playerName, $req);
break;
// ===== 玩家端:移除访客 =====
case 'remove_visitor':
if ($method !== 'GET' && $method !== 'POST') {
echo json_encode(['success' => false, 'error' => 'GET/POST only']);
exit;
}
$req = $method === 'POST' ? $_POST : $_GET;
handleRemoveVisitor($db, $playerName, $req);
break;
// ===== 玩家端:访客列表 =====
case 'list_visitors':
handleListVisitors($db, $_GET['name'] ?? '');
break;
// ===== 玩家端:领地商店(购买访客权限)=====
case 'land_shop':
handleLandShop($db, $_GET['name'] ?? '');
break;
default:
echo json_encode(['success' => false, 'error' => 'unknown action']);
}
@@ -128,6 +223,20 @@ function initLandTables($db) {
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
)");
// ★ 访客权限表(Java端同步过来)
$db->exec("CREATE TABLE IF NOT EXISTS web_area_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
land_id INTEGER NOT NULL,
land_name TEXT DEFAULT '',
player_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'visitor',
permissions TEXT DEFAULT '',
granted_at INTEGER DEFAULT 0,
expires_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0,
UNIQUE(land_id, player_name)
)");
}
function handleSyncLands($db, $post) {
@@ -280,6 +389,219 @@ function handleDeleteShopItem($db, $post) {
echo json_encode(['success' => true]);
}
// ==================== Java端同步:访客权限 ====================
function handleSyncPermissions($db, $post) {
$perms = json_decode($post['permissions'] ?? '[]', true);
if (!is_array($perms)) {
echo json_encode(['success' => false, 'error' => 'invalid permissions data']);
return;
}
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_permissions
(land_id, land_name, player_name, role, permissions, granted_at, expires_at, synced_at)
VALUES (:land_id, :land_name, :player, :role, :perms, :granted, :expires, :synced)");
foreach ($perms as $p) {
$stmt->bindValue(':land_id', (int)($p['land_id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_name', $p['land_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':player', $p['player_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':role', $p['role'] ?? 'visitor', SQLITE3_TEXT);
$stmt->bindValue(':perms', $p['permissions'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':granted', (int)($p['granted_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':expires', (int)($p['expires_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
}
echo json_encode(['success' => true, 'count' => count($perms)]);
}
// ==================== 玩家端:我拥有的领地 ====================
function handleMyLands($db, $playerName) {
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE owner = :owner ORDER BY created_at DESC");
$stmt->bindValue(':owner', $playerName, SQLITE3_TEXT);
$result = $stmt->execute();
$lands = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$lands[] = $row;
}
echo json_encode(['success' => true, 'lands' => $lands, 'player' => $playerName]);
}
// ==================== 玩家端:领地详情(含访客) ====================
function handleLandDetail($db, $playerName, $landName) {
if (empty($landName)) {
echo json_encode(['success' => false, 'error' => '缺少领地名称']);
return;
}
// 获取领地信息
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $landName, SQLITE3_TEXT);
$result = $stmt->execute();
$land = $result->fetchArray(SQLITE3_ASSOC);
if (!$land) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
// 权限检查:只有所有者或管理员能查看详情
if ($land['owner'] !== $playerName) {
echo json_encode(['success' => false, 'error' => '你不是此领地的所有者']);
return;
}
// 获取访客列表
$stmt2 = $db->prepare("SELECT * FROM web_area_permissions WHERE land_id = :land_id ORDER BY granted_at DESC");
$stmt2->bindValue(':land_id', (int)$land['id'], SQLITE3_INTEGER);
$result2 = $stmt2->execute();
$visitors = [];
while ($row = $result2->fetchArray(SQLITE3_ASSOC)) {
$visitors[] = $row;
}
echo json_encode(['success' => true, 'land' => $land, 'visitors' => $visitors]);
}
// ==================== 玩家端:添加访客 ====================
function handleAddVisitor($db, $playerName, $req) {
$landName = $req['name'] ?? $req['land'] ?? '';
$visitor = $req['visitor'] ?? $req['player'] ?? '';
if (empty($landName) || empty($visitor)) {
echo json_encode(['success' => false, 'error' => '缺少领地名或玩家名']);
return;
}
// 获取领地
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $landName, SQLITE3_TEXT);
$result = $stmt->execute();
$land = $result->fetchArray(SQLITE3_ASSOC);
if (!$land) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
if ($land['owner'] !== $playerName) {
echo json_encode(['success' => false, 'error' => '只有领地所有者才能添加访客']);
return;
}
// 插入访客
$stmt2 = $db->prepare("INSERT OR REPLACE INTO web_area_permissions
(land_id, land_name, player_name, role, permissions, granted_at, expires_at, synced_at)
VALUES (:land_id, :land_name, :player, 'visitor', '', :now, 0, :now)");
$stmt2->bindValue(':land_id', (int)$land['id'], SQLITE3_INTEGER);
$stmt2->bindValue(':land_name', $landName, SQLITE3_TEXT);
$stmt2->bindValue(':player', $visitor, SQLITE3_TEXT);
$stmt2->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt2->execute();
echo json_encode(['success' => true, 'message' => "已添加访客: $visitor"]);
}
// ==================== 玩家端:移除访客 ====================
function handleRemoveVisitor($db, $playerName, $req) {
$landName = $req['name'] ?? $req['land'] ?? '';
$visitor = $req['visitor'] ?? $req['player'] ?? '';
if (empty($landName) || empty($visitor)) {
echo json_encode(['success' => false, 'error' => '缺少领地名或玩家名']);
return;
}
// 获取领地
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $landName, SQLITE3_TEXT);
$result = $stmt->execute();
$land = $result->fetchArray(SQLITE3_ASSOC);
if (!$land) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
if ($land['owner'] !== $playerName) {
echo json_encode(['success' => false, 'error' => '只有领地所有者才能移除访客']);
return;
}
$stmt2 = $db->prepare("DELETE FROM web_area_permissions WHERE land_id = :land_id AND player_name = :player");
$stmt2->bindValue(':land_id', (int)$land['id'], SQLITE3_INTEGER);
$stmt2->bindValue(':player', $visitor, SQLITE3_TEXT);
$stmt2->execute();
echo json_encode(['success' => true, 'message' => "已移除访客: $visitor"]);
}
// ==================== 玩家端:访客列表 ====================
function handleListVisitors($db, $landName) {
if (empty($landName)) {
echo json_encode(['success' => false, 'error' => '缺少领地名称']);
return;
}
$stmt = $db->prepare("SELECT * FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $landName, SQLITE3_TEXT);
$result = $stmt->execute();
$land = $result->fetchArray(SQLITE3_ASSOC);
if (!$land) {
echo json_encode(['success' => false, 'error' => '领地不存在']);
return;
}
$stmt2 = $db->prepare("SELECT * FROM web_area_permissions WHERE land_id = :land_id ORDER BY granted_at DESC");
$stmt2->bindValue(':land_id', (int)$land['id'], SQLITE3_INTEGER);
$result2 = $stmt2->execute();
$visitors = [];
while ($row = $result2->fetchArray(SQLITE3_ASSOC)) {
$visitors[] = $row;
}
echo json_encode(['success' => true, 'visitors' => $visitors]);
}
// ==================== 玩家端:领地商店 ====================
function handleLandShop($db, $landName) {
$sql = "SELECT * FROM web_area_shop WHERE status = 'active'";
$params = [];
if (!empty($landName)) {
$sql .= " AND land_name = :name";
$params[':name'] = $landName;
}
$sql .= " ORDER BY created_at DESC LIMIT 50";
$stmt = $db->prepare($sql);
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v, SQLITE3_TEXT);
}
$result = $stmt->execute();
$items = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$items[] = $row;
}
echo json_encode(['success' => true, 'items' => $items]);
}
function validateSecret($secret) {
if (empty($secret)) return false;
$validSecrets = [
+47
View File
@@ -305,6 +305,9 @@ switch ($action) {
case 'sync_land_shop':
syncLandShop();
break;
case 'sync_permissions':
syncPermissions();
break;
default:
error('未知操作: ' . $action);
}
@@ -528,6 +531,50 @@ function syncLandShop() {
success("权限商店同步成功: {$count}个");
}
// ===== 插件推送访客权限数据 =====
function syncPermissions() {
$secret = getParam('secret');
if ($secret !== SECRET_KEY) error('密钥验证失败', 403);
$permsRaw = getParam('permissions');
if (!$permsRaw) error('缺少permissions参数');
$perms = is_array($permsRaw) ? $permsRaw : json_decode($permsRaw, true);
if (!is_array($perms)) error('permissions格式无效');
$db = getDB();
$db->exec("CREATE TABLE IF NOT EXISTS web_area_permissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
land_id INTEGER NOT NULL,
land_name TEXT DEFAULT '',
player_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'visitor',
permissions TEXT DEFAULT '',
granted_at INTEGER DEFAULT 0,
expires_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0,
UNIQUE(land_id, player_name)
)");
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_permissions
(land_id, land_name, player_name, role, permissions, granted_at, expires_at, synced_at)
VALUES (:land_id, :land_name, :player, :role, :perms, :granted, :expires, :synced)");
$count = 0;
foreach ($perms as $p) {
$stmt->bindValue(':land_id', (int)($p['land_id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_name', $p['land_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':player', $p['player_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':role', $p['role'] ?? 'visitor', SQLITE3_TEXT);
$stmt->bindValue(':perms', $p['permissions'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':granted', (int)($p['granted_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':expires', (int)($p['expires_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
$count++;
}
success("访客权限同步成功: {$count}个");
}
// ===== 插件推送债券余额 =====
function syncBonds() {
// ★ 支持token或SECRET_KEY认证
+3 -2
View File
@@ -37,11 +37,12 @@ function getDB() {
}
$db = new SQLite3(DB_PATH);
$db->enableExceptions(true);
// 减少 PRAGMA 调用,避免可能的锁问题
// WAL模式 + 长等待超时防止database is locked
$db->exec('PRAGMA journal_mode=WAL');
$db->exec('PRAGMA busy_timeout=15000'); // 数据库锁等待15秒
$db->exec('PRAGMA busy_timeout=30000'); // 30秒等待
$db->exec('PRAGMA synchronous=NORMAL');
$db->exec('PRAGMA cache_size=-64000'); // 64MB 缓存
$db->exec('PRAGMA wal_autocheckpoint=1000');
initTables($db);
migrateDatabase($db); // 迁移旧数据库
}
-615
View File
@@ -1,615 +0,0 @@
<?php
// ★ IP查询调试页面 v2 - 完整追踪查询流程
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('max_execution_time', 120);
// ★ 关键:在任何输出之前设置缓冲区
ob_start();
// 不要包含 admin.php(它会输出JSON并exit)
// 只包含 core.php
require_once __DIR__ . '/../core.php';
// 清空输出缓冲区
while (ob_get_level() > 0) {
ob_end_clean();
}
// 现在开始输出HTML
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IP查询调试 v2</title>
<style>
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
.ok { color: #0f0; }
.err { color: #f00; }
.warn { color: #ff0; }
.info { color: #00f; }
pre { background: #333; padding: 10px; overflow-x: auto; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; }
.step { margin: 10px 0; padding: 10px; border-left: 3px solid #0f0; }
.step.error { border-left-color: #f00; }
.step.warn { border-left-color: #ff0; }
.api-result { margin: 5px 0; padding: 5px; background: #222; }
</style>
</head>
<body>
<h1>🔍 IP查询调试页面 v2</h1>
<?php
// 获取要测试的IP(支持逗号分隔多IP)
$allIps = [];
$ipParam = trim($_GET['ip'] ?? '');
if ($ipParam) {
$allIps = array_map('trim', explode(',', $ipParam));
}
$ip2Param = trim($_GET['ip2'] ?? '');
if ($ip2Param) {
$allIps = array_merge($allIps, array_map('trim', explode(',', $ip2Param)));
}
// 去重并过滤空值
$allIps = array_unique(array_filter($allIps));
if (empty($allIps)) {
$allIps = ['36.148.150.193', '120.231.236.80'];
}
echo "<h2>测试IP (" . count($allIps) . "个): " . implode(', ', $allIps) . "</h2>";
// ===== 第1步:检查数据库中的当前状态 =====
echo "<div class='step'>";
echo "<h3>📊 步骤1: 检查数据库当前状态</h3>";
try {
$db = getDB();
foreach ($allIps as $ip) {
echo "<p><strong>$ip:</strong></p>";
$stmt = $db->prepare("SELECT * FROM player_ip_locations WHERE ip_address = :ip");
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row) {
echo "<pre>" . json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "</pre>";
} else {
echo "<p class='warn'>数据库中无此IP记录</p>";
}
}
} catch (Exception $e) {
echo "<p class='err'>错误: " . $e->getMessage() . "</p>";
}
echo "</div>";
// ===== 第2步:测试4个API接口(直接调用,不包含admin.php) =====
echo "<div class='step'>";
echo "<h3>🌐 步骤2: 测试4个API接口</h3>";
// ★ 直接定义函数,不包含 admin.php
function testFetchPconline($ip) {
$url = 'https://whois.pconline.com.cn/ipJson.jsp?ip=' . urlencode($ip) . '&json=true';
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 SDF1_IP_Lookup/1.0"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'PConline',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
// ★ 修复编码问题:太平洋返回GBK编码,需要转换为UTF-8
$response = trim($response);
if (mb_detect_encoding($response, 'UTF-8', true) === false) {
$response = mb_convert_encoding($response, 'UTF-8', 'GBK');
$result['raw_response'] = $response; // 更新原始响应为转换后的
}
$json = $response;
if (preg_match('/^\s*\(?[\'"]?(.*?)[\'"]?\)?\s*$/s', $json, $matches)) {
$json = $matches[1];
}
$data = json_decode($json, true);
$result['parsed'] = $data;
if (!$data || !is_array($data)) {
$result['error'] = 'JSON解析失败';
return $result;
}
// 优先使用 addr 字段
if (isset($data['addr']) && trim($data['addr']) !== '') {
$addr = trim($data['addr']);
if (strpos($addr, '中国 ') !== 0) {
$result['location'] = $addr;
return $result;
}
}
// 尝试用 pro + city 手动拼接
if (isset($data['pro']) && trim($data['pro']) !== '') {
$loc = trim($data['pro']);
if (isset($data['city']) && trim($data['city']) !== '') {
$loc .= trim($data['city']);
}
if (isset($data['addr'])) {
$parts = preg_split('/\s+/', trim($data['addr']), -1, PREG_SPLIT_NO_EMPTY);
if (!empty($parts)) {
$op = end($parts);
if ($op !== '中国') {
$loc .= ' ' . $op;
}
}
}
$result['location'] = strlen($loc) > 1 ? $loc : null;
}
return $result;
}
function testFetchBaidu($ip) {
$url = 'https://opendata.baidu.com/api.php?query=' . urlencode($ip) . '&resource_id=6006&oe=utf8&format=json';
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'Baidu',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
if ($data && isset($data['data']) && is_array($data['data']) && !empty($data['data'])) {
$loc = $data['data'][0]['location'] ?? null;
if ($loc && $loc !== '--' && strpos($loc, '中国 ') !== 0) {
$result['location'] = $loc;
}
}
return $result;
}
function testFetchAa1($ip) {
$url = 'https://v.api.aa1.cn/api/ipcha-baidu/?ip=' . urlencode($ip);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'aa1.cn',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
if ($data && isset($data['code']) && $data['code'] == 1) {
$loc = $data['ip_add'] ?? null;
if ($loc && $loc !== '未知' && strpos($loc, '中国 ') !== 0) {
$result['location'] = $loc;
}
}
return $result;
}
function testFetchIp9($ip) {
// ★ 使用正确的 API 地址:ip9.com.cn/get
$url = 'https://ip9.com.cn/get?ip=' . urlencode($ip);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json, text/plain, */*\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\nReferer: https://ip9.com.cn/\r\nOrigin: https://ip9.com.cn"
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'IP9',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
$debug = [];
// IP9 返回格式有两种可能:
// 旧版: {"ret":"200","data":[{"ip":"...","prov":"广东","city":"深圳","isp":"中国移动"}]}
// 新版: {"ret":"200","data":{"ip":"...","prov":"广东","city":"深圳","isp":"中国移动"}}
// ★ 兼容两种格式
$item = null;
if (isset($data['data'][0]) && is_array($data['data'][0])) {
$item = $data['data'][0];
$debug['format'] = 'array';
} elseif (isset($data['data']) && is_array($data['data']) && isset($data['data']['prov'])) {
$item = $data['data'];
$debug['format'] = 'object';
} else {
$debug['format'] = 'unknown';
$debug['data_type'] = gettype($data['data'] ?? null);
}
if ($item) {
$loc = '';
$debug['item_prov'] = $item['prov'] ?? 'NOT_SET';
$debug['item_city'] = $item['city'] ?? 'NOT_SET';
$debug['item_isp'] = $item['isp'] ?? 'NOT_SET';
if (isset($item['prov'])) $loc .= $item['prov'];
if (isset($item['city'])) $loc .= $item['city'];
if (isset($item['isp'])) $loc .= ' ' . $item['isp'];
$debug['loc'] = $loc;
$debug['loc_len'] = strlen($loc);
if (strlen($loc) > 0) {
$result['location'] = $loc;
}
}
$result['debug'] = $debug;
return $result;
}
// 测试每个IP
foreach ($allIps as $ip) {
echo "<h4>测试IP: $ip</h4>";
// API1: PConline
echo "<div class='api-result'>";
echo "<p><strong>API1 - PConline:</strong></p>";
$start = microtime(true);
$r1 = testFetchPconline($ip);
$time1 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time1, 2) . "ms\n";
echo "原始响应: " . substr($r1['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r1['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r1['location'] ?? 'null') . "\n";
echo "错误: " . ($r1['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API2: Baidu
echo "<div class='api-result'>";
echo "<p><strong>API2 - Baidu:</strong></p>";
$start = microtime(true);
$r2 = testFetchBaidu($ip);
$time2 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time2, 2) . "ms\n";
echo "原始响应: " . substr($r2['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r2['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r2['location'] ?? 'null') . "\n";
echo "错误: " . ($r2['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API3: aa1.cn
echo "<div class='api-result'>";
echo "<p><strong>API3 - aa1.cn:</strong></p>";
$start = microtime(true);
$r3 = testFetchAa1($ip);
$time3 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time3, 2) . "ms\n";
echo "原始响应: " . substr($r3['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r3['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r3['location'] ?? 'null') . "\n";
echo "错误: " . ($r3['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API4: IP9
echo "<div class='api-result'>";
echo "<p><strong>API4 - IP9:</strong></p>";
$start = microtime(true);
$r4 = testFetchIp9($ip);
$time4 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time4, 2) . "ms\n";
echo "原始响应: " . substr($r4['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r4['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r4['location'] ?? 'null') . "\n";
echo "错误: " . ($r4['error'] ?? '无') . "\n";
echo "调试信息: " . json_encode($r4['debug'] ?? [], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
echo "</pre>";
echo "</div>";
echo "<hr>";
}
echo "</div>";
// ===== 第3步:模拟入库逻辑 =====
echo "<div class='step'>";
echo "<h3>🔄 步骤3: 模拟入库逻辑</h3>";
echo "<p>以下是服务器处理IP归属地的完整流程:</p>";
// 模拟 queryIpLocation 函数的逻辑
foreach ($allIps as $ip) {
echo "<h4>模拟IP: $ip</h4>";
echo "<p><strong>1. 检查是否是内网IP</strong></p>";
if (!$ip || $ip === '-' || $ip === '127.0.0.1' || strpos($ip, '10.') === 0 || strpos($ip, '192.168.') === 0) {
echo "<p class='warn'>是内网IP,返回 '内网IP'</p>";
continue;
}
echo "<p>不是内网IP,继续查询</p>";
echo "<p><strong>2. 检查数据库缓存</strong></p>";
$stmt = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip LIMIT 1");
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row && isset($row['location'])) {
$cachedLoc = $row['location'];
echo "<p>找到缓存: $cachedLoc</p>";
// ★ 使用统一校验函数
$isValidFormat = isValidIpLocationFormat($cachedLoc);
if (!$isValidFormat) {
echo "<p class='err'>无效格式: " . htmlspecialchars($cachedLoc) . "</p>";
}
echo "<p>格式验证结果: " . ($isValidFormat ? "✅ 有效" : "❌ 无效") . "</p>";
if (!$isValidFormat) {
echo "<p>将删除此缓存并重新查询</p>";
$stmt2 = $db->prepare("DELETE FROM player_ip_locations WHERE ip_address = :ip");
$stmt2->bindValue(':ip', $ip, SQLITE3_TEXT);
$stmt2->execute();
} else {
echo "<p class='ok'>格式有效,直接使用缓存</p>";
continue;
}
} else {
echo "<p>无缓存,需要查询</p>";
}
echo "<p><strong>3. 调用API查询(格式无效则继续下一个)</strong></p>";
// ★ 与 admin.php 的 doQueryIpLocation() 一致:每个API返回后先验证格式
$location = null;
$bestResult = null;
// API1: PConline
echo "<p>尝试 API1 (PConline)...</p>";
$r1 = testFetchPconline($ip);
if ($r1['location']) {
echo "<p>返回: " . htmlspecialchars($r1['location']) . "</p>";
if (isValidIpLocationFormat($r1['location'])) {
$location = $r1['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
$bestResult = $r1['location'];
}
} else {
echo "<p class='err'>❌ API1 无结果</p>";
}
// API2: Baidu(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API2 (Baidu)...</p>";
$r2 = testFetchBaidu($ip);
if ($r2['location']) {
echo "<p>返回: " . htmlspecialchars($r2['location']) . "</p>";
if (isValidIpLocationFormat($r2['location'])) {
$location = $r2['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r2['location'];
}
} else {
echo "<p class='err'>❌ API2 无结果</p>";
}
}
// API3: aa1.cn(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API3 (aa1.cn)...</p>";
$r3 = testFetchAa1($ip);
if ($r3['location']) {
echo "<p>返回: " . htmlspecialchars($r3['location']) . "</p>";
if (isValidIpLocationFormat($r3['location'])) {
$location = $r3['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r3['location'];
}
} else {
echo "<p class='err'>❌ API3 无结果</p>";
}
}
// API4: IP9(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API4 (IP9)...</p>";
$r4 = testFetchIp9($ip);
if ($r4['location']) {
echo "<p>返回: " . htmlspecialchars($r4['location']) . "</p>";
if (isValidIpLocationFormat($r4['location'])) {
$location = $r4['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r4['location'];
}
} else {
echo "<p class='err'>❌ API4 无结果</p>";
}
}
// 所有API都没返回有效格式:标记为查询失败
if (!$location) {
$location = '查询失败';
echo '<p class="err">❌ 所有API均失败或格式无效,标记为"查询失败"</p>';
}
echo "<p><strong>4. 格式验证和修复(与 admin.php 一致)</strong></p>";
echo "<p>原始位置: $location</p>";
// 格式修复逻辑
if ($location && $location !== '查询失败') {
$loc = $location;
// 1. 过滤英文格式
if (strpos($loc, 'China - ') === 0 || strpos($loc, 'China -') === 0) {
echo "<p class='warn'>拒绝英文格式: $loc</p>";
$location = null;
}
// 2. 过滤不完整的中文格式
if (strpos($loc, '中国 - -') === 0 || strpos($loc, '中国 - -') === 0) {
echo "<p class='warn'>拒绝不完整格式: $loc</p>";
$location = null;
}
// 3. 格式修复:确保"位置"和"运营商"之间有空格
// 仅在没有空格时修复(如"广东省移动"→"广东省 移动")
if ($location && strpos($loc, ' ') === false) {
// 无空格,尝试添加空格
if (preg_match('/^(.*?(?:省|市|州|盟|地区|区|县))(.+)$/u', $loc, $matches)) {
$loc = $matches[1] . ' ' . $matches[2];
echo "<p>格式修复: $location => $loc</p>";
$location = $loc;
}
}
// ★ 4. 存储前格式验证(使用统一校验函数)
if ($location) {
$shouldStore = isValidIpLocationFormat($location);
if (!$shouldStore) {
echo "<p class='err'>格式无效(不满足入库条件): " . htmlspecialchars($location) . "</p>";
$location = null;
}
}
}
echo "<p><strong>5. 存入数据库</strong></p>";
if ($location && $location !== '查询失败') {
echo "<p class='ok'>将存储: $location</p>";
// 模拟存储(不实际执行)
echo "<pre>INSERT OR REPLACE INTO player_ip_locations (player_name, ip_address, location, updated_at) VALUES ('global', '$ip', '$location', " . time() . ")</pre>";
} else {
echo "<p class='err'>不存储(查询失败或格式无效)</p>";
}
echo "<hr>";
}
echo "</div>";
// ===== 第4步:显示所有无效格式的记录 =====
echo "<div class='step'>";
echo "<h3>❌ 步骤4: 显示所有无效格式的记录</h3>";
$invalidStmt = $db->query("
SELECT ip_address, location
FROM player_ip_locations
WHERE location LIKE 'China%'
OR location = '-'
OR location = '--'
OR location = '查询失败'
OR location = ''
OR ip_address LIKE '127.%'
OR ip_address LIKE '192.168.%'
OR ip_address LIKE '10.%'
OR location LIKE '中国 - -%'
OR location LIKE '中国 - -%'
OR (location NOT LIKE '% %' AND (location LIKE '%省%' OR location LIKE '%市%'))
");
$invalidCount = 0;
while ($row = $invalidStmt->fetchArray(SQLITE3_ASSOC)) {
echo "<p>" . $row['ip_address'] . " => " . $row['location'] . "</p>";
$invalidCount++;
}
if ($invalidCount == 0) {
echo "<p class='ok'>没有发现无效格式的记录</p>";
} else {
echo "<p class='warn'>发现 $invalidCount 条无效格式的记录</p>";
}
echo "</div>";
echo "<hr>";
echo "<p>💡 此页面用于调试IP查询流程,检查每个API的返回结果和入库逻辑。</p>";
?>
</body>
</html>
-144
View File
@@ -1,144 +0,0 @@
<?php
// 管理后台API诊断脚本
while (ob_get_level() > 0) { @ob_end_clean(); }
header('Content-Type: text/plain; charset=utf-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
echo "=== Admin API Diagnostic ===\n\n";
require_once __DIR__ . '/core.php';
echo "1. Core loaded: " . (function_exists('getDB') ? 'YES' : 'NO') . "\n";
echo "2. ADMIN_PASS defined: " . (defined('ADMIN_PASS') ? 'YES' : 'NO') . "\n";
// Test database connection
echo "\n--- Database ---\n";
try {
$db = getDB();
echo "DB opened OK\n";
// List all tables
$result = $db->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
$tables = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$tables[] = $row['name'];
}
echo "Tables (" . count($tables) . "): " . implode(', ', $tables) . "\n";
// Check player_ip_changes structure
if (in_array('player_ip_changes', $tables)) {
$result = $db->query("PRAGMA table_info(player_ip_changes)");
echo "\nplayer_ip_changes columns:\n";
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " - {$row['name']} ({$row['type']}) PK=" . ($row['pk'] ? 'YES' : 'no') . "\n";
}
$cnt = $db->query("SELECT COUNT(*) as c FROM player_ip_changes")->fetchArray(SQLITE3_ASSOC)['c'];
echo " Rows: $cnt\n";
}
// Check player_ip_locations structure
if (in_array('player_ip_locations', $tables)) {
$result = $db->query("PRAGMA table_info(player_ip_locations)");
echo "\nplayer_ip_locations columns:\n";
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " - {$row['name']} ({$row['type']}) PK=" . ($row['pk'] ? 'YES' : 'no') . "\n";
}
$cnt = $db->query("SELECT COUNT(*) as c FROM player_ip_locations")->fetchArray(SQLITE3_ASSOC)['c'];
echo " Rows: $cnt\n";
// Show a sample
$result = $db->query("SELECT ip_address, location FROM player_ip_locations LIMIT 5");
if ($result) {
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " Sample: {$row['ip_address']} => {$row['location']}\n";
}
}
}
// Check users table
if (in_array('users', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM users")->fetchArray(SQLITE3_ASSOC)['c'];
echo "\nusers rows: $cnt\n";
}
// Check online_players
if (in_array('online_players', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM online_players")->fetchArray(SQLITE3_ASSOC)['c'];
echo "online_players rows: $cnt\n";
}
// Check web_session_log
if (in_array('web_session_log', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM web_session_log")->fetchArray(SQLITE3_ASSOC)['c'];
echo "web_session_log rows: $cnt\n";
}
// Test batchGetPlayerIps
echo "\n--- Test batchGetPlayerIps ---\n";
if (function_exists('batchGetPlayerIps') || class_exists('adminListUsersPaginated')) {
echo "batchGetPlayerIps function NOT available in this scope\n";
} else {
echo "Function not available (defined in api/admin.php, not core.php)\n";
}
// Test simple queries that adminListUsersPaginated would run
echo "\n--- Test Query Simulation ---\n";
try {
$sql = "SELECT player_name, register_time, last_login_time, points, total_online_time, email FROM users ORDER BY register_time DESC LIMIT 10 OFFSET 0";
$result = $db->query($sql);
$count = 0;
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$count++;
}
echo "Users query OK, got $count rows\n";
} catch (\Throwable $e) {
echo "Users query FAILED: " . $e->getMessage() . "\n";
}
// Test player_ip_changes query
try {
$stmt = $db->prepare("SELECT player_name, new_ip AS ip_address FROM player_ip_changes WHERE player_name IN ('test') ORDER BY changed_at DESC");
echo "player_ip_changes simple query: OK (prepared)\n";
} catch (\Throwable $e) {
echo "player_ip_changes query FAILED: " . $e->getMessage() . "\n";
}
// Test player_ip_locations query
try {
$stmt = $db->prepare("SELECT ip_address, location FROM player_ip_locations WHERE ip_address IN ('1.2.3.4')");
echo "player_ip_locations query: OK (prepared)\n";
} catch (\Throwable $e) {
echo "player_ip_locations query FAILED: " . $e->getMessage() . "\n";
}
// Test location search query
try {
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' ORDER BY changed_at DESC LIMIT 5");
$result = $stmt->execute();
$ips = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$ips[] = $row['new_ip'];
}
echo "Location search pre-query: OK, got " . count($ips) . " IPs\n";
} catch (\Throwable $e) {
echo "Location search pre-query FAILED: " . $e->getMessage() . "\n";
}
// Test region search SQL
try {
$searchBound = "%长沙%";
$stmt = $db->prepare("SELECT COUNT(*) as cnt FROM player_ip_locations WHERE location LIKE :region");
$stmt->bindValue(':region', $searchBound, SQLITE3_TEXT);
$cnt = $stmt->execute()->fetchArray(SQLITE3_ASSOC)['cnt'];
echo "Region cache check: $cnt matching records\n";
} catch (\Throwable $e) {
echo "Region cache check FAILED: " . $e->getMessage() . "\n";
}
} catch (\Throwable $e) {
echo "\nFATAL ERROR: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "Stack: " . $e->getTraceAsString() . "\n";
}
echo "\n=== Done ===\n";
-142
View File
@@ -1,142 +0,0 @@
<?php
/**
* 调试地区搜索问题
* 显示 player_ip_locations 和 player_ip_changes 表内容,以及地区搜索SQL测试结果
*/
header('Content-Type: text/html; charset=utf-8');
require_once 'core.php';
$db = getDB();
echo "<h2>调试地区搜索</h2>";
// 1. player_ip_locations 表内容
echo "<h3>player_ip_locations 表</h3>";
try {
$result = $db->query("SELECT COUNT(*) as cnt FROM player_ip_locations");
$row = $result->fetchArray(SQLITE3_ASSOC);
echo "总记录数: " . $row['cnt'] . "<br>";
if ($row['cnt'] > 0) {
echo "<table border='1' cellpadding='5'>";
echo "<tr><th>ip_address</th><th>location</th><th>player_name</th><th>updated_at</th></tr>";
$result = $db->query("SELECT * FROM player_ip_locations ORDER BY updated_at DESC LIMIT 20");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "<tr>";
echo "<td>" . htmlspecialchars($r['ip_address']) . "</td>";
echo "<td>" . htmlspecialchars($r['location']) . "</td>";
echo "<td>" . htmlspecialchars($r['player_name']) . "</td>";
echo "<td>" . date('Y-m-d H:i:s', $r['updated_at']) . "</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "表为空!<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 2. player_ip_changes 表内容
echo "<h3>player_ip_changes 表</h3>";
try {
$result = $db->query("SELECT COUNT(*) as cnt FROM player_ip_changes");
$row = $result->fetchArray(SQLITE3_ASSOC);
echo "总记录数: " . $row['cnt'] . "<br>";
if ($row['cnt'] > 0) {
echo "<table border='1' cellpadding='5'>";
echo "<tr><th>player_name</th><th>new_ip</th><th>changed_at</th></tr>";
$result = $db->query("SELECT * FROM player_ip_changes ORDER BY changed_at DESC LIMIT 20");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "<tr>";
echo "<td>" . htmlspecialchars($r['player_name']) . "</td>";
echo "<td>" . htmlspecialchars($r['new_ip']) . "</td>";
echo "<td>" . date('Y-m-d H:i:s', $r['changed_at']) . "</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "表为空!<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 3. 地区搜索SQL测试
echo "<h3>地区搜索SQL测试</h3>";
$search = '长沙';
$regionKey = $search;
// 测试JOIN
$sql = "SELECT c.player_name FROM player_ip_changes c
INNER JOIN player_ip_locations l ON c.new_ip = l.ip_address
WHERE l.location LIKE '%$regionKey%' AND c.new_ip != '' AND c.new_ip != '-'
GROUP BY c.player_name";
echo "SQL: <pre>" . htmlspecialchars($sql) . "</pre>";
try {
$result = $db->query($sql);
$count = 0;
$players = [];
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$count++;
$players[] = $r['player_name'];
}
echo "匹配玩家数: $count<br>";
if ($count > 0) {
echo "匹配玩家: " . implode(', ', $players) . "<br>";
}
} catch (Exception $e) {
echo "SQL错误: " . $e->getMessage() . "<br>";
}
// 4. 检查所有location中包含"长沙"的
echo "<h3>包含'长沙'的location记录</h3>";
try {
$result = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations WHERE location LIKE '%长沙%'");
$found = false;
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$found = true;
echo "IP: " . htmlspecialchars($r['ip_address']) . " → " . htmlspecialchars($r['location']) . "<br>";
}
if (!$found) {
echo "没有找到包含'长沙'的location记录<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 5. 检查users表中是否有来自长沙的玩家(通过IP推断)
echo "<h3>测试:直接搜索users表</h3>";
try {
$result = $db->query("SELECT player_name, email FROM users LIMIT 10");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "玩家: " . htmlspecialchars($r['player_name']) . " (email: " . htmlspecialchars($r['email']) . ")<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 6. 尝试手动查询一个IP的归属地
echo "<h3>测试:手动查询IP归属地</h3>";
try {
$result = $db->query("SELECT DISTINCT new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' AND new_ip NOT LIKE '192.168.%' AND new_ip != '127.0.0.1' LIMIT 5");
$ips = [];
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$ips[] = $r['new_ip'];
}
if (!empty($ips)) {
echo "测试查询以下IP的归属地: " . implode(', ', $ips) . "<br>";
require_once 'api/admin.php';
foreach ($ips as $ip) {
$loc = queryIpLocationWithTimeout($ip, 3);
echo "IP: $ip → " . ($loc ? $loc['location'] : 'null') . "<br>";
}
} else {
echo "没有找到可用的外部IP<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
?>
-167
View File
@@ -1,167 +0,0 @@
<?php
// 地区搜索诊断脚本 - 直接测试PHP两步法
while (ob_get_level() > 0) { @ob_end_clean(); }
header('Content-Type: text/plain; charset=utf-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
echo "=== Region Search Debug v2 ===\n\n";
require_once __DIR__ . '/core.php';
try {
$db = getDB();
echo "DB opened OK\n\n";
// 1. Check tables and data
echo "--- Data Overview ---\n";
$tables = ['users', 'player_ip_changes', 'player_ip_locations', 'web_session_log'];
foreach ($tables as $table) {
try {
$cnt = $db->query("SELECT COUNT(*) as c FROM $table")->fetchArray(SQLITE3_ASSOC)['c'];
echo "$table: $cnt rows\n";
} catch (\Throwable $e) {
echo "$table: ERROR - " . $e->getMessage() . "\n";
}
}
// 2. Check player_ip_locations samples
echo "\n--- player_ip_locations Samples ---\n";
try {
$result = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations LIMIT 10");
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " {$row['ip_address']} => '{$row['location']}' (player: {$row['player_name']})\n";
}
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
// 3. Test region search for "长沙"
$regionKey = '长沙';
echo "\n--- Testing Region Search: '$regionKey' ---\n";
// Step 1: Find IPs with matching location
try {
$locStmt = $db->prepare("SELECT ip_address FROM player_ip_locations WHERE location LIKE :region");
$locStmt->bindValue(':region', "%$regionKey%", SQLITE3_TEXT);
$locResult = $locStmt->execute();
$matchedIps = [];
while ($locRow = $locResult->fetchArray(SQLITE3_ASSOC)) {
$matchedIps[] = $locRow['ip_address'];
}
echo "Step 1: Found " . count($matchedIps) . " IPs with '$regionKey' location\n";
if (!empty($matchedIps)) {
echo " IPs: " . implode(', ', array_slice($matchedIps, 0, 10)) . "\n";
}
} catch (\Throwable $e) {
echo "Step 1 ERROR: " . $e->getMessage() . "\n";
$matchedIps = [];
}
// Step 2: Find players using those IPs
$matchedPlayers = [];
if (!empty($matchedIps)) {
$ipPlaceholders = implode(',', array_fill(0, count($matchedIps), '?'));
// From player_ip_changes
try {
$pStmt = $db->prepare("SELECT DISTINCT player_name FROM player_ip_changes WHERE new_ip IN ($ipPlaceholders)");
$idx = 1;
foreach ($matchedIps as $ip) { $pStmt->bindValue($idx++, $ip, SQLITE3_TEXT); }
$pResult = $pStmt->execute();
while ($pRow = $pResult->fetchArray(SQLITE3_ASSOC)) {
$matchedPlayers[] = $pRow['player_name'];
}
echo "Step 2a (player_ip_changes): Found " . count($matchedPlayers) . " players\n";
} catch (\Throwable $e) {
echo "Step 2a ERROR: " . $e->getMessage() . "\n";
}
// From web_session_log
try {
$wStmt = $db->prepare("SELECT DISTINCT player_name FROM web_session_log WHERE ip_address IN ($ipPlaceholders)");
$idx = 1;
foreach ($matchedIps as $ip) { $wStmt->bindValue($idx++, $ip, SQLITE3_TEXT); }
$wResult = $wStmt->execute();
$beforeCount = count($matchedPlayers);
while ($wRow = $wResult->fetchArray(SQLITE3_ASSOC)) {
if (!in_array($wRow['player_name'], $matchedPlayers)) {
$matchedPlayers[] = $wRow['player_name'];
}
}
echo "Step 2b (web_session_log): Found " . (count($matchedPlayers) - $beforeCount) . " new players\n";
} catch (\Throwable $e) {
echo "Step 2b ERROR: " . $e->getMessage() . "\n";
}
echo "Step 2 Total: " . count($matchedPlayers) . " players\n";
if (!empty($matchedPlayers)) {
echo " Players: " . implode(', ', array_slice($matchedPlayers, 0, 10)) . "\n";
}
}
// Step 3: Query users
if (!empty($matchedPlayers)) {
$namePlaceholders = implode(',', array_fill(0, count($matchedPlayers), '?'));
try {
$uStmt = $db->prepare("SELECT player_name, email FROM users WHERE player_name IN ($namePlaceholders) LIMIT 5");
$idx = 1;
foreach ($matchedPlayers as $pn) { $uStmt->bindValue($idx++, $pn, SQLITE3_TEXT); }
$uResult = $uStmt->execute();
$users = [];
while ($uRow = $uResult->fetchArray(SQLITE3_ASSOC)) {
$users[] = $uRow;
}
echo "Step 3: Found " . count($users) . " users\n";
foreach ($users as $u) {
echo " {$u['player_name']} ({$u['email']})\n";
}
} catch (\Throwable $e) {
echo "Step 3 ERROR: " . $e->getMessage() . "\n";
}
} else {
echo "Step 3: Skipped (no matching players)\n";
}
// 4. Also check what batchGetPlayerIps would return for all users
echo "\n--- batchGetPlayerIps Check ---\n";
try {
$allUsers = $db->query("SELECT player_name FROM users")->fetchArray(SQLITE3_ASSOC);
// Get a sample user
$sampleUser = 'youpaishidifu';
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE player_name = :name ORDER BY changed_at DESC LIMIT 1");
$stmt->bindValue(':name', $sampleUser, SQLITE3_TEXT);
$result = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if ($result) {
echo "$sampleUser IP from player_ip_changes: {$result['new_ip']}\n";
// Check if this IP has location
$locStmt = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip");
$locStmt->bindValue(':ip', $result['new_ip'], SQLITE3_TEXT);
$locResult = $locStmt->execute()->fetchArray(SQLITE3_ASSOC);
echo " Location: " . ($locResult ? $locResult['location'] : 'NOT FOUND') . "\n";
} else {
echo "$sampleUser NOT in player_ip_changes\n";
// Check web_session_log
$stmt2 = $db->prepare("SELECT ip_address FROM web_session_log WHERE player_name = :name ORDER BY login_time DESC LIMIT 1");
$stmt2->bindValue(':name', $sampleUser, SQLITE3_TEXT);
$result2 = $stmt2->execute()->fetchArray(SQLITE3_ASSOC);
if ($result2) {
echo "$sampleUser IP from web_session_log: {$result2['ip_address']}\n";
$locStmt2 = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip");
$locStmt2->bindValue(':ip', $result2['ip_address'], SQLITE3_TEXT);
$locResult2 = $locStmt2->execute()->fetchArray(SQLITE3_ASSOC);
echo " Location: " . ($locResult2 ? $locResult2['location'] : 'NOT FOUND') . "\n";
} else {
echo "$sampleUser NOT in web_session_log either\n";
}
}
} catch (\Throwable $e) {
echo "batchGetPlayerIps check ERROR: " . $e->getMessage() . "\n";
}
} catch (\Throwable $e) {
echo "\nFATAL ERROR: " . $e->getMessage() . "\n";
}
echo "\n=== Done ===\n";
?>
+315 -5
View File
@@ -222,6 +222,34 @@ if ($currentVersion !== $BUILD_VERSION) {
from { opacity: 0; transform: translateY(30px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ★ 毛玻璃Alert弹窗 */
.glass-alert-overlay {
display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.45);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
z-index: 5000; justify-content: center; align-items: center;
animation: glassFadeIn 0.25s ease;
}
.glass-alert-overlay.show { display: flex; }
.glass-alert-card {
background: rgba(22,27,34,0.92);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(88,166,255,0.25);
border-radius: 16px; padding: 28px 28px 20px; width: 360px; max-width: 88%;
box-shadow: 0 12px 48px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
animation: glassSlideUp 0.3s ease;
text-align: center;
}
.glass-alert-card .alert-icon { font-size: 36px; margin-bottom: 12px; }
.glass-alert-card .alert-msg { font-size: 14px; color: #e6edf3; line-height: 1.6; margin-bottom: 20px; word-break: break-word; }
.glass-alert-card .alert-btns { display: flex; gap: 10px; justify-content: center; }
.glass-alert-card .alert-btns button {
padding: 8px 24px; border: none; border-radius: 8px; font-size: 13px; font-weight: 600;
cursor: pointer; transition: all 0.2s;
}
.glass-alert-card .alert-btns .ag-ok { background: #58a6ff; color: #fff; }
.glass-alert-card .alert-btns .ag-cancel { background: rgba(255,255,255,0.08); color: #8b949e; }
.glass-alert-card .alert-btns button:hover { opacity: 0.85; transform: scale(1.03); }
@media (max-width: 768px) {
.sidebar { display: none; }
.grid { grid-template-columns: 1fr; }
@@ -240,8 +268,9 @@ if ($currentVersion !== $BUILD_VERSION) {
<div class="main">
<div class="sidebar">
<div class="sidebar-item active" data-page="dashboard" onclick="switchPage('dashboard')">📊 个人中心</div>
<div class="sidebar-item" data-page="dashboard" onclick="switchPage('dashboard')">📊 个人中心</div>
<div class="sidebar-item" data-page="shop" onclick="switchPage('shop')">🛒 商城</div>
<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="ticket" onclick="switchPage('ticket')">📋 工单系统</div>
@@ -257,6 +286,17 @@ if ($currentVersion !== $BUILD_VERSION) {
<button class="glow-btn" onclick="openGlassLogin()">🔑 立即登录</button>
</div>
<!-- ★ 毛玻璃Alert弹窗 -->
<div class="glass-alert-overlay" id="glassAlertOverlay">
<div class="glass-alert-card" onclick="event.stopPropagation()">
<div class="alert-icon" id="glassAlertIcon">⚠️</div>
<div class="alert-msg" id="glassAlertMsg"></div>
<div class="alert-btns" id="glassAlertBtns">
<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>
</div>
</div>
</div>
<!-- ★ 毛玻璃登录弹窗 -->
<div class="glass-login-overlay" id="glassLoginOverlay" onclick="closeGlassLogin(event)">
<div class="glass-card" onclick="event.stopPropagation()">
@@ -655,7 +695,7 @@ if ($currentVersion !== $BUILD_VERSION) {
if (data.data.need_password_change === 1) {
document.getElementById('playerInfo').textContent = '已登录: ' + currentPlayer + ' ⚠️ 请使用临时密码登录后立即修改密码';
setTimeout(() => {
alert('您正在使用临时密码登录。为了您的账号安全,请在登录后立即修改密码!');
glassAlert('您正在使用临时密码登录。为了您的账号安全,请在登录后立即修改密码!', '🔐');
}, 500);
} else {
document.getElementById('playerInfo').textContent = '已登录: ' + currentPlayer;
@@ -980,6 +1020,7 @@ if ($currentVersion !== $BUILD_VERSION) {
if (!c) return;
if (page === 'dashboard') renderDashboard(c);
else if (page === 'shop') renderShop(c);
else if (page === 'lands') renderLands(c);
else if (page === 'cdk') renderCDK(c);
else if (page === 'balance') renderBalance(c);
else if (page === 'account') renderAccount(c);
@@ -1366,6 +1407,31 @@ if ($currentVersion !== $BUILD_VERSION) {
setTimeout(() => t.remove(), 3000);
}
// ★ 毛玻璃Alert/Confirm弹窗(替代原生alert/confirm)
let _glassAlertResolve = null;
function glassAlertResolve(val) {
document.getElementById('glassAlertOverlay').classList.remove('show');
if (_glassAlertResolve) { _glassAlertResolve(val); _glassAlertResolve = null; }
}
function glassAlert(msg, icon = '⚠️') {
return new Promise(resolve => {
_glassAlertResolve = resolve;
document.getElementById('glassAlertIcon').textContent = icon;
document.getElementById('glassAlertMsg').textContent = msg;
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
document.getElementById('glassAlertOverlay').classList.add('show');
});
}
function glassConfirm(msg, icon = '❓') {
return new Promise(resolve => {
_glassAlertResolve = resolve;
document.getElementById('glassAlertIcon').textContent = icon;
document.getElementById('glassAlertMsg').textContent = msg;
document.getElementById('glassAlertBtns').innerHTML = '<button class="ag-cancel" onclick="glassAlertResolve(false)">取消</button><button class="ag-ok" onclick="glassAlertResolve(true)">确定</button>';
document.getElementById('glassAlertOverlay').classList.add('show');
});
}
// ===== 主题切换 =====
function toggleLightTheme() {
document.body.classList.toggle('light-theme');
@@ -1830,7 +1896,7 @@ if ($currentVersion !== $BUILD_VERSION) {
}
async function withdrawTicket(id) {
if (!confirm('确定要撤销此工单吗?')) return;
if (!await glassConfirm('确定要撤销此工单吗?')) return;
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'withdraw');
@@ -1842,10 +1908,10 @@ if ($currentVersion !== $BUILD_VERSION) {
ticketState.view = 'list';
renderTicket(document.getElementById('content'));
} else {
alert(data.message);
glassAlert(data.message);
}
} catch (e) {
alert('撤销失败: ' + e.message);
glassAlert('撤销失败: ' + e.message);
}
}
@@ -1994,6 +2060,250 @@ if ($currentVersion !== $BUILD_VERSION) {
} catch(e) { console.error('[MD_RENDER] Error:', e); return escHtml(text).replace(/\n/g, '<br>'); }
}
// ==================== 领地管理 ====================
let landsState = { view: 'list', currentLand: null };
async function renderLands(el) {
if (!TOKEN) {
el.innerHTML = `<div class="card" style="text-align:center;padding:40px">
<div style="font-size:48px;margin-bottom:16px">🏡</div>
<h2 style="margin:0 0 8px;color:var(--fg)">领地管理</h2>
<p style="color:var(--dim);margin-bottom:16px">请先登录以管理你的领地</p>
<button class="btn btn-blue" onclick="openGlassLogin()">🔑 登录</button>
</div>`;
return;
}
if (landsState.view === 'detail' && landsState.currentLand) {
await renderLandDetail(el, landsState.currentLand);
return;
}
// 加载我的领地列表
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载领地中...</p></div>`;
try {
const url = new URL(API + 'land_api.php', location.href);
url.searchParams.set('action', 'my_lands');
url.searchParams.set('token', TOKEN);
const res = await fetch(url);
const data = await res.json();
if (data.needLogin) {
el.innerHTML = `<div class="card" style="text-align:center;padding:40px">
<div style="font-size:48px;margin-bottom:16px">🏡</div>
<h2 style="margin:0 0 8px;color:var(--fg)">领地管理</h2>
<p style="color:var(--dim);margin-bottom:16px">请先登录以管理你的领地</p>
<button class="btn btn-blue" onclick="openGlassLogin()">🔑 登录</button>
</div>`;
return;
}
if (!data.success) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p></div>`;
return;
}
const lands = data.lands || [];
let html = `<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="margin:0;color:var(--fg)">🏡 我的领地 (${lands.length})</h2>
<span style="color:var(--dim);font-size:13px">玩家: ${escHtml(data.player)}</span>
</div>`;
if (lands.length === 0) {
html += `<div style="text-align:center;padding:32px;color:var(--dim)">
<div style="font-size:40px;margin-bottom:12px">🏗️</div>
<p>你还没有领地</p>
<p style="font-size:12px;margin-top:8px">在游戏中输入 <code style="background:rgba(88,166,255,0.1);padding:2px 6px;border-radius:4px;color:var(--accent)">/protect create <领地名></code> 来创建领地</p>
</div>`;
} else {
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">`;
for (const land of lands) {
const areaSize = land.area_size || 0;
const createdAt = land.created_at ? new Date(land.created_at * 1000).toLocaleDateString('zh-CN') : '未知';
const coordStr = `(${land.x1},${land.z1}) → (${land.x2},${land.z2})`;
html += `<div onclick="landsState.view='detail';landsState.currentLand='${escHtml(land.name)}';renderLands(document.getElementById('content'))"
style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all 0.2s"
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h3 style="margin:0;color:var(--fg);font-size:16px">📍 ${escHtml(land.name)}</h3>
<span style="color:var(--green);font-size:11px">✦ 所有者</span>
</div>
<div style="font-size:13px;color:var(--dim);margin-bottom:4px">🌍 ${escHtml(land.world || '未知')}</div>
<div style="font-size:13px;color:var(--dim);margin-bottom:4px">📐 ${coordStr}</div>
<div style="display:flex;justify-content:space-between;font-size:12px;color:var(--dim);margin-top:8px">
<span>📏 ${areaSize} 格²</span>
<span>📅 ${createdAt}</span>
</div>
</div>`;
}
html += `</div>`;
}
html += `</div>`;
// 领地商店
html += `<div class="card" style="margin-top:12px">
<h3 style="margin:0 0 12px;color:var(--fg)">🛍️ 领地权限商店</h3>`;
try {
const shopUrl = new URL(API + 'land_api.php', location.href);
shopUrl.searchParams.set('action', 'land_shop');
const shopRes = await fetch(shopUrl);
const shopData = await shopRes.json();
if (shopData.success && shopData.items && shopData.items.length > 0) {
html += `<table class="table"><thead><tr>
<th>领地</th><th>卖家</th><th>权限</th><th>价格(债券)</th><th>时长</th><th>操作</th>
</tr></thead><tbody>`;
for (const item of shopData.items) {
const dur = item.duration >= 86400 ? Math.floor(item.duration/86400)+'天' : Math.floor(item.duration/3600)+'小时';
html += `<tr>
<td>${escHtml(item.land_name)}</td>
<td>${escHtml(item.seller)}</td>
<td><span style="color:var(--accent)">${escHtml(item.permission)}</span></td>
<td style="color:var(--green)">${item.price}</td>
<td>${dur}</td>
<td><button class="btn btn-blue" style="font-size:11px;padding:3px 8px" onclick="buyLandPermission('${escHtml(item.land_name)}',${item.id})">购买</button></td>
</tr>`;
}
html += `</tbody></table>`;
} else {
html += `<p style="color:var(--dim);font-size:13px">暂无在售的领地权限</p>`;
}
} catch (e) {
html += `<p style="color:var(--dim);font-size:13px">商店加载失败</p>`;
}
html += `</div>`;
el.innerHTML = html;
} catch (e) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
}
}
async function renderLandDetail(el, landName) {
el.innerHTML = `<div class="card"><p style="color:var(--dim)">加载领地详情...</p></div>`;
try {
const url = new URL(API + 'land_api.php', location.href);
url.searchParams.set('action', 'land_detail');
url.searchParams.set('token', TOKEN);
url.searchParams.set('name', landName);
const res = await fetch(url);
const data = await res.json();
if (!data.success) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">${data.error}</p><button class="btn" onclick="landsState.view='list';landsState.currentLand=null;renderLands(document.getElementById('content'))">← 返回</button></div>`;
return;
}
const land = data.land;
const visitors = data.visitors || [];
const coordStr = `(${land.x1},${land.z1}) → (${land.x2},${land.z2})`;
let html = `<div class="card">
<button class="btn" onclick="landsState.view='list';landsState.currentLand=null;renderLands(document.getElementById('content'))" style="margin-bottom:12px;font-size:13px">← 返回我的领地</button>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="margin:0;color:var(--fg)">📍 ${escHtml(land.name)}</h2>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:16px">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">🌍 世界</div>
<div style="color:var(--fg);font-size:14px">${escHtml(land.world || '未知')}</div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📐 坐标</div>
<div style="color:var(--fg);font-size:14px">${coordStr}</div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📏 面积</div>
<div style="color:var(--fg);font-size:14px">${land.area_size || 0} 格²</div>
</div>
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px">
<div style="color:var(--dim);font-size:12px;margin-bottom:4px">📅 创建时间</div>
<div style="color:var(--fg);font-size:14px">${land.created_at ? new Date(land.created_at*1000).toLocaleDateString('zh-CN') : '未知'}</div>
</div>
</div>`;
// 访客管理
html += `<div style="margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h3 style="margin:0;color:var(--fg)">👥 访客管理 (${visitors.length})</h3>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px">
<input type="text" id="newVisitorName" placeholder="输入玩家名" style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:13px">
<button class="btn btn-blue" onclick="addLandVisitor('${escHtml(land.name)}')">添加访客</button>
</div>`;
if (visitors.length === 0) {
html += `<p style="color:var(--dim);font-size:13px">暂无访客</p>`;
} else {
html += `<table class="table"><thead><tr><th>玩家名</th><th>角色</th><th>授权时间</th><th>操作</th></tr></thead><tbody>`;
for (const v of visitors) {
const grantDate = v.granted_at ? new Date(v.granted_at * 1000).toLocaleString('zh-CN') : '未知';
html += `<tr>
<td><strong>${escHtml(v.player_name)}</strong></td>
<td><span style="color:var(--accent)">${escHtml(v.role)}</span></td>
<td style="font-size:12px;color:var(--dim)">${grantDate}</td>
<td><button class="btn" style="color:var(--red);font-size:11px;padding:3px 8px" onclick="removeLandVisitor('${escHtml(land.name)}','${escHtml(v.player_name)}')">移除</button></td>
</tr>`;
}
html += `</tbody></table>`;
}
html += `</div></div>`;
el.innerHTML = html;
} catch (e) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">加载失败: ${e.message}</p></div>`;
}
}
async function addLandVisitor(landName) {
const input = document.getElementById('newVisitorName');
if (!input) return;
const visitor = input.value.trim();
if (!visitor) return;
try {
const url = new URL(API + 'land_api.php', location.href);
url.searchParams.set('action', 'add_visitor');
url.searchParams.set('token', TOKEN);
url.searchParams.set('name', landName);
url.searchParams.set('visitor', visitor);
const res = await fetch(url);
const data = await res.json();
if (data.success) {
landsState.currentLand = landName;
await renderLandDetail(document.getElementById('content'), landName);
} else {
glassAlert(data.error || '操作失败');
}
} catch (e) {
glassAlert('操作失败: ' + e.message);
}
}
async function removeLandVisitor(landName, player) {
if (!await glassConfirm(`确定要移除访客 ${player} 吗?`)) return;
try {
const url = new URL(API + 'land_api.php', location.href);
url.searchParams.set('action', 'remove_visitor');
url.searchParams.set('token', TOKEN);
url.searchParams.set('name', landName);
url.searchParams.set('visitor', player);
const res = await fetch(url);
const data = await res.json();
if (data.success) {
landsState.currentLand = landName;
await renderLandDetail(document.getElementById('content'), landName);
} else {
glassAlert(data.error || '操作失败');
}
} catch (e) {
glassAlert('操作失败: ' + e.message);
}
}
function buyLandPermission(landName, itemId) {
glassAlert('请在游戏中使用 /protect shop 购买领地权限', '🎮');
}
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
</script>
-98
View File
@@ -1,98 +0,0 @@
<?php
/**
* 自更新脚本 - 通过HTTP POST上传文件内容
* 用法: curl -X POST -d "secret=KEY&action=update_file&target=api/admin.php" --data-binary @admin.php https://.../self_update.php
* 或: curl -X POST -F "secret=KEY" -F "action=update_file" -F "target=api/admin.php" -F "file=@admin.php" https://.../self_update.php
*/
header('Content-Type: text/plain; charset=utf-8');
@ob_start();
require_once __DIR__ . '/config.php';
$secret = $_POST['secret'] ?? $_GET['secret'] ?? '';
$action = $_POST['action'] ?? $_GET['action'] ?? '';
if ($secret !== SECRET_KEY) {
echo "ERROR: Invalid secret key\n";
exit;
}
$timestamp = date('Y-m-d H:i:s');
if ($action === 'update_file') {
$target = $_POST['target'] ?? '';
$allowedTargets = ['api/admin.php', 'api/sync.php', 'core.php', 'config.php'];
if (!in_array($target, $allowedTargets)) {
echo "ERROR: Target '$target' not in allowed list: " . implode(', ', $allowedTargets) . "\n";
exit;
}
$targetPath = __DIR__ . '/' . $target;
// Handle file upload
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$content = file_get_contents($_FILES['file']['tmp_name']);
} elseif (isset($_POST['content'])) {
$content = $_POST['content'];
} else {
// Read from raw POST body
$content = file_get_contents('php://input');
}
if (empty($content)) {
echo "ERROR: No file content received\n";
exit;
}
// Backup
$backupPath = $targetPath . '.bak';
if (file_exists($targetPath)) {
copy($targetPath, $backupPath);
}
// Write
$bytesWritten = file_put_contents($targetPath, $content);
if ($bytesWritten !== false) {
echo "SUCCESS: Updated $target ($bytesWritten bytes) at $timestamp\n";
echo "Backup: $backupPath\n";
// Verify
echo "Verify MD5: " . md5_file($targetPath) . "\n";
// Clear opcache
if (function_exists('opcache_reset')) {
@opcache_reset();
echo "OPCache: cleared\n";
}
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($targetPath, true);
echo "OPCache: invalidated $target\n";
}
} else {
echo "ERROR: Failed to write file\n";
}
} elseif ($action === 'verify') {
echo "=== File Verification ===\n";
$files = ['api/admin.php', 'api/sync.php', 'core.php'];
foreach ($files as $f) {
$path = __DIR__ . '/' . $f;
if (file_exists($path)) {
$size = filesize($path);
$md5 = md5_file($path);
$mtime = date('Y-m-d H:i:s', filemtime($path));
echo "$f: $size bytes, MD5: $md5, modified: $mtime\n";
} else {
echo "$f: NOT FOUND\n";
}
}
} else {
echo "Usage: ?action=update_file|verify&secret=KEY\n";
}
echo "\nDone at $timestamp\n";
@ob_end_flush();
?>
-102
View File
@@ -1,102 +0,0 @@
<?php
/**
* 测试脚本 - 模拟Java推送online_players数据
* 用于验证PHP端syncOnlinePlayers是否正常工作
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/core.php';
$db = getDB();
$action = $_GET['action'] ?? '';
if ($action === 'simulate_push') {
// 模拟Java推送一个已登录的玩家
$player = $_GET['player'] ?? 'yupaishidifu';
$secret = $_GET['secret'] ?? SECRET_KEY;
$players = [
['name' => $player, 'login_time' => time()]
];
$db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER NOT NULL)");
$db->exec("CREATE TABLE IF NOT EXISTS online_player_hb (id INTEGER PRIMARY KEY DEFAULT 1, last_seen INTEGER DEFAULT 0)");
$db->exec("BEGIN IMMEDIATE");
try {
$db->exec("DELETE FROM online_players");
$stmt = $db->prepare("INSERT OR REPLACE INTO online_players (player_name, login_time) VALUES (:name, :time)");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
$hbSql = $db->prepare("INSERT OR REPLACE INTO online_player_hb (id, last_seen) VALUES (1, :time)");
$hbSql->bindValue(':time', time(), SQLITE3_INTEGER);
$hbSql->execute();
$db->exec("COMMIT");
} catch (Exception $e) {
$db->exec("ROLLBACK");
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
// 验证
$stmt = $db->query("SELECT * FROM online_players");
$rows = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
}
echo json_encode([
'success' => true,
'message' => "模拟推送玩家 $player 成功",
'online_players' => $rows,
'timestamp' => time()
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'check_token') {
// 检查指定玩家的token是否存在
$player = $_GET['player'] ?? 'yupaishidifu';
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE player_name = :player ORDER BY created_at DESC LIMIT 1");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
echo json_encode([
'player' => $player,
'token_found' => $row !== false,
'token_data' => $row,
'timestamp' => time()
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'validate') {
// 模拟validateWebAccess调用
$player = $_GET['player'] ?? 'yupaishidifu';
$token = $_GET['token'] ?? '';
if (!$token) {
// 查找最新token
$stmt = $db->prepare("SELECT web_token FROM weblogin_tokens WHERE player_name = :player ORDER BY created_at DESC LIMIT 1");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$token = $row ? $row['web_token'] : '';
}
$result = validateWebAccess($token, 'view');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode([
'error' => '未知操作',
'usage' => [
'test_sync.php?action=simulate_push&player=yupaishidifu',
'test_sync.php?action=check_token&player=yupaishidifu',
'test_sync.php?action=validate&player=yupaishidifu'
]
], JSON_UNESCAPED_UNICODE);
-32
View File
@@ -1,32 +0,0 @@
<?php
// FTP上传脚本
$ftpServer = '43.153.203.46';
$ftpPort = 61868;
$ftpUser = 'qwed';
$ftpPass = 'z22K2Ws8r38a';
$localFile = __DIR__ . '/debug_region_search.php';
$remoteDir = '/caoyuan.ypshidifu.cn/test1/';
$remoteFile = $remoteDir . 'debug_region_search.php';
echo "Connecting to FTP...\n";
$conn = ftp_connect($ftpServer, $ftpPort);
if (!$conn) {
die("FTP connection failed\n");
}
echo "Logging in...\n";
if (!ftp_login($conn, $ftpUser, $ftpPass)) {
die("FTP login failed\n");
}
echo "Uploading $localFile to $remoteFile...\n";
if (ftp_put($conn, $remoteFile, $localFile, FTP_BINARY)) {
echo "Upload successful\n";
} else {
echo "Upload failed\n";
}
ftp_close($conn);
echo "Done\n";
?>
-30
View File
@@ -1,30 +0,0 @@
$ftpServer = "43.153.203.46"
$ftpPort = 61868
$ftpUser = "qwed"
$ftpPass = "z22K2Ws8r38a"
$localFile = "D:\桌面\源码\mc_plugn\Web\debug_region_search.php"
$remoteDir = "/caoyuan.ypshidifu.cn/test1/"
$remoteFile = $remoteDir + "debug_region_search.php"
Write-Host "Connecting to FTP..."
$ftpRequest = [System.Net.FtpWebRequest]::Create("ftp://$ftpServer`:$ftpPort$remoteFile")
$ftpRequest.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$ftpRequest.Credentials = New-Object System.Net.NetworkCredential($ftpUser, $ftpPass)
$ftpRequest.UseBinary = $true
$ftpRequest.UsePassive = $true
$fileContents = [System.IO.File]::ReadAllBytes($localFile)
$ftpRequest.ContentLength = $fileContents.Length
try {
$requestStream = $ftpRequest.GetRequestStream()
$requestStream.Write($fileContents, 0, $fileContents.Length)
$requestStream.Close()
$requestStream.Dispose()
Write-Host "Upload successful"
} catch {
Write-Host "Upload failed: $_"
}
Write-Host "Done"
-295
View File
@@ -1,295 +0,0 @@
<?php
/**
* HTTP文件上传器 - 用于在FTP不可用时通过HTTP上传文件
* 访问方式: upload_via_http.php?secret=YOUR_SECRET&action=upload
*
* 安全措施:
* 1. 需要SECRET_KEY认证
* 2. 只允许覆盖特定文件
* 3. 记录所有操作
*/
header('Content-Type: text/plain; charset=utf-8');
@ob_start();
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/core.php';
$secret = getParam('secret', '');
$action = getParam('action', '');
if ($secret !== SECRET_KEY) {
echo "ERROR: Invalid secret key\n";
exit;
}
$logFile = __DIR__ . '/upload_log.txt';
file_put_contents($logFile, date('Y-m-d H:i:s') . " | Action: $action | IP: " . $_SERVER['REMOTE_ADDR'] . "\n", FILE_APPEND);
if ($action === 'status') {
echo "=== Server Status ===\n";
echo "PHP Version: " . phpversion() . "\n";
echo "SQLite Version: " . SQLite3::version()['versionString'] . "\n";
echo "Time: " . date('Y-m-d H:i:s') . "\n";
echo "DB Path: " . (defined('DB_PATH') ? DB_PATH : 'undefined') . "\n";
if (defined('DB_PATH') && file_exists(DB_PATH)) {
$db = new SQLite3(DB_PATH);
$db->enableExceptions(false);
echo "\n=== Table Row Counts ===\n";
$tables = ['users', 'player_ip_changes', 'player_ip_locations', 'web_session_log', 'online_players', 'player_daily_logins', 'player_checkins'];
foreach ($tables as $t) {
try {
$cnt = $db->querySingle("SELECT COUNT(*) FROM $t");
echo "$t: $cnt rows\n";
} catch (\Throwable $e) {
echo "$t: ERROR - " . $e->getMessage() . "\n";
}
}
// 检查 player_ip_locations 中的格式
echo "\n=== IP Location Samples (first 10) ===\n";
try {
$r = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['ip_address'] . " => " . $row['location'] . " (player: " . $row['player_name'] . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 player_ip_changes 中的玩家IP
echo "\n=== Player IP Changes Samples (first 10) ===\n";
try {
$r = $db->query("SELECT player_name, new_ip, changed_at FROM player_ip_changes ORDER BY changed_at DESC LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['player_name'] . " => " . $row['new_ip'] . " (at: " . date('Y-m-d H:i:s', $row['changed_at']) . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 web_session_log 中的玩家IP
echo "\n=== Web Session Log Samples (first 10) ===\n";
try {
$r = $db->query("SELECT player_name, ip_address, login_time FROM web_session_log ORDER BY login_time DESC LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['player_name'] . " => " . $row['ip_address'] . " (at: " . date('Y-m-d H:i:s', $row['login_time']) . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 region 搜索模拟
echo "\n=== Region Search Simulation for '长沙' ===\n";
echo "Step 1: player_ip_locations with '长沙':\n";
try {
$r = $db->query("SELECT ip_address, location FROM player_ip_locations WHERE location LIKE '%长沙%'");
$cnt = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo " " . $row['ip_address'] . " => " . $row['location'] . "\n";
$cnt++;
}
echo " Found: $cnt IPs\n";
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
// 查找有长沙IP的玩家
echo "\nStep 2: Players with 长沙 IPs:\n";
try {
$r = $db->query("
SELECT DISTINCT c.player_name
FROM player_ip_changes c
INNER JOIN player_ip_locations l ON c.new_ip = l.ip_address
WHERE l.location LIKE '%长沙%'
UNION
SELECT DISTINCT w.player_name
FROM web_session_log w
INNER JOIN player_ip_locations l ON w.ip_address = l.ip_address
WHERE l.location LIKE '%长沙%'
");
$cnt = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo " " . $row['player_name'] . "\n";
$cnt++;
}
echo " Found: $cnt players\n";
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
$db->close();
} else {
echo "Database not found!\n";
}
echo "\n=== admin.php Version ===\n";
$adminFile = __DIR__ . '/api/admin.php';
if (file_exists($adminFile)) {
$content = file_get_contents($adminFile);
echo "File size: " . filesize($adminFile) . " bytes\n";
echo "Has _region_debug: " . (strpos($content, '_region_debug') !== false ? 'YES' : 'NO') . "\n";
echo "Has regionDebugEnabled: " . (strpos($content, 'regionDebugEnabled') !== false ? 'YES' : 'NO') . "\n";
echo "Has batchGetPlayerIps: " . (strpos($content, 'function batchGetPlayerIps') !== false ? 'YES' : 'NO') . "\n";
} else {
echo "admin.php not found!\n";
}
// ★ 模拟 admin 的精确 region search 逻辑
echo "\n=== Simulate Admin Region Search Logic ===\n";
$db = new SQLite3(DB_PATH);
$db->enableExceptions(false);
$regionKey = '长沙';
// Step 1: Get all player names
$allPlayerNames = [];
$r = $db->query("SELECT player_name FROM users");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$allPlayerNames[] = $row['player_name'];
}
echo "All players: " . count($allPlayerNames) . "\n";
// Step 2: Build playerIpMap from player_ip_changes
$playerIpMap = [];
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' AND new_ip NOT LIKE '192.168.%' AND new_ip != '127.0.0.1'");
$r = $stmt->execute();
$changesCount = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$changesCount++;
$pn = $row['player_name'];
$ip = trim($row['new_ip']);
if (!isset($playerIpMap[$pn])) $playerIpMap[$pn] = [];
if (!in_array($ip, $playerIpMap[$pn])) $playerIpMap[$pn][] = $ip;
}
echo "player_ip_changes rows: $changesCount\n";
echo "Players with IPs from changes: " . count($playerIpMap) . "\n";
// web_session_log is empty, skip
echo "web_session_log rows: 0 (skipped)\n";
// Step 3: Get IP locations
$ipLocMap = [];
$r = $db->query("SELECT ip_address, location FROM player_ip_locations");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$loc = trim($row['location'] ?? '');
$ip = trim($row['ip_address'] ?? '');
if ($loc && $ip) {
$ipLocMap[$ip] = $loc;
}
}
echo "IP locations cached: " . count($ipLocMap) . "\n";
// Show some sample IP from playerIpMap vs ipLocMap
echo "\n--- IP Comparison ---\n";
$samplePlayers = array_slice(array_keys($playerIpMap), 0, 5);
foreach ($samplePlayers as $pn) {
$ips = $playerIpMap[$pn];
echo "Player: $pn => IPs: " . implode(', ', $ips) . "\n";
foreach ($ips as $ip) {
if (isset($ipLocMap[$ip])) {
echo " FOUND in cache: $ip => " . $ipLocMap[$ip] . "\n";
} else {
echo " NOT in cache: $ip\n";
}
}
}
// Step 4: PHP matching
$matchedPlayers = [];
$matchDebug = [];
foreach ($playerIpMap as $pn => $ips) {
foreach ($ips as $ip) {
if (isset($ipLocMap[$ip])) {
$loc = $ipLocMap[$ip];
if (mb_stripos($loc, $regionKey) !== false) {
if (!in_array($pn, $matchedPlayers)) $matchedPlayers[] = $pn;
$matchDebug[] = "$ip=>$loc ($pn)";
break;
}
}
}
}
echo "\n=== PHP Match Result ===\n";
echo "Matched players: " . count($matchedPlayers) . "\n";
foreach ($matchedPlayers as $pn) {
echo " $pn\n";
}
// Check if any 长沙 IPs are NOT in the playerIpMap
echo "\n--- All 长沙 IPs in cache ---\n";
foreach ($ipLocMap as $ip => $loc) {
if (mb_stripos($loc, $regionKey) !== false) {
echo " $ip => $loc\n";
// Check which player has this IP
$foundPlayer = false;
foreach ($playerIpMap as $pn => $ips) {
if (in_array($ip, $ips)) {
echo " Has player: $pn\n";
$foundPlayer = true;
break;
}
}
if (!$foundPlayer) {
echo " NO player found with this IP!\n";
// Try partial match
foreach ($playerIpMap as $pn => $ips) {
foreach ($ips as $pip) {
if (strpos($ip, $pip) !== false || strpos($pip, $ip) !== false) {
echo " Partial match: $pip (player: $pn)\n";
}
}
}
}
}
}
$db->close();
} elseif ($action === 'upload' || (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK)) {
// 通过 POST 上传文件
// 支持 target= 和 path= 两种参数名
$target = $_POST['target'] ?? $_POST['path'] ?? '';
$allowed = ['api/admin.php', 'api/sync.php', 'api/cdk.php', 'api/ticket.php', 'api/register.php', 'api/shop.php', 'api/balance.php', 'core.php', 'config.php', 'player.php', 'admin.php', 'service.php', 'upload_via_http.php'];
if (!in_array($target, $allowed)) {
echo "ERROR: Target not allowed: $target\nAllowed: " . implode(', ', $allowed) . "\n";
exit;
}
$targetPath = __DIR__ . '/' . $target;
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$content = file_get_contents($_FILES['file']['tmp_name']);
} elseif (isset($_POST['content'])) {
$content = base64_decode($_POST['content']);
} else {
$content = file_get_contents('php://input');
}
if (empty($content)) {
echo "ERROR: No content\n";
exit;
}
if (file_exists($targetPath)) {
copy($targetPath, $targetPath . '.bak');
}
$bytes = file_put_contents($targetPath, $content);
echo "Wrote $bytes bytes to $target\n";
echo "MD5: " . md5_file($targetPath) . "\n";
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($targetPath, true);
}
if (function_exists('opcache_reset')) {
@opcache_reset();
}
echo "OPCache cleared\n";
}
echo "\nDone.\n";
@ob_end_flush();
?>
-8
View File
@@ -1,8 +0,0 @@
<?php
// 版本标记文件 - 用于验证上传是否成功
header('Content-Type: text/plain');
echo "admin.php version: 2026-06-19-1527\n";
echo "File size: " . filesize(__DIR__ . '/api/admin.php') . " bytes\n";
echo "MD5: " . md5_file(__DIR__ . '/api/admin.php') . "\n";
echo "Time: " . date('Y-m-d H:i:s') . "\n";
?>