修复工单上传图片误判+用户组max_effects配置同步
This commit is contained in:
@@ -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=普通玩家, <=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">领地增益效果上限(<=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
@@ -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
@@ -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
@@ -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'] : '未知错误'));
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user