第十六批:玩家端封禁公示卡片+AGPL合规页脚与开源地址.txt+封禁synced_at保序
This commit is contained in:
+10
-1
@@ -1032,6 +1032,14 @@ function handleSyncBans($db, $post) {
|
||||
return;
|
||||
}
|
||||
$now = time();
|
||||
// ★ 第十六批:保留旧 synced_at 作为「首见时间」,供封禁公示按时间从近到远排序
|
||||
$firstSeen = array();
|
||||
$oldRes = $db->query("SELECT target, ban_type, synced_at FROM web_player_bans");
|
||||
if ($oldRes) {
|
||||
while ($orow = $oldRes->fetchArray(SQLITE3_ASSOC)) {
|
||||
$firstSeen[$orow['target'] . "\x1f" . $orow['ban_type']] = (int) ($orow['synced_at'] ?? 0);
|
||||
}
|
||||
}
|
||||
// 先清空旧数据再全量写入
|
||||
$db->exec("DELETE FROM web_player_bans");
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO web_player_bans (target, ban_type, reason, source, expire_time, synced_at) VALUES (:target, :type, :reason, :source, :expire, :now)");
|
||||
@@ -1042,7 +1050,8 @@ function handleSyncBans($db, $post) {
|
||||
$stmt->bindValue(':reason', $ban['reason'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':source', $ban['source'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':expire', (int)($ban['expire'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
|
||||
$fk = $ban['target'] . "\x1f" . ($ban['type'] ?? 'name');
|
||||
$stmt->bindValue(':now', (isset($firstSeen[$fk]) && $firstSeen[$fk] > 0) ? $firstSeen[$fk] : $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
}
|
||||
echo json_encode(['success' => true, 'count' => count($bans)]);
|
||||
|
||||
+53
-1
@@ -366,6 +366,10 @@ switch ($action) {
|
||||
case 'sync_bans':
|
||||
syncBans();
|
||||
break;
|
||||
// ★ 第十六批:公开封禁公示(玩家端首页卡片数据源,无需SECRET,限流保护)
|
||||
case 'get_ban_board':
|
||||
getBanBoard();
|
||||
break;
|
||||
default:
|
||||
error('未知操作: ' . $action);
|
||||
}
|
||||
@@ -5394,6 +5398,44 @@ function syncAdmins() {
|
||||
success(['count' => count($admins)]);
|
||||
}
|
||||
|
||||
// ===== 第十六批:公开封禁公示(玩家端首页卡片,从近到远取前9条) =====
|
||||
// 无需 SECRET / token:web_player_bans 是已公开的公示名单(不含敏感字段),
|
||||
// 返回 target/ban_type/reason/expire_time。用 synced_at 作为近似封禁时间排序。
|
||||
function getBanBoard() {
|
||||
$limit = (int) getParam('limit', 9);
|
||||
if ($limit <= 0 || $limit > 50) $limit = 9;
|
||||
|
||||
try { $db = getDB(); } catch (Throwable $e) { $db = null; }
|
||||
if (!$db) { success(['total' => 0, 'bans' => []], 'db unavailable'); return; }
|
||||
|
||||
// 表不存在 → 空结果
|
||||
$chk = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='web_player_bans'");
|
||||
if (!$chk || !$chk->fetchArray()) { success(['total' => 0, 'bans' => []], 'empty'); return; }
|
||||
|
||||
// 仅公示名字封禁(ban_type='name'),IP 封禁不公开(IP 属敏感信息)
|
||||
$now = time();
|
||||
$res = $db->query("SELECT target, ban_type, reason, expire_time, synced_at
|
||||
FROM web_player_bans
|
||||
WHERE ban_type = 'name'
|
||||
ORDER BY synced_at DESC
|
||||
LIMIT " . $limit);
|
||||
$bans = [];
|
||||
if ($res) {
|
||||
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
|
||||
$expire = (int) ($row['expire_time'] ?? 0);
|
||||
// 跳过已过期的临时封禁(expire_time>0 且已过期)
|
||||
if ($expire > 0 && $expire <= $now) continue;
|
||||
$bans[] = [
|
||||
'target' => (string) ($row['target'] ?? ''),
|
||||
'reason' => (string) ($row['reason'] ?? ''),
|
||||
'expire_time'=> $expire, // 0 = 永久
|
||||
'synced_at' => (int) ($row['synced_at'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
success(['total' => count($bans), 'bans' => $bans], 'ok');
|
||||
}
|
||||
|
||||
// ===== Java端推送封禁名单到PHP =====
|
||||
function syncBans() {
|
||||
$secret = getParam('secret');
|
||||
@@ -5415,6 +5457,15 @@ function syncBans() {
|
||||
)");
|
||||
|
||||
$now = time();
|
||||
// ★ 第十六批:保留旧 synced_at 作为「首见时间」,供封禁公示按时间从近到远排序
|
||||
// (新封禁用本次同步时间,老封禁保持首次出现时间,避免全量重写抹平时间轴)
|
||||
$firstSeen = array();
|
||||
$oldRes = $db->query("SELECT target, ban_type, synced_at FROM web_player_bans");
|
||||
if ($oldRes) {
|
||||
while ($orow = $oldRes->fetchArray(SQLITE3_ASSOC)) {
|
||||
$firstSeen[$orow['target'] . "\x1f" . $orow['ban_type']] = (int) ($orow['synced_at'] ?? 0);
|
||||
}
|
||||
}
|
||||
// 先清空旧数据再全量写入
|
||||
$db->exec("DELETE FROM web_player_bans");
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO web_player_bans (target, ban_type, reason, source, expire_time, synced_at) VALUES (:target, :type, :reason, :source, :expire, :now)");
|
||||
@@ -5425,7 +5476,8 @@ function syncBans() {
|
||||
$stmt->bindValue(':reason', $ban['reason'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':source', $ban['source'] ?? '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':expire', (int)($ban['expire'] ?? 0), SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
|
||||
$fk = $ban['target'] . "\x1f" . ($ban['type'] ?? 'name');
|
||||
$stmt->bindValue(':now', (isset($firstSeen[$fk]) && $firstSeen[$fk] > 0) ? $firstSeen[$fk] : $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
|
||||
+131
@@ -8,6 +8,18 @@ header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
header('X-Accel-Expires: 0'); // Nginx反代也禁缓存
|
||||
|
||||
// ★ AGPL-3.0 合规:源码地址(使用者可在 开源地址.txt 中改成自己的仓库地址)
|
||||
$OSS_REPO_DEFAULT = 'https://git.ypshidifu.cn/youpaishidifu/Sdf1_login/src/branch/php/';
|
||||
$OSS_REPO = $OSS_REPO_DEFAULT;
|
||||
$ossTxt = __DIR__ . '/开源地址.txt';
|
||||
if (is_file($ossTxt)) {
|
||||
foreach (file($ossTxt, FILE_IGNORE_NEW_LINES) as $ossLine) {
|
||||
$ossLine = trim($ossLine);
|
||||
if ($ossLine === '' || $ossLine[0] === '#') continue;
|
||||
if (preg_match('#^https?://#i', $ossLine)) { $OSS_REPO = $ossLine; break; }
|
||||
}
|
||||
}
|
||||
|
||||
$BUILD_VERSION = 'v' . filemtime(__FILE__); // 文件修改时间 = 版本号
|
||||
$currentVersion = isset($_GET['_v']) ? $_GET['_v'] : '';
|
||||
if ($currentVersion !== $BUILD_VERSION) {
|
||||
@@ -259,6 +271,25 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
.sidebar { display: none; }
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ===== 第十六批:封禁常态化公示卡片(从下往上滚动替换) ===== */
|
||||
.ban-roll-wrap {
|
||||
position: relative; height: 320px; overflow: hidden;
|
||||
border-radius: 8px; margin-top: 4px;
|
||||
}
|
||||
.ban-roll-inner {
|
||||
position: absolute; left: 0; right: 0; top: 0;
|
||||
transition: transform 0.7s cubic-bezier(.22,.61,.36,1);
|
||||
}
|
||||
.ban-roll-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 12px; border-bottom: 1px solid var(--border);
|
||||
font-size: 13px; line-height: 1.3;
|
||||
}
|
||||
.ban-roll-item .bname { color: var(--red); font-weight: 700; flex-shrink: 0; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ban-roll-item .breason { color: var(--dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ban-roll-item .btime { color: var(--yellow); font-size: 11px; flex-shrink: 0; }
|
||||
.ban-roll-item .bperm { color: var(--red); font-size: 11px; flex-shrink: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -4464,6 +4495,106 @@ async function clearAllMemberPerms(landName, targetPlayer) {
|
||||
|
||||
function escHtml(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
// ===== 第十六批:封禁常态化公示卡片(独立卡片,每15秒一个窗口期,从下往上滚动替换) =====
|
||||
const BAN_CARD_ID = 'banBoardCard';
|
||||
const BAN_SLOT_ID = 'banBoardSlot';
|
||||
const BAN_PAGE_SIZE = 9; // 卡片内展示9名被封禁玩家
|
||||
const BAN_WINDOW_MS = 15000; // 每15秒一个窗口期
|
||||
let banBoardRows = []; // 当前生效封禁(从近到远)
|
||||
let banBoardShown = false; // 是否已渲染过(控制动画方向)
|
||||
let banBoardTimer = null;
|
||||
|
||||
function ensureBanCard() {
|
||||
if (!document.getElementById(BAN_CARD_ID)) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.id = BAN_CARD_ID;
|
||||
card.style.borderColor = 'var(--red)';
|
||||
card.innerHTML = '<h2 style="color:var(--red)">⚠ 封禁常态化公示</h2>' +
|
||||
'<p style="color:var(--dim);font-size:12px;margin-bottom:8px">违规玩家将被公示,杀鸡儆猴,请各位引以为戒</p>' +
|
||||
'<div class="ban-roll-wrap"><div class="ban-roll-inner" id="' + BAN_SLOT_ID + '"></div></div>';
|
||||
const main = document.querySelector('.main');
|
||||
if (main && main.parentNode) main.parentNode.insertBefore(card, main);
|
||||
}
|
||||
return document.getElementById(BAN_SLOT_ID);
|
||||
}
|
||||
|
||||
function banItemHtml(b) {
|
||||
const name = escHtml(b.target || '');
|
||||
const reason = escHtml(b.reason || '违反服务器规则');
|
||||
const expire = parseInt(b.expire_time, 10) || 0;
|
||||
const perm = expire === 0 ? '<span class="bperm">永久</span>' : '';
|
||||
let when = '';
|
||||
if (expire > 0) {
|
||||
const d = new Date(expire * 1000);
|
||||
when = '<span class="btime">至 ' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + '</span>';
|
||||
}
|
||||
return '<div class="ban-roll-item"><span class="bname">☠ ' + name + '</span>' +
|
||||
'<span class="breason">' + reason + '</span>' + perm + when + '</div>';
|
||||
}
|
||||
|
||||
function banPageHtml(rows) {
|
||||
if (!rows.length) return '<div class="ban-roll-item"><span class="breason" style="color:var(--green)">✓ 当前暂无被封禁玩家</span></div>';
|
||||
return rows.map(banItemHtml).join('');
|
||||
}
|
||||
|
||||
// 从下往上滚动替换信息:先把新页放到下方(translateY(100%)),再滚到 0 位
|
||||
function banRollUpdate(slot, html) {
|
||||
const inner = slot;
|
||||
if (!banBoardShown) {
|
||||
// 首次:直接显示
|
||||
inner.innerHTML = html;
|
||||
inner.style.transition = 'none';
|
||||
inner.style.transform = 'translateY(0)';
|
||||
banBoardShown = true;
|
||||
return;
|
||||
}
|
||||
// 旧页先滚出上方(-100%),同时把新页放在下方再滚入
|
||||
const oldH = inner.offsetHeight;
|
||||
inner.style.transition = 'transform 0.7s cubic-bezier(.22,.61,.36,1)';
|
||||
inner.style.transform = 'translateY(-' + oldH + 'px)';
|
||||
// 滚出结束后换成新页并从下方滚入
|
||||
setTimeout(() => {
|
||||
inner.style.transition = 'none';
|
||||
inner.style.transform = 'translateY(100%)';
|
||||
inner.innerHTML = html;
|
||||
// 强制 reflow 后再滚回 0(新页从下往上出现)
|
||||
void inner.offsetHeight;
|
||||
inner.style.transition = 'transform 0.7s cubic-bezier(.22,.61,.36,1)';
|
||||
inner.style.transform = 'translateY(0)';
|
||||
}, 720);
|
||||
}
|
||||
|
||||
async function banBoardLoadAndRoll() {
|
||||
const slot = ensureBanCard();
|
||||
if (!slot) return;
|
||||
try {
|
||||
const res = await fetch(API + 'sync.php?action=get_ban_board&limit=' + BAN_PAGE_SIZE, {cache: 'no-store'});
|
||||
const j = await res.json();
|
||||
if (j && j.success && j.data && Array.isArray(j.data.bans)) {
|
||||
banBoardRows = j.data.bans;
|
||||
}
|
||||
} catch (e) { /* 网络抖动沿用上一帧 */ }
|
||||
banRollUpdate(slot, banPageHtml(banBoardRows.slice(0, BAN_PAGE_SIZE)));
|
||||
}
|
||||
|
||||
function banBoardStart() {
|
||||
if (banBoardTimer) return;
|
||||
banBoardLoadAndRoll();
|
||||
banBoardTimer = setInterval(banBoardLoadAndRoll, BAN_WINDOW_MS);
|
||||
}
|
||||
// DOM 就绪后启动(脚本在 body 末尾,此时 .main 已存在;保险起见仍判断 readyState)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', banBoardStart);
|
||||
} else {
|
||||
banBoardStart();
|
||||
}
|
||||
|
||||
</script>
|
||||
<!-- ★ AGPL-3.0 合规页脚:源码地址取自 开源地址.txt(可由使用者替换为自己的仓库) -->
|
||||
<footer style="text-align:center;padding:14px 16px;font-size:12px;color:var(--dim);border-top:1px solid var(--border)">
|
||||
本后台基于 <a href="LICENSE" target="_blank" style="color:var(--accent);text-decoration:none">AGPL-3.0</a> 开源协议发布
|
||||
· 源代码地址:<a href="<?php echo htmlspecialchars($OSS_REPO, ENT_QUOTES, 'UTF-8'); ?>" target="_blank" style="color:var(--accent);text-decoration:none"><?php echo htmlspecialchars($OSS_REPO, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
- 衍生作品必须同样以 AGPL-3.0 授权分发。
|
||||
- 本项目按「原样」提供,不附带任何担保。
|
||||
|
||||
> **部署者须知**:请把根目录的 [`开源地址.txt`](开源地址.txt) 里的地址改成**你自己**的后端源码仓库地址(第一行非 `#` 注释的 http/https 地址即生效)。玩家端页脚会自动读取并展示该地址,用于满足 AGPL 对网络服务提供源码获取途径的要求;出厂默认指向 `https://git.ypshidifu.cn/youpaishidifu/Sdf1_login/src/branch/php/`。
|
||||
|
||||
> 本目录是 Minecraft PaperMC 插件 **`Sdf1_login`**(即本仓库的 `../Sdf1_login`)配套的 PHP Web 后端。
|
||||
> 两个工程是**一对**:Java 插件负责游戏内逻辑(登录注册、债券经济、商城、领地、工单、PVP…),
|
||||
> 本目录负责 Web 侧的界面与数据(玩家面板、管理后台、收银台、在线充值、正版登录)。
|
||||
|
||||
Reference in New Issue
Block a user