修复工单上传图片误判+用户组max_effects配置同步

This commit is contained in:
ypsdf1
2026-10-04 14:48:43 +08:00
parent fe1ed613ba
commit 419b677aa9
6 changed files with 158 additions and 18 deletions
+14 -2
View File
@@ -3105,6 +3105,12 @@ function showAddUserGroup() {
<input id="ugMaxLands" type="number" value="-1" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"> <input id="ugMaxLands" type="number" value="-1" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
<div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">领地增益效果上限(5=普通玩家, &lt;=0=不限)</label>
<input id="ugMaxEffects" type="number" value="5" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
<div> <div>
<label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=跟随默认)</label> <label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=跟随默认)</label>
@@ -3143,6 +3149,7 @@ async function doAddUserGroup() {
const priority = document.getElementById('ugPriority')?.value || '0'; const priority = document.getElementById('ugPriority')?.value || '0';
const price = document.getElementById('ugPrice')?.value || '-1'; const price = document.getElementById('ugPrice')?.value || '-1';
const maxLands = document.getElementById('ugMaxLands')?.value || '-1'; const maxLands = document.getElementById('ugMaxLands')?.value || '-1';
const maxEffects = document.getElementById('ugMaxEffects')?.value || '5';
const homeLimit = document.getElementById('ugHomeLimit')?.value || '0'; const homeLimit = document.getElementById('ugHomeLimit')?.value || '0';
const joinPrice = document.getElementById('ugJoinPrice')?.value || '0'; const joinPrice = document.getElementById('ugJoinPrice')?.value || '0';
const renewPrice = document.getElementById('ugRenewPrice')?.value || '0'; const renewPrice = document.getElementById('ugRenewPrice')?.value || '0';
@@ -3157,7 +3164,7 @@ async function doAddUserGroup() {
try { try {
const res = await apiCall('update_user_group', { const res = await apiCall('update_user_group', {
name, display_name: displayName, display_color: color, name, display_name: displayName, display_color: color,
priority, land_price_per_sqm: price, max_lands: maxLands, priority, land_price_per_sqm: price, max_lands: maxLands, max_effects: maxEffects,
home_limit: homeLimit, join_price: joinPrice, renew_price: renewPrice, home_limit: homeLimit, join_price: joinPrice, renew_price: renewPrice,
duration_minutes: duration, auto_renew: autoRenew, duration_minutes: duration, auto_renew: autoRenew,
default_perms: '{}' default_perms: '{}'
@@ -3203,6 +3210,10 @@ async function showEditUserGroup(groupName) {
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">最大领地数</label> <div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">最大领地数</label>
<input id="eugMaxLands" type="number" value="${g.max_lands}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div> <input id="eugMaxLands" type="number" value="${g.max_lands}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
</div>`; </div>`;
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">领地增益效果上限(&lt;=0=不限)</label>
<input id="eugMaxEffects" type="number" value="${g.max_effects ?? 5}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
</div>`;
html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px"> html += `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
<div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=默认)</label> <div><label style="display:block;font-size:12px;color:var(--dim);margin-bottom:4px">Home上限(0=默认)</label>
<input id="eugHomeLimit" type="number" value="${g.home_limit || 0}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div> <input id="eugHomeLimit" type="number" value="${g.home_limit || 0}" style="width:100%;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg)"></div>
@@ -3247,6 +3258,7 @@ async function doEditUserGroup(groupName) {
const priority = document.getElementById('eugPriority')?.value || '0'; const priority = document.getElementById('eugPriority')?.value || '0';
const price = document.getElementById('eugPrice')?.value || '-1'; const price = document.getElementById('eugPrice')?.value || '-1';
const maxLands = document.getElementById('eugMaxLands')?.value || '-1'; const maxLands = document.getElementById('eugMaxLands')?.value || '-1';
const maxEffects = document.getElementById('eugMaxEffects')?.value || '5';
const homeLimit = document.getElementById('eugHomeLimit')?.value || '0'; const homeLimit = document.getElementById('eugHomeLimit')?.value || '0';
const duration = document.getElementById('eugDuration')?.value || '0'; const duration = document.getElementById('eugDuration')?.value || '0';
const joinPrice = document.getElementById('eugJoinPrice')?.value || '0'; const joinPrice = document.getElementById('eugJoinPrice')?.value || '0';
@@ -3262,7 +3274,7 @@ async function doEditUserGroup(groupName) {
try { try {
const res = await apiCall('update_user_group', { const res = await apiCall('update_user_group', {
name: groupName, display_name: displayName, display_color: color, name: groupName, display_name: displayName, display_color: color,
priority, land_price_per_sqm: price, max_lands: maxLands, priority, land_price_per_sqm: price, max_lands: maxLands, max_effects: maxEffects,
home_limit: homeLimit, duration_minutes: duration, home_limit: homeLimit, duration_minutes: duration,
join_price: joinPrice, renew_price: renewPrice, auto_renew: autoRenew, join_price: joinPrice, renew_price: renewPrice, auto_renew: autoRenew,
default_perms: JSON.stringify(perms) default_perms: JSON.stringify(perms)
+70 -2
View File
@@ -1662,6 +1662,55 @@ function handleValidationCallback($db, $data) {
// ==================== 玩家端:更新领地字段(效果管理)==================== // ==================== 玩家端:更新领地字段(效果管理)====================
/**
* ★ 领地增益效果数量上限(2026-10-04 任务3)
* 取领地所有者所属最高优先级用户组的 max_effects,默认 5;<=0 表示不限。
*/
function getLandMaxEffects($db, $land) {
$fallback = 5;
$owner = $land['owner'] ?? '';
if (empty($owner)) return $fallback;
try {
$db->exec("CREATE TABLE IF NOT EXISTS web_user_groups (
group_name TEXT PRIMARY KEY,
display_name TEXT DEFAULT '',
display_color TEXT DEFAULT '§f',
display_emoji TEXT DEFAULT '',
priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0,
renew_price INTEGER DEFAULT 0,
duration_minutes INTEGER DEFAULT 0,
default_perms TEXT DEFAULT '{}',
synced_at INTEGER DEFAULT 0
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_user_group_members (
player_name TEXT NOT NULL,
group_name TEXT NOT NULL,
added_by TEXT DEFAULT 'system',
added_time INTEGER DEFAULT 0,
expiry_time INTEGER DEFAULT 0,
PRIMARY KEY(player_name, group_name)
)");
$st = $db->prepare("SELECT g.max_effects FROM web_user_group_members m
LEFT JOIN web_user_groups g ON m.group_name = g.group_name
WHERE m.player_name = :p AND (m.expiry_time = 0 OR m.expiry_time > :now)
ORDER BY g.priority DESC LIMIT 1");
$st->bindValue(':p', $owner, SQLITE3_TEXT);
$st->bindValue(':now', time(), SQLITE3_INTEGER);
$row = $st->execute()->fetchArray(SQLITE3_ASSOC);
if ($row && $row['max_effects'] !== null) {
$v = (int)$row['max_effects'];
return $v <= 0 ? PHP_INT_MAX : $v;
}
} catch (\Throwable $e) { /* 表缺失等 */ }
return $fallback;
}
function handleUpdateLandField($db, $playerName, $post) { function handleUpdateLandField($db, $playerName, $post) {
$name = $post['name'] ?? ''; $name = $post['name'] ?? '';
$field = $post['field'] ?? ''; $field = $post['field'] ?? '';
@@ -1723,10 +1772,19 @@ function handleUpdateLandField($db, $playerName, $post) {
$arr = json_decode($value, true); $arr = json_decode($value, true);
if (is_array($arr)) { if (is_array($arr)) {
if ($field === 'give_effects') { if ($field === 'give_effects') {
// ★ 2026-10-04 任务3:数量上限(按领地所有者用户组 max_effects,默认5)+ 等级封顶1~255
$maxEff = getLandMaxEffects($db, $land);
if (count($arr) > $maxEff) {
$arr = array_slice($arr, 0, $maxEff);
}
// [["夜视","1","99999"]] → "夜视:1:99999" // [["夜视","1","99999"]] → "夜视:1:99999"
$parts = []; $parts = [];
foreach ($arr as $e) { foreach ($arr as $e) {
if (is_array($e)) { if (is_array($e)) {
if (isset($e[1]) && $e[1] !== '') {
$lv = (int)$e[1];
$e[1] = (string)max(1, min(255, $lv));
}
$parts[] = implode(':', $e); $parts[] = implode(':', $e);
} else { } else {
$parts[] = (string)$e; $parts[] = (string)$e;
@@ -2312,6 +2370,7 @@ function handleListUserGroups($db) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -2332,6 +2391,7 @@ function handleListUserGroups($db) {
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0', 'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0', 'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0', 'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
'max_effects' => 'ALTER TABLE web_user_groups ADD COLUMN max_effects INTEGER DEFAULT 5',
]; ];
foreach ($migrations as $col => $sql) { foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) { if (!in_array($col, $columns)) {
@@ -2369,6 +2429,7 @@ function handleUpdateUserGroup($db, $data) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -2384,6 +2445,7 @@ function handleUpdateUserGroup($db, $data) {
$priority = (int)($data['priority'] ?? 0); $priority = (int)($data['priority'] ?? 0);
$pricePerSqm = (int)($data['land_price_per_sqm'] ?? -1); $pricePerSqm = (int)($data['land_price_per_sqm'] ?? -1);
$maxLands = (int)($data['max_lands'] ?? -1); $maxLands = (int)($data['max_lands'] ?? -1);
$maxEffects = (int)($data['max_effects'] ?? 5); // 领地增益效果上限,<=0=不限
$homeLimit = (int)($data['home_limit'] ?? 0); $homeLimit = (int)($data['home_limit'] ?? 0);
$joinPrice = (int)($data['join_price'] ?? 0); $joinPrice = (int)($data['join_price'] ?? 0);
$renewPrice = (int)($data['renew_price'] ?? 0); $renewPrice = (int)($data['renew_price'] ?? 0);
@@ -2393,9 +2455,9 @@ function handleUpdateUserGroup($db, $data) {
$stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups $stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups
(group_name, display_name, display_color, display_emoji, priority, (group_name, display_name, display_color, display_emoji, priority,
land_price_per_sqm, max_lands, home_limit, join_price, auto_renew, renew_price, duration_minutes, default_perms, synced_at) land_price_per_sqm, max_lands, max_effects, home_limit, join_price, auto_renew, renew_price, duration_minutes, default_perms, synced_at)
VALUES (:name, :display, :color, :emoji, :priority, VALUES (:name, :display, :color, :emoji, :priority,
:price, :maxlands, :homeLimit, :joinPrice, :autoRenew, :renewPrice, :duration, :perms, :synced)"); :price, :maxlands, :maxEffects, :homeLimit, :joinPrice, :autoRenew, :renewPrice, :duration, :perms, :synced)");
$stmt->bindValue(':name', $name, SQLITE3_TEXT); $stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->bindValue(':display', $displayName, SQLITE3_TEXT); $stmt->bindValue(':display', $displayName, SQLITE3_TEXT);
$stmt->bindValue(':color', $displayColor, SQLITE3_TEXT); $stmt->bindValue(':color', $displayColor, SQLITE3_TEXT);
@@ -2403,6 +2465,7 @@ function handleUpdateUserGroup($db, $data) {
$stmt->bindValue(':priority', $priority, SQLITE3_INTEGER); $stmt->bindValue(':priority', $priority, SQLITE3_INTEGER);
$stmt->bindValue(':price', $pricePerSqm, SQLITE3_INTEGER); $stmt->bindValue(':price', $pricePerSqm, SQLITE3_INTEGER);
$stmt->bindValue(':maxlands', $maxLands, SQLITE3_INTEGER); $stmt->bindValue(':maxlands', $maxLands, SQLITE3_INTEGER);
$stmt->bindValue(':maxEffects', $maxEffects, SQLITE3_INTEGER);
$stmt->bindValue(':homeLimit', $homeLimit, SQLITE3_INTEGER); $stmt->bindValue(':homeLimit', $homeLimit, SQLITE3_INTEGER);
$stmt->bindValue(':joinPrice', $joinPrice, SQLITE3_INTEGER); $stmt->bindValue(':joinPrice', $joinPrice, SQLITE3_INTEGER);
$stmt->bindValue(':autoRenew', $autoRenew, SQLITE3_INTEGER); $stmt->bindValue(':autoRenew', $autoRenew, SQLITE3_INTEGER);
@@ -2590,6 +2653,7 @@ function handleGetPlayerGroups($db, $player) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -2608,6 +2672,7 @@ function handleGetPlayerGroups($db, $player) {
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0', 'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0', 'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0', 'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
'max_effects' => 'ALTER TABLE web_user_groups ADD COLUMN max_effects INTEGER DEFAULT 5',
]; ];
foreach ($migrations as $col => $sql) { foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) { if (!in_array($col, $columns)) {
@@ -2631,6 +2696,7 @@ function handleGetPlayerGroups($db, $player) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -2666,6 +2732,7 @@ function handleListAvailableGroups($db) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -2682,6 +2749,7 @@ function handleListAvailableGroups($db) {
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0', 'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0', 'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0', 'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
'max_effects' => 'ALTER TABLE web_user_groups ADD COLUMN max_effects INTEGER DEFAULT 5',
]; ];
foreach ($migrations as $col => $sql) { foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) { if (!in_array($col, $columns)) {
+5 -2
View File
@@ -913,6 +913,7 @@ function syncUserGroups() {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
@@ -934,6 +935,7 @@ function syncUserGroups() {
'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0', 'auto_renew' => 'ALTER TABLE web_user_groups ADD COLUMN auto_renew INTEGER DEFAULT 0',
'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0', 'renew_price' => 'ALTER TABLE web_user_groups ADD COLUMN renew_price INTEGER DEFAULT 0',
'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0', 'duration_minutes' => 'ALTER TABLE web_user_groups ADD COLUMN duration_minutes INTEGER DEFAULT 0',
'max_effects' => 'ALTER TABLE web_user_groups ADD COLUMN max_effects INTEGER DEFAULT 5',
]; ];
foreach ($migrations as $col => $sql) { foreach ($migrations as $col => $sql) {
if (!in_array($col, $columns)) { if (!in_array($col, $columns)) {
@@ -944,10 +946,10 @@ function syncUserGroups() {
$now = time(); $now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups $stmt = $db->prepare("INSERT OR REPLACE INTO web_user_groups
(group_name, display_name, display_color, display_emoji, priority, (group_name, display_name, display_color, display_emoji, priority,
land_price_per_sqm, max_lands, home_limit, join_price, auto_renew, land_price_per_sqm, max_lands, max_effects, home_limit, join_price, auto_renew,
renew_price, duration_minutes, default_perms, synced_at) renew_price, duration_minutes, default_perms, synced_at)
VALUES (:name, :display, :color, :emoji, :priority, VALUES (:name, :display, :color, :emoji, :priority,
:price, :maxlands, :homelimit, :joinprice, :autorenew, :price, :maxlands, :maxeffects, :homelimit, :joinprice, :autorenew,
:renewprice, :duration, :perms, :synced)"); :renewprice, :duration, :perms, :synced)");
$count = 0; $count = 0;
foreach ($groups as $g) { foreach ($groups as $g) {
@@ -958,6 +960,7 @@ function syncUserGroups() {
$stmt->bindValue(':priority', (int)($g['priority'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':priority', (int)($g['priority'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':price', (int)($g['land_price_per_sqm'] ?? -1), SQLITE3_INTEGER); $stmt->bindValue(':price', (int)($g['land_price_per_sqm'] ?? -1), SQLITE3_INTEGER);
$stmt->bindValue(':maxlands', (int)($g['max_lands'] ?? -1), SQLITE3_INTEGER); $stmt->bindValue(':maxlands', (int)($g['max_lands'] ?? -1), SQLITE3_INTEGER);
$stmt->bindValue(':maxeffects', (int)($g['max_effects'] ?? 5), SQLITE3_INTEGER);
$stmt->bindValue(':homelimit', (int)($g['home_limit'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':homelimit', (int)($g['home_limit'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':joinprice', (int)($g['join_price'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':joinprice', (int)($g['join_price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':autorenew', (int)($g['auto_renew'] ?? 0), SQLITE3_INTEGER); $stmt->bindValue(':autorenew', (int)($g['auto_renew'] ?? 0), SQLITE3_INTEGER);
+59 -9
View File
@@ -189,8 +189,50 @@ function decodeTicketImages($raw) {
/** /**
* 上传工单附件图片:转存自家兰空图床,返回可直接引用的 URL。 * 上传工单附件图片:转存自家兰空图床,返回可直接引用的 URL。
* 认证:管理后台 session 或用户 token(getAuthUser)。 * 认证:管理后台 session 或用户 token(getAuthUser)。
* 限制:仅 png/jpg、单张 ≤2048K、内容必须与后缀一致;同会话每分钟最多 12 次(防刷)。 * 限制:仅 png/jpg(★ 2026-10-04 起按图片真实内容判定,不看文件名后缀)、单张 ≤2048K;同会话每分钟最多 12 次(防刷)。
*/ */
/**
* ★ 清洗上传文件名(2026-10-04 任务4)
* 全角句点→半角、零宽字符剔除、去老浏览器带的完整路径、去首尾空白。
*/
function sanitizeImageFileName($name) {
// 全角/非法字符 -> 半角合法字符(Windows 会拒收含全角字符的文件名)
$name = str_replace(array(
"\xe3\x80\x8e", // 全角句点
"\xef\xbd\xa1", // 半角浊点
"\xe3\x80\x8f", // 全角斜线
"\xe2\x80\x8b", // 零宽空格
"\xe2\x80\x8c", // 零宽非连接符
"\xe2\x80\x8d", // 零宽连接符
"\xef\xbb\xbf", // BOM
"\xef\xbc\x8c", // 全角 小于号
"\xef\xbc\x9e", // 全角 大于号
"\xef\xbc\x9a", // 全角 冒号
"\xef\xbc\x9f", // 全角 问号
"\xef\xbc\x8a", // 全角 星号
"\xef\xbc\x87", // 全角 双引号
"\xef\xbc\x9c", // 全角 竖线
"\xef\xbc\xa0", // 全角空格
), array(
'.', '.', '/', '', '', '', '',
'_', '_', '_', '_', '_', '_', '_', ' ',
), $name);
// 老浏览器会带完整路径 C:\fakepath\a.png
$name = str_replace('\\', '/', $name);
$pos = strrpos($name, '/');
if ($pos !== false) $name = substr($name, $pos + 1);
return trim($name);
}
/** 后缀归一:jpeg / jpe / jfif -> jpg,其余转小写去点 */
function normalizeImageExt($ext) {
$ext = strtolower(trim((string)$ext));
$ext = str_replace('.', '', $ext);
if (in_array($ext, array('jpeg', 'jpe', 'jfif', 'jif'), true)) return 'jpg';
return $ext;
}
function ticketUploadImage() { function ticketUploadImage() {
getAuthUser(); getAuthUser();
@@ -218,19 +260,27 @@ function ticketUploadImage() {
$m = isset($msgs[$f['error']]) ? $msgs[$f['error']] : ('上传错误码 ' . $f['error']); $m = isset($msgs[$f['error']]) ? $msgs[$f['error']] : ('上传错误码 ' . $f['error']);
error($m); error($m);
} }
// 后缀:仅 png / jpg // 文件名清洗:全角句点→半角、去零宽字符、去首尾空白
$orig = basename((string)$f['name']); $orig = sanitizeImageFileName(basename((string)$f['name']));
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if ($ext !== 'png' && $ext !== 'jpg') error('仅支持 png / jpg 格式');
// 大小:≤2048K // 大小:≤2048K
if ((int)$f['size'] > 2048 * 1024) error('图片不能超过 2048K'); if ((int)$f['size'] > 2048 * 1024) error('图片不能超过 2048K');
if (!is_uploaded_file($f['tmp_name'])) error('非法上传'); if (!is_uploaded_file($f['tmp_name'])) error('非法上传');
// 内容真实校验:防伪造后缀上传任意文件 // ★ 2026-10-04 修复:判定「是什么图」只看图片真实内容,不看文件名后缀。
// 旧逻辑用 pathinfo 后缀判类型,导致 .jpeg / 全角句点 / 尾部空格 /
// 无后缀的真实 png、jpg 全被误判成「不是 png/jpg 图片」。
// 这里 getimagesize 认真实格式,防伪造后缀的安全目的依然成立。
$info = @getimagesize($f['tmp_name']); $info = @getimagesize($f['tmp_name']);
if ($info === false) error('无法识别的图片内容'); if ($info === false) error('无法识别的图片内容(请确认文件确实是 png 或 jpg)');
$itype = (int)$info[2]; $itype = (int)$info[2];
if ($ext === 'png' && $itype !== IMAGETYPE_PNG) error('文件内容与 .png 后缀不符'); if ($itype === IMAGETYPE_PNG) $realExt = 'png';
if ($ext === 'jpg' && $itype !== IMAGETYPE_JPEG) error('文件内容与 .jpg 后缀不符'); elseif ($itype === IMAGETYPE_JPEG) $realExt = 'jpg';
else error('仅支持 png / jpg 格式');
// 后缀归一:jpeg/jpe/jfif 一律当 jpg
$ext = normalizeImageExt(pathinfo($orig, PATHINFO_EXTENSION));
// 用真实后缀重命名,去掉 .jpeg / 空格 / 全角点等乱七八糟的原名
$base = pathinfo($orig, PATHINFO_FILENAME);
if ($base === '') $base = 'ticket_image';
$orig = mb_substr($base, 0, 60) . '.' . $realExt;
$r = lskyUploadFile($f['tmp_name'], $orig); $r = lskyUploadFile($f['tmp_name'], $orig);
if (empty($r['ok'])) error('图片上传失败: ' . (isset($r['msg']) && $r['msg'] !== '' ? $r['msg'] : '未知错误')); if (empty($r['ok'])) error('图片上传失败: ' . (isset($r['msg']) && $r['msg'] !== '' ? $r['msg'] : '未知错误'));
+1
View File
@@ -644,6 +644,7 @@ function initTables(SQLite3 $db) {
priority INTEGER DEFAULT 0, priority INTEGER DEFAULT 0,
land_price_per_sqm INTEGER DEFAULT -1, land_price_per_sqm INTEGER DEFAULT -1,
max_lands INTEGER DEFAULT -1, max_lands INTEGER DEFAULT -1,
max_effects INTEGER DEFAULT 5,
home_limit INTEGER DEFAULT 0, home_limit INTEGER DEFAULT 0,
join_price INTEGER DEFAULT 0, join_price INTEGER DEFAULT 0,
auto_renew INTEGER DEFAULT 0, auto_renew INTEGER DEFAULT 0,
+9 -3
View File
@@ -2843,7 +2843,7 @@ if ($currentVersion !== $BUILD_VERSION) {
<div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">详细描述</label> <div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">详细描述</label>
<textarea id="ticketDesc" rows="6" placeholder="详细描述您的问题,支持Markdown格式" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px;resize:vertical;font-family:monospace"></textarea></div> <textarea id="ticketDesc" rows="6" placeholder="详细描述您的问题,支持Markdown格式" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:14px;resize:vertical;font-family:monospace"></textarea></div>
<div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">附件图片(最多 3 张)</label> <div style="margin-bottom:12px"><label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">附件图片(最多 3 张)</label>
<input type="file" id="ticketImg" accept=".png,.jpg,image/png,image/jpeg" multiple onchange="uploadTicketImage(this)" style="font-size:13px"> <input type="file" id="ticketImg" accept=".png,.jpg,.jpeg,image/png,image/jpeg" multiple onchange="uploadTicketImage(this)" style="font-size:13px">
<p style="color:var(--dim);font-size:12px;margin-top:4px">仅支持 png / jpg,单张 ≤ 2048K;选中即上传,并在上方描述光标处插入 [图片N] 占位符</p> <p style="color:var(--dim);font-size:12px;margin-top:4px">仅支持 png / jpg,单张 ≤ 2048K;选中即上传,并在上方描述光标处插入 [图片N] 占位符</p>
<div id="ticketImgList" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px"></div> <div id="ticketImgList" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px"></div>
<p id="ticketImgMsg" style="color:var(--red);font-size:12px;margin-top:4px"></p></div> <p id="ticketImgMsg" style="color:var(--red);font-size:12px;margin-top:4px"></p></div>
@@ -2866,8 +2866,14 @@ if ($currentVersion !== $BUILD_VERSION) {
input.value = ''; input.value = '';
for (const file of files) { for (const file of files) {
if ((ticketState.images || []).length >= 3) { setImgMsg(msg, '最多只能上传 3 张图片', true); break; } if ((ticketState.images || []).length >= 3) { setImgMsg(msg, '最多只能上传 3 张图片', true); break; }
const ext = (file.name.split('.').pop() || '').toLowerCase(); // ★ 2026-10-04 修复:不再用文件名后缀判类型
if (ext !== 'png' && ext !== 'jpg') { setImgMsg(msg, '仅支持 png / jpg:' + file.name, true); continue; } // (.jpeg / 全角句点 / 无后缀的真实 png、jpg 曾被误拦)
// 只有明显不是图片类型(.gif/.webp/.heic/文档等)才提前拦下,
// 其余一律上传,由服务端按图片真实内容判定。
const ext = (file.name.split('.').pop() || '').toLowerCase().trim();
const okExt = ['', 'png', 'jpg', 'jpeg', 'jpe', 'jfif'];
if (!okExt.includes(ext)) { setImgMsg(msg, '仅支持 png / jpg(.jpeg 也算):' + file.name, true); continue; }
if (file.type && !file.type.startsWith('image/')) { setImgMsg(msg, '这不是图片文件:' + file.name, true); continue; }
if (file.size > 2048 * 1024) { setImgMsg(msg, '超过 2048K:' + file.name, true); continue; } if (file.size > 2048 * 1024) { setImgMsg(msg, '超过 2048K:' + file.name, true); continue; }
setImgMsg(msg, '上传中… ' + file.name, false); setImgMsg(msg, '上传中… ' + file.name, false);
try { try {