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 += '' +
+ '| ' + creditTypeBadge(r.type) + ' | ' +
+ '' + escHtml(r.reporter_player) + ' | ' +
+ '' + creditStatusBadge(r.status) + ' | ' +
+ '' +
+ (r.score_change > 0 ? '+' : '') + r.score_change + ' | ' +
+ '' + escHtml(r.content || '') + ' | ' +
+ '' + escHtml(r.source || '') + ' |
';
+ }
+ html += '
';
+ }
+ 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秒一个窗口期,从下往上滚动替换) =====