RPU v2, promo/bank система, админка, оптимизации

— RPU v2: бюджет удачи, потолок (ceiling), комбек после
  проигрышей, hot/cold анализ, интеграция с promo-фазами
— Bank API: mock-банк для карты (депозит/вывод с балансом 1000₽)
— Promo-сервис: автогенерация промокодов каждый час, типы
  (card_to_site, luck_boost, ceiling_boost, free_case, reset_streak)
— Promo-фазы: LUCKY/NEUTRAL/UNLUCKY, переключение каждые 1-6ч
— Admin: история транзакций (user_history), управление промокодами,
  управление promo-фазами, API фильтров/коллекций, RPU info
— Database: WAL mode, новые поля RPU v2, TransactionLog,
  карточный баланс, статистики (streaks, ceiling, budget)
— Frontend: /deposit, /promo/activate, /withdraw, /card-balance,
  /online-count, /rpu-info, lifespan вместо startup event
— UI: user_history.html, улучшения профиля, кейсов, навигации
— Cases: обновление cases.json (1892 строки новых данных)
— Achievements: кэш коллекций, оптимизация is_custom проверок
— Sounds: дополнительные звуковые эффекты
— Upgrade: интеграция с RPU (множители, streaks)
— .gitignore: исключены .db, .bak, __pycache__
This commit is contained in:
2026-07-25 13:11:08 +00:00
parent 6638ab99f3
commit 47c7f17c90
30 changed files with 5862 additions and 1295 deletions
+162 -10
View File
@@ -140,6 +140,32 @@
transform: none;
}
.withdraw-selected-btn {
padding: 0.45rem 1.25rem;
background: var(--success-color);
color: #000;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 0.78rem;
transition: all 0.2s;
font-family: 'Russo One', sans-serif;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.withdraw-selected-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.withdraw-selected-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.sell-all-btn {
padding: 0.45rem 1.25rem;
background: transparent;
@@ -234,6 +260,25 @@
color: white;
}
.btn-withdraw-single {
flex: 1;
padding: 0.3rem;
background: transparent;
border: 1px solid var(--success-color);
color: var(--success-color);
border-radius: 6px;
cursor: pointer;
font-size: 0.7rem;
transition: all 0.2s;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
}
.btn-withdraw-single:hover {
background: var(--success-color);
color: #000;
}
.filter-bar {
display: flex;
gap: 0.5rem;
@@ -628,6 +673,7 @@
</div>
<div style="display:flex;gap:0.75rem;">
<button class="sell-selected-btn" id="sellSelectedBtn" onclick="sellSelected()" disabled>💰 Продать</button>
<button class="withdraw-selected-btn" id="withdrawSelectedBtn" onclick="withdrawSelected()" disabled>💳 Вывести</button>
<button class="sell-all-btn" onclick="openBulkSellModal()">📦 Массовая</button>
</div>
</div>
@@ -658,12 +704,12 @@
data-rarity="{{ item.rarity }}"
data-type="{{ item.type }}"
data-name="{{ item.name|lower }}"
data-price="{{ item.price_rub or 100 }}">
data-price="{{ item.price_rub }}">
{% if not is_public %}
<input type="checkbox" class="item-select-checkbox"
onchange="onItemSelect(this)"
data-id="{{ item.id }}"
data-price="{{ item.price_rub or 100 }}">
data-price="{{ item.price_rub }}">
{% endif %}
<div class="skin-image-container">
<img src="{{ item.image_url or '/static/placeholder.png' }}"
@@ -681,11 +727,12 @@
<span>{{ item.wear }}</span>
</div>
<div class="skin-details" style="margin-top:3px;color:var(--success-color);">
<span>💰 {{ (item.price_rub or 100)|money }} ₽</span>
<span>💰 {{ (item.price_rub)|money }} ₽</span>
</div>
{% if not is_public %}
<div class="item-actions">
<button onclick="sellSingleItem({{ item.id }})" class="btn-sell-single">Продать</button>
<button onclick="withdrawSingleItem({{ item.id }}, {{ (item.price_rub)|int }})" class="btn-withdraw-single">Вывести</button>
</div>
{% endif %}
</div>
@@ -719,7 +766,8 @@
</div>
<div class="bulk-sell-actions">
<button class="btn btn-outline" onclick="closeBulkSellModal()">Отмена</button>
<button class="btn btn-primary" onclick="executeBulkSell()" style="background:var(--danger-color);">Продать</button>
<button class="btn btn-primary" onclick="executeBulkSell()" style="background:var(--danger-color);">💰 Продать</button>
<button class="btn btn-primary" onclick="executeBulkWithdraw()" style="background:var(--success-color);color:#000;">💳 Вывести</button>
</div>
</div>
</div>
@@ -826,6 +874,7 @@
const countBadge = document.getElementById('selectedCountBadge');
const priceBadge = document.getElementById('totalPriceBadge');
const sellBtn = document.getElementById('sellSelectedBtn');
const withdrawBtn = document.getElementById('withdrawSelectedBtn');
if (count > 0) {
countBadge.style.display = 'inline-block';
countBadge.textContent = count + ' выбрано';
@@ -834,10 +883,12 @@
priceBadge.style.display = 'inline-block';
priceBadge.textContent = total.toFixed(0) + ' ₽';
sellBtn.disabled = false;
withdrawBtn.disabled = false;
} else {
countBadge.style.display = 'none';
priceBadge.style.display = 'none';
sellBtn.disabled = true;
withdrawBtn.disabled = true;
}
}
@@ -846,9 +897,10 @@
const r = await fetch('/web/api/user/balance');
const d = await r.json();
if (d.success) {
document.querySelectorAll('.user-balance').forEach(el => {
el.textContent = Math.floor(d.balance) + ' ₽';
});
const el = document.getElementById('siteBalanceDisplay');
if (el) {
el.innerHTML = Math.floor(d.balance).toLocaleString('ru') + ' ₽ <span style="font-size:0.6rem;opacity:0.5;margin-left:0.25rem">▼ депозит</span>';
}
}
} catch(e) {}
}
@@ -906,6 +958,64 @@
});
}
function withdrawSingleItem(inventoryId, price) {
Notify.confirm('Вывести ' + (price||'').toLocaleString() + ' ₽ на карту? (Комиссия 10%)', 'Вывод', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const formData = new FormData();
formData.append('inventory_item_id', inventoryId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
SoundManager.balanceUpdate();
Notify.success('Выведено ' + data.net.toFixed(0) + ' ₽ на карту');
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + inventoryId + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
refreshBalance();
} else {
SoundManager.error();
Notify.error(data.error || 'Ошибка при выводе');
}
})
.catch(() => { SoundManager.error(); Notify.error('Ошибка соединения'); });
});
}
function withdrawSelected() {
if (selectedItems.size === 0) return;
let total = 0;
selectedItems.forEach(id => { total += itemPrices.get(id) || 100; });
const fee = total * 0.1;
Notify.confirm('Вывести ' + selectedItems.size + ' предметов на карту?\nСумма: ' + total.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (total - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
if (!confirmed) return;
SoundManager.click();
const ids = Array.from(selectedItems);
let completed = 0, errors = 0;
ids.forEach(function(invId) {
const formData = new FormData();
formData.append('inventory_item_id', invId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) {
completed++;
const card = document.querySelector('.inventory-skin-card[data-inventory-id="' + invId + '"]');
if (card) { card.style.opacity = '0'; card.style.transform = 'scale(0.8)'; setTimeout(() => card.remove(), 300); }
} else {
errors++;
}
if (completed + errors === ids.length) {
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
selectedItems.clear();
refreshBalance();
}
})
.catch(function() { errors++; });
});
});
}
function openBulkSellModal() {
const modal = document.getElementById('bulkSellModal');
const optionsDiv = document.getElementById('bulkSellOptions');
@@ -997,6 +1107,45 @@
});
}
function executeBulkWithdraw() {
const selected = [];
document.querySelectorAll('#bulkSellOptions input[type="checkbox"]:checked').forEach(cb => { if (cb.value && cb.value !== 'all') selected.push(cb.value); });
const allChecked = document.querySelector('#bulkSellOptions input[value="all"]')?.checked;
const ids = [];
document.querySelectorAll('.inventory-skin-card').forEach(card => {
if (card.style.display !== 'none') {
if (allChecked || selected.includes(card.dataset.rarity)) {
ids.push(parseInt(card.dataset.inventoryId));
}
}
});
if (ids.length === 0) { Notify.error('Выберите хотя бы одну редкость'); return; }
let totalPrice = 0;
ids.forEach(id => { totalPrice += itemPrices.get(id) || 100; });
const fee = totalPrice * 0.1;
Notify.confirm('Вывести ' + ids.length + ' предметов на карту?\nСумма: ' + totalPrice.toFixed(0) + ' ₽\nКомиссия: ' + fee.toFixed(0) + ' ₽\nК зачислению: ' + (totalPrice - fee).toFixed(0) + ' ₽', 'Массовый вывод', function(confirmed) {
if (!confirmed) return;
let completed = 0, errors = 0;
ids.forEach(function(invId) {
const formData = new FormData();
formData.append('inventory_item_id', invId);
fetch('/web/api/withdraw', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.success) { completed++; }
else { errors++; }
if (completed + errors === ids.length) {
Notify.success('Выведено ' + completed + ' предметов' + (errors ? ', ошибок: ' + errors : ''));
refreshBalance();
closeBulkSellModal();
window.location.reload();
}
})
.catch(function() { errors++; });
});
});
}
function filterItems() {
const rf = document.getElementById('rarityFilter').value.toLowerCase();
const tf = document.getElementById('typeFilter').value.toLowerCase();
@@ -1071,7 +1220,7 @@
+ '<div class="skin-details"><span class="rarity-' + rc + '">' + (item.rarity||'') + '</span><span>' + fs + '</span></div>'
+ '<div class="skin-details" style="margin-top:3px;"><span>' + (item.type||'') + '</span><span>' + (item.wear||'') + '</span></div>'
+ '<div class="skin-details" style="margin-top:3px;color:var(--success-color);"><span>💰 ' + ((item.price_rub||100)).toLocaleString() + ' ₽</span></div>'
+ '<div class="item-actions"><button onclick="sellSingleItem(' + item.id + ')" class="btn-sell-single">Продать</button></div></div></div>';
+ '<div class="item-actions"><button onclick="sellSingleItem(' + item.id + ')" class="btn-sell-single">Продать</button><button onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub||100) + ')" class="btn-withdraw-single">Вывести</button></div></div></div>';
});
grid.innerHTML = html;
selectedItems.clear();
@@ -1096,7 +1245,7 @@
grid.addEventListener('click', function(e) {
const card = e.target.closest('.inventory-skin-card');
if (!card) return;
if (e.target.closest('.item-select-checkbox, .btn-sell-single')) return;
if (e.target.closest('.item-select-checkbox, .btn-sell-single, .btn-withdraw-single')) return;
const invId = card.dataset.inventoryId;
if (!invId) return;
openItemCard(invId);
@@ -1155,7 +1304,10 @@
+ '<div class="ic-detail"><span class="ic-label">Тип</span><span class="ic-value">' + (item.type || 'Normal') + '</span></div>'
+ '<div class="ic-detail"><span class="ic-label">Цена</span><span class="ic-value" style="color:var(--success-color)">💰 ' + (item.price_rub || 0).toLocaleString() + ' ₽</span></div>'
+ (collHtml ? '<div class="ic-collection-title">🎨 Скины коллекции</div><div class="ic-collection-list">' + collHtml + '</div>' : '')
+ '<button class="btn-sell-single ic-sell-btn" onclick="sellSingleItem(' + item.id + '); closeItemCard();">Продать за ' + (item.price_rub || 0).toLocaleString() + ' ₽</button>'
+ '<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">'
+ '<button class="ic-sell-btn" style="flex:1;" onclick="sellSingleItem(' + item.id + '); closeItemCard();">Продать за ' + (item.price_rub || 0).toLocaleString() + ' ₽</button>'
+ '<button class="ic-sell-btn" style="flex:1;background:var(--success-color);color:#000;" onclick="withdrawSingleItem(' + item.id + ', ' + (item.price_rub || 100) + '); closeItemCard();">Вывести</button>'
+ '</div>'
+ '</div></div>';
}