diff --git a/api/credit.php b/api/credit.php new file mode 100644 index 0000000..e46be5d --- /dev/null +++ b/api/credit.php @@ -0,0 +1,240 @@ + 0) ob_end_clean(); +ob_start(); + +$coreFile = __DIR__ . '/../core.php'; +if (!file_exists($coreFile)) { + while (ob_get_level() > 0) ob_end_clean(); + header('Content-Type: application/json; charset=utf-8'); + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'core.php not found'], JSON_UNESCAPED_UNICODE); + exit; +} +set_error_handler(function($errno, $errstr, $errfile, $errline) { + @error_log("[credit.php PRE-LOAD] $errstr in $errfile on line $errline"); + return true; +}); +require_once $coreFile; + +$action = $_GET['action'] ?? ''; +$db = getDB(); + +// ====== 初始化表结构 ====== +function ensureCreditTables($db) { + $db->exec("CREATE TABLE IF NOT EXISTS credit_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target_player TEXT NOT NULL, + reporter_player TEXT NOT NULL, + type TEXT NOT NULL, + content TEXT DEFAULT '', + status TEXT DEFAULT 'pending', + score_change INTEGER DEFAULT 0, + created_at INTEGER DEFAULT 0, + approved_at INTEGER DEFAULT 0, + approve_reason TEXT DEFAULT '', + source TEXT DEFAULT 'web' + )"); + $db->exec("CREATE TABLE IF NOT EXISTS credit_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rule_name TEXT NOT NULL UNIQUE, + min_score INTEGER DEFAULT 0, + max_score INTEGER DEFAULT 0, + restriction_type TEXT DEFAULT '', + restriction_duration INTEGER DEFAULT 0, + enabled INTEGER DEFAULT 1, + description TEXT DEFAULT '' + )"); + // users 表加 credit_score 列(如果不存在) + $ci = $db->query("PRAGMA table_info(users)"); + $cols = []; + while ($c = $ci->fetchArray(SQLITE3_ASSOC)) $cols[] = $c['name']; + if (!in_array('credit_score', $cols)) { + $db->exec("ALTER TABLE users ADD COLUMN credit_score INTEGER DEFAULT 100"); + } + // 默认规则 + $db->exec("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) VALUES ('禁止交易', 60, 0, 'trade', 0, '低于60分时禁止与NPC交易')"); + $db->exec("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) VALUES ('禁止PVP', 40, 0, 'pvp', 0, '低于40分时禁止进入PVP竞技场')"); + $db->exec("INSERT OR IGNORE INTO credit_rules (rule_name, min_score, max_score, restriction_type, restriction_duration, description) VALUES ('禁止传送', 20, 0, 'teleport', 0, '低于20分时禁止使用传送功能')"); +} + +ensureCreditTables($db); + +/** 玩家端 token 校验,返回玩家名 */ +function creditRequirePlayer() { + $token = getParam('token', ''); + if (empty($token)) { + error('未登录', 401); + } + $info = validateTokenSilent($token); + if (!$info || empty($info['player'])) { + error('Token 无效或已过期', 401); + } + return $info['player']; +} + +/** 管理端 session 校验 */ +function creditRequireAdmin() { + if (empty($_SESSION['admin_logged_in']) || !$_SESSION['admin_logged_in']) { + error('需要管理员权限', 401); + } +} + +switch ($action) { + // ====== 用户端:上报好人好事 ====== + case 'submit_good': + $player = creditRequirePlayer(); + $target = getParam('target', ''); + $content = getParam('content', ''); + if (!$target) error('缺少目标玩家名'); + $stmt = $db->prepare("INSERT INTO credit_records (target_player, reporter_player, type, content, created_at, source) VALUES (?, ?, 'good', ?, ?, 'web')"); + $stmt->bindValue(1, $target, SQLITE3_TEXT); + $stmt->bindValue(2, $player, SQLITE3_TEXT); + $stmt->bindValue(3, $content, SQLITE3_TEXT); + $stmt->bindValue(4, time(), SQLITE3_INTEGER); + $stmt->execute(); + success(['id' => $db->lastInsertRowID()], '好人好事上报成功,等待管理员审批'); + break; + + // ====== 用户端:举报违规 ====== + case 'submit_bad': + $player = creditRequirePlayer(); + $target = getParam('target', ''); + $content = getParam('content', ''); + if (!$target) error('缺少目标玩家名'); + $stmt = $db->prepare("INSERT INTO credit_records (target_player, reporter_player, type, content, created_at, source) VALUES (?, ?, 'bad', ?, ?, 'web')"); + $stmt->bindValue(1, $target, SQLITE3_TEXT); + $stmt->bindValue(2, $player, SQLITE3_TEXT); + $stmt->bindValue(3, $content, SQLITE3_TEXT); + $stmt->bindValue(4, time(), SQLITE3_INTEGER); + $stmt->execute(); + success(['id' => $db->lastInsertRowID()], '举报已提交,等待管理员审批'); + break; + + // ====== 用户端:查询自己的积分 ====== + case 'my_score': + $player = creditRequirePlayer(); + $stmt = $db->prepare("SELECT credit_score FROM users WHERE player_name = ?"); + $stmt->bindValue(1, $player, SQLITE3_TEXT); + $res = $stmt->execute(); + $score = 100; + if ($row = $res->fetchArray(SQLITE3_ASSOC)) $score = (int)$row['credit_score']; + success(['player' => $player, 'credit_score' => $score]); + break; + + // ====== 用户端:查询他人积分 ====== + case 'user_score': + $player = creditRequirePlayer(); + $target = getParam('target', ''); + if (!$target) error('缺少目标玩家名'); + $stmt = $db->prepare("SELECT credit_score FROM users WHERE player_name = ?"); + $stmt->bindValue(1, $target, SQLITE3_TEXT); + $res = $stmt->execute(); + $score = 100; + if ($row = $res->fetchArray(SQLITE3_ASSOC)) $score = (int)$row['credit_score']; + success(['player' => $target, 'credit_score' => $score]); + break; + + // ====== 用户端:查看自己的积分记录 ====== + case 'my_records': + $player = creditRequirePlayer(); + $stmt = $db->prepare("SELECT * FROM credit_records WHERE target_player = ? ORDER BY created_at DESC LIMIT 50"); + $stmt->bindValue(1, $player, SQLITE3_TEXT); + $res = $stmt->execute(); + $records = []; + while ($row = $res->fetchArray(SQLITE3_ASSOC)) $records[] = $row; + success(['records' => $records]); + break; + + // ====== 管理员:查看待审批列表 ====== + case 'pending_list': + creditRequireAdmin(); + $stmt = $db->prepare("SELECT * FROM credit_records WHERE status = 'pending' ORDER BY created_at DESC LIMIT 100"); + $res = $stmt->execute(); + $records = []; + while ($row = $res->fetchArray(SQLITE3_ASSOC)) $records[] = $row; + success(['records' => $records]); + break; + + // ====== 管理员:审批通过 ====== + case 'approve': + creditRequireAdmin(); + $recordId = (int)getParam('id', 0); + $amount = (int)getParam('amount', 10); + $reason = getParam('reason', '信誉积分审批通过'); + if ($recordId <= 0) error('缺少记录ID'); + // 查目标玩家 + $stmt = $db->prepare("SELECT target_player FROM credit_records WHERE id = ?"); + $stmt->bindValue(1, $recordId, SQLITE3_INTEGER); + $res = $stmt->execute(); + $target = ''; + if ($row = $res->fetchArray(SQLITE3_ASSOC)) $target = $row['target_player']; + if (!$target) error('记录不存在'); + // 更新记录 + $stmt = $db->prepare("UPDATE credit_records SET status = 'approved', score_change = ?, approved_at = ?, approve_reason = ? WHERE id = ?"); + $stmt->bindValue(1, $amount, SQLITE3_INTEGER); + $stmt->bindValue(2, time(), SQLITE3_INTEGER); + $stmt->bindValue(3, $reason, SQLITE3_TEXT); + $stmt->bindValue(4, $recordId, SQLITE3_INTEGER); + $stmt->execute(); + // 更新积分 + $stmt = $db->prepare("UPDATE users SET credit_score = credit_score + ? WHERE player_name = ?"); + $stmt->bindValue(1, $amount, SQLITE3_INTEGER); + $stmt->bindValue(2, $target, SQLITE3_TEXT); + $stmt->execute(); + success(['target' => $target, 'amount' => $amount], '审批通过,积分已更新'); + break; + + // ====== 管理员:审批驳回 ====== + case 'reject': + creditRequireAdmin(); + $recordId = (int)getParam('id', 0); + $reason = getParam('reason', '管理员驳回'); + if ($recordId <= 0) error('缺少记录ID'); + $stmt = $db->prepare("UPDATE credit_records SET status = 'rejected', approved_at = ?, approve_reason = ? WHERE id = ?"); + $stmt->bindValue(1, time(), SQLITE3_INTEGER); + $stmt->bindValue(2, $reason, SQLITE3_TEXT); + $stmt->bindValue(3, $recordId, SQLITE3_INTEGER); + $stmt->execute(); + success(null, '已驳回,不扣分不加分'); + break; + + // ====== 管理员:查看审批记录 ====== + case 'approved_list': + creditRequireAdmin(); + $stmt = $db->prepare("SELECT * FROM credit_records ORDER BY created_at DESC LIMIT 100"); + $res = $stmt->execute(); + $records = []; + while ($row = $res->fetchArray(SQLITE3_ASSOC)) $records[] = $row; + success(['records' => $records]); + break; + + // ====== 管理员:查看所有规则 ====== + case 'rules': + creditRequireAdmin(); + $stmt = $db->prepare("SELECT * FROM credit_rules ORDER BY min_score ASC"); + $res = $stmt->execute(); + $rules = []; + while ($row = $res->fetchArray(SQLITE3_ASSOC)) $rules[] = $row; + success(['rules' => $rules]); + break; + + // ====== 管理员:更新规则 ====== + case 'update_rule': + creditRequireAdmin(); + $ruleId = (int)getParam('id', 0); + $enabled = (int)getParam('enabled', 1); + if ($ruleId <= 0) error('缺少规则ID'); + $stmt = $db->prepare("UPDATE credit_rules SET enabled = ? WHERE id = ?"); + $stmt->bindValue(1, $enabled, SQLITE3_INTEGER); + $stmt->bindValue(2, $ruleId, SQLITE3_INTEGER); + $stmt->execute(); + success(null, '规则已更新'); + break; + + default: + error('未知操作: ' . $action); +} diff --git a/api/sync.php b/api/sync.php index 15633df..581907f 100644 --- a/api/sync.php +++ b/api/sync.php @@ -1848,7 +1848,7 @@ function syncLogin() { if ($registerTime > 1000000000000) { $registerTime = intval($registerTime / 1000); } if ($lastLoginTime > 1000000000000) { $lastLoginTime = intval($lastLoginTime / 1000); } - $stmt = $db->prepare("INSERT OR REPLACE INTO users (player_name, register_time, last_login_time, email, points, gift_stage, total_online_time) VALUES (:name, :rt, :lt, :email, :pts, :gs, :tot)"); + $stmt = $db->prepare("INSERT OR REPLACE INTO users (player_name, register_time, last_login_time, email, points, gift_stage, total_online_time, credit_score) VALUES (:name, :rt, :lt, :email, :pts, :gs, :tot, :cs)"); $stmt->bindValue(':name', $name, SQLITE3_TEXT); $stmt->bindValue(':rt', $registerTime, SQLITE3_INTEGER); $stmt->bindValue(':lt', $lastLoginTime, SQLITE3_INTEGER); @@ -1856,6 +1856,7 @@ function syncLogin() { $stmt->bindValue(':pts', (int)($user['points'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':gs', (int)($user['gift_stage'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':tot', (int)($user['total_online_time'] ?? 0), SQLITE3_INTEGER); + $stmt->bindValue(':cs', (int)($user['credit_score'] ?? 100), SQLITE3_INTEGER); $stmt->execute(); $count++; } diff --git a/core.php b/core.php index 532d0f4..4aa3644 100644 --- a/core.php +++ b/core.php @@ -1920,6 +1920,7 @@ function initTables($db, $side = null) { 'last_login_ip' => "TEXT DEFAULT ''", 'frozen' => "INTEGER DEFAULT 0", 'freeze_token' => "TEXT DEFAULT ''", + 'credit_score' => "INTEGER DEFAULT 100", ]; foreach ($addCols as $col => $type) { if (!in_array($col, $userCols)) { diff --git a/player.php b/player.php index 538c812..3803f90 100644 --- a/player.php +++ b/player.php @@ -380,6 +380,7 @@ if ($currentVersion !== $BUILD_VERSION) { +
@@ -1269,6 +1270,7 @@ if ($currentVersion !== $BUILD_VERSION) { else if (page === 'account') renderAccount(c); else if (page === 'ticket') renderTicket(c); else if (page === 'sn') renderSn(c); + else if (page === 'credit') renderCredit(c); else if (page === 'recharge') renderRecharge(c); else if (page === 'orders') renderMyOrders(c); } @@ -5463,6 +5465,93 @@ async function clearAllMemberPerms(landName, targetPlayer) { p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); } + // ===== 信誉积分 ===== + function creditTypeBadge(t) { + const m = { good: ['好人好事', 'var(--green)'], bad: ['举报', 'var(--red)'] }; + const v = m[t] || [t, 'var(--dim)']; + return '' + v[0] + ''; + } + function creditStatusBadge(s) { + const m = { + pending: ['待审批', 'var(--yellow)'], + approved: ['已通过', 'var(--green)'], + rejected: ['已驳回', 'var(--red)'] + }; + const v = m[s] || [s, 'var(--dim)']; + return '' + v[0] + ''; + } + + async function renderCredit(el) { + el.innerHTML = '

⭐ 信誉积分

加载中...

'; + try { + const [scoreRes, recRes] = await Promise.all([ + api('credit.php', { action: 'my_score' }), + api('credit.php', { action: 'my_records' }) + ]); + const score = (scoreRes.success && scoreRes.data) ? scoreRes.data.credit_score : 100; + const records = (recRes.success && recRes.data) ? (recRes.data.records || []) : []; + + let html = '
' + + '
' + + '

⭐ 我的信誉积分

' + + '
' + score + '
' + + '
'; + + // 上报按钮区 + html += '
' + + '' + + '' + + '
'; + + // 积分规则 + html += '
' + + '初始积分 100 · 低于 60 禁止交易 · 低于 40 禁止PVP · 低于 20 禁止传送' + + '
'; + + // 记录列表 + html += '

📋 积分变动记录 (' + records.length + ')

'; + if (!records.length) { + html += '

暂无记录

'; + } else { + html += '
' + + '' + + '' + + ''; + for (const r of records) { + html += '' + + '' + + '' + + '' + + '' + + '' + + ''; + } + html += '
类型上报人状态变动内容来源
' + creditTypeBadge(r.type) + '' + escHtml(r.reporter_player) + '' + creditStatusBadge(r.status) + '' + + (r.score_change > 0 ? '+' : '') + r.score_change + '' + escHtml(r.content || '') + '' + escHtml(r.source || '') + '
'; + } + html += '
'; + el.innerHTML = html; + } catch (e) { + el.innerHTML = '

加载失败: ' + escHtml(e.message) + '

'; + } + } + + async function creditSubmitFlow(type) { + const target = await glassPrompt('请输入目标玩家名'); + if (!target) return; + const content = await glassPrompt('请输入上报理由'); + if (!content) return; + try { + const action = type === 'good' ? 'submit_good' : 'submit_bad'; + const r = await api('credit.php', { action: action, target: target, content: content }); + if (!r.success) { toast(r.message || '上报失败', 'error'); return; } + toast(r.message || '上报成功'); + renderCredit(document.getElementById('content')); + } catch (e) { + toast('上报失败: ' + e.message, 'error'); + } + } + function escHtml(s) { return (s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ===== 第十六批:封禁常态化公示卡片(独立卡片,每15秒一个窗口期,从下往上滚动替换) =====