实现折扣价格功能:数据库表、后端API、前端界面完整支持折扣价格字段

This commit is contained in:
ypsdf1
2026-07-16 18:33:21 +08:00
parent 00da16abb6
commit f3919a8715
4 changed files with 99 additions and 18 deletions
+15
View File
@@ -3540,6 +3540,7 @@ async function loadShopConfig(el) {
<div class="form-row"><label>临时优惠</label><input id="scfOffer" placeholder="如:限时8折"></div> <div class="form-row"><label>临时优惠</label><input id="scfOffer" placeholder="如:限时8折"></div>
<div class="form-row"><label>优惠有效期</label><input id="scfExpire" placeholder="yyyy-MM-dd 或 2026年7月16日"></div> <div class="form-row"><label>优惠有效期</label><input id="scfExpire" placeholder="yyyy-MM-dd 或 2026年7月16日"></div>
<div class="form-row"><label>售价(元)</label><input id="scfPrice" type="number" min="0.01" step="0.01" placeholder="0.01"></div> <div class="form-row"><label>售价(元)</label><input id="scfPrice" type="number" min="0.01" step="0.01" placeholder="0.01"></div>
<div class="form-row"><label>优惠后价格</label><input id="scfDiscountPrice" type="text" inputmode="decimal" placeholder="留空=无优惠, 0.9=9折, 1.5=直接价格"></div>
<div class="form-row"><label>到账债券</label><input id="scfBond" placeholder="如:1-10 或 100"></div> <div class="form-row"><label>到账债券</label><input id="scfBond" placeholder="如:1-10 或 100"></div>
<div class="form-row"><label>上下架</label><select id="scfActive"><option value="1">上架</option><option value="0">下架</option></select></div> <div class="form-row"><label>上下架</label><select id="scfActive"><option value="1">上架</option><option value="0">下架</option></select></div>
<div style="margin-top:14px;text-align:right"> <div style="margin-top:14px;text-align:right">
@@ -3593,6 +3594,18 @@ function showShopConfigForm(config) {
document.getElementById('scfOffer').value = config ? (config.temporary_offer||'') : ''; document.getElementById('scfOffer').value = config ? (config.temporary_offer||'') : '';
document.getElementById('scfExpire').value = config ? (config.offer_expire||'') : ''; document.getElementById('scfExpire').value = config ? (config.offer_expire||'') : '';
document.getElementById('scfPrice').value = config ? config.price : ''; document.getElementById('scfPrice').value = config ? config.price : '';
// 优惠后价格:如果原价<1认为是折扣,否则是直接价格
if (config && config.discount_price > 0) {
const ratio = config.discount_price / (config.price || 1);
// 如果是整数倍(如原价10,优惠价5),显示折扣形式
if (ratio < 1 && ratio > 0) {
document.getElementById('scfDiscountPrice').value = ratio;
} else {
document.getElementById('scfDiscountPrice').value = config.discount_price;
}
} else {
document.getElementById('scfDiscountPrice').value = '';
}
document.getElementById('scfBond').value = config ? config.bond_reward : '1-1'; document.getElementById('scfBond').value = config ? config.bond_reward : '1-1';
document.getElementById('scfActive').value = config ? config.is_active : 1; document.getElementById('scfActive').value = config ? config.is_active : 1;
document.getElementById('shopConfigFormOverlay').classList.add('show'); document.getElementById('shopConfigFormOverlay').classList.add('show');
@@ -3630,6 +3643,7 @@ async function saveShopConfig() {
const offer = document.getElementById('scfOffer').value.trim(); const offer = document.getElementById('scfOffer').value.trim();
const expire = document.getElementById('scfExpire').value.trim(); const expire = document.getElementById('scfExpire').value.trim();
const price = parseFloat(document.getElementById('scfPrice').value) || 0; const price = parseFloat(document.getElementById('scfPrice').value) || 0;
const discountPriceRaw = document.getElementById('scfDiscountPrice').value.trim();
const bond = document.getElementById('scfBond').value.trim(); const bond = document.getElementById('scfBond').value.trim();
const active = parseInt(document.getElementById('scfActive').value) || 1; const active = parseInt(document.getElementById('scfActive').value) || 1;
@@ -3651,6 +3665,7 @@ async function saveShopConfig() {
temporary_offer: offer, temporary_offer: offer,
offer_expire: expire, offer_expire: expire,
price: price, price: price,
discount_price: discountPriceRaw,
bond_reward: bond, bond_reward: bond,
is_active: active is_active: active
}); });
+26 -2
View File
@@ -315,7 +315,12 @@ function createOrder($token, $productId = 0) {
error('商品已售罄'); error('商品已售罄');
} }
$money = number_format($product['price'], 2, '.', ''); // 使用折扣价格(如果有)
$actualPrice = $product['price'];
if (isset($product['discount_price']) && $product['discount_price'] > 0) {
$actualPrice = $product['discount_price'];
}
$money = number_format($actualPrice, 2, '.', '');
$name = $product['item_name']; $name = $product['item_name'];
// 解析债券范围 // 解析债券范围
@@ -952,10 +957,21 @@ function getShopProducts() {
temporary_offer TEXT NOT NULL DEFAULT '', temporary_offer TEXT NOT NULL DEFAULT '',
offer_expire TEXT NOT NULL DEFAULT '', offer_expire TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0, price REAL NOT NULL DEFAULT 0,
discount_price REAL NOT NULL DEFAULT 0,
bond_reward TEXT NOT NULL DEFAULT '1-1', bond_reward TEXT NOT NULL DEFAULT '1-1',
is_active INTEGER NOT NULL DEFAULT 1, is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT 0 created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
)"); )");
// 兼容旧表:检查并添加discount_price字段
$hasCol = false;
$cols = $db->query("PRAGMA table_info(shop_configs)");
while ($col = $cols->fetchArray(SQLITE3_ASSOC)) {
if ($col['name'] === 'discount_price') { $hasCol = true; break; }
}
if (!$hasCol) {
$db->exec("ALTER TABLE shop_configs ADD COLUMN discount_price REAL NOT NULL DEFAULT 0");
}
// 查询上架商品(is_active=1) // 查询上架商品(is_active=1)
$stmt = $db->query("SELECT * FROM shop_configs WHERE is_active = 1 ORDER BY id ASC"); $stmt = $db->query("SELECT * FROM shop_configs WHERE is_active = 1 ORDER BY id ASC");
@@ -976,6 +992,12 @@ function getShopProducts() {
// 解析有效期 // 解析有效期
$expireInfo = parseExpireDate($row['offer_expire']); $expireInfo = parseExpireDate($row['offer_expire']);
// 计算实际显示价格(如果有折扣价格且折扣价格>0)
$displayPrice = $row['price'];
if (isset($row['discount_price']) && $row['discount_price'] > 0) {
$displayPrice = $row['discount_price'];
}
$products[] = [ $products[] = [
'id' => $row['id'], 'id' => $row['id'],
'item_name' => $row['item_name'], 'item_name' => $row['item_name'],
@@ -985,6 +1007,8 @@ function getShopProducts() {
'expire_valid' => $expireInfo['valid'], 'expire_valid' => $expireInfo['valid'],
'expire_display' => $expireInfo['display'], 'expire_display' => $expireInfo['display'],
'price' => $row['price'], 'price' => $row['price'],
'discount_price' => $row['discount_price'] ?? 0,
'display_price' => $displayPrice,
'bond_reward' => $bondReward, 'bond_reward' => $bondReward,
'bond_min' => $bondMin, 'bond_min' => $bondMin,
'bond_max' => $bondMax, 'bond_max' => $bondMax,
+48 -14
View File
@@ -45,11 +45,21 @@ function ensureShopConfigsTable($db) {
temporary_offer TEXT NOT NULL DEFAULT '', temporary_offer TEXT NOT NULL DEFAULT '',
offer_expire TEXT NOT NULL DEFAULT '', offer_expire TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0, price REAL NOT NULL DEFAULT 0,
discount_price REAL NOT NULL DEFAULT 0,
bond_reward TEXT NOT NULL DEFAULT '1-1', bond_reward TEXT NOT NULL DEFAULT '1-1',
is_active INTEGER NOT NULL DEFAULT 1, is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0 updated_at INTEGER NOT NULL DEFAULT 0
)"); )");
// 兼容旧表:检查并添加discount_price字段
$hasCol = false;
$cols = $db->query("PRAGMA table_info(shop_configs)");
while ($col = $cols->fetchArray(SQLITE3_ASSOC)) {
if ($col['name'] === 'discount_price') { $hasCol = true; break; }
}
if (!$hasCol) {
$db->exec("ALTER TABLE shop_configs ADD COLUMN discount_price REAL NOT NULL DEFAULT 0");
}
} }
// 检查管理员登录 // 检查管理员登录
@@ -463,40 +473,63 @@ function handleShopConfigSave($db) {
$temporaryOffer = getParam('temporary_offer', ''); $temporaryOffer = getParam('temporary_offer', '');
$offerExpire = getParam('offer_expire', ''); $offerExpire = getParam('offer_expire', '');
$price = floatval(getParam('price', 0)); $price = floatval(getParam('price', 0));
$discountPriceRaw = getParam('discount_price', '0');
$bondReward = getParam('bond_reward', '1-1'); $bondReward = getParam('bond_reward', '1-1');
$isActive = intval(getParam('is_active', 1)); $isActive = intval(getParam('is_active', 1));
// 验证库存 // 验证库存
if ($stock < -1) { if ($stock < -1) {
error('库存不能小于-1'); error('库存不能小于-1');
} }
// 验证售价 // 验证售价
if ($price <= 0) { if ($price <= 0) {
error('售价必须大于0'); error('售价必须大于0');
} }
// 解析优惠后价格
// 支持两种格式:
// 1. 直接价格(如 1、2、1.5)→ 直接使用
// 2. 折扣(如 0.9、0.8)→ 计算 原价 × 折扣
$discountPrice = 0;
$discountPriceRaw = trim($discountPriceRaw);
if ($discountPriceRaw !== '' && $discountPriceRaw !== '0') {
$discountValue = floatval($discountPriceRaw);
if ($discountValue <= 0) {
error('优惠后价格必须大于0');
}
// 判断是直接价格还是折扣(<1的数认为是折扣)
if ($discountValue < 1) {
// 折扣:原价 × 折扣
$discountPrice = round($price * $discountValue, 2);
} else {
// 直接价格
$discountPrice = round($discountValue, 2);
}
}
// 验证债券范围格式(支持盲盒范围,如"1-10") // 验证债券范围格式(支持盲盒范围,如"1-10")
if (!preg_match('/^\d+(-\d+)?$/', $bondReward)) { if (!preg_match('/^\d+(-\d+)?$/', $bondReward)) {
error('债券范围格式错误,应为数字或范围如"1-10"'); error('债券范围格式错误,应为数字或范围如"1-10"');
} }
// 解析并验证有效期 // 解析并验证有效期
$expireInfo = parseExpireDate($offerExpire); $expireInfo = parseExpireDate($offerExpire);
if ($offerExpire && !$expireInfo['valid']) { if ($offerExpire && !$expireInfo['valid']) {
error('有效期格式错误,请使用yyyy-MM-dd或中文格式如"2026年7月16日"'); error('有效期格式错误,请使用yyyy-MM-dd或中文格式如"2026年7月16日"');
} }
$now = time(); $now = time();
if ($id > 0) { if ($id > 0) {
// 编辑 // 编辑
$stmt = $db->prepare("UPDATE shop_configs SET $stmt = $db->prepare("UPDATE shop_configs SET
item_name = :name, item_name = :name,
stock = :stock, stock = :stock,
temporary_offer = :offer, temporary_offer = :offer,
offer_expire = :expire, offer_expire = :expire,
price = :price, price = :price,
discount_price = :discount_price,
bond_reward = :bond, bond_reward = :bond,
is_active = :active, is_active = :active,
updated_at = :updated updated_at = :updated
@@ -504,25 +537,26 @@ function handleShopConfigSave($db) {
$stmt->bindValue(':id', $id, SQLITE3_INTEGER); $stmt->bindValue(':id', $id, SQLITE3_INTEGER);
} else { } else {
// 添加 // 添加
$stmt = $db->prepare("INSERT INTO shop_configs $stmt = $db->prepare("INSERT INTO shop_configs
(item_name, stock, temporary_offer, offer_expire, price, bond_reward, is_active, created_at, updated_at) (item_name, stock, temporary_offer, offer_expire, price, discount_price, bond_reward, is_active, created_at, updated_at)
VALUES (:name, :stock, :offer, :expire, :price, :bond, :active, :created, :updated)"); VALUES (:name, :stock, :offer, :expire, :price, :discount_price, :bond, :active, :created, :updated)");
$stmt->bindValue(':created', $now, SQLITE3_INTEGER); $stmt->bindValue(':created', $now, SQLITE3_INTEGER);
} }
$stmt->bindValue(':name', $itemName, SQLITE3_TEXT); $stmt->bindValue(':name', $itemName, SQLITE3_TEXT);
$stmt->bindValue(':stock', $stock, SQLITE3_INTEGER); $stmt->bindValue(':stock', $stock, SQLITE3_INTEGER);
$stmt->bindValue(':offer', $temporaryOffer, SQLITE3_TEXT); $stmt->bindValue(':offer', $temporaryOffer, SQLITE3_TEXT);
$stmt->bindValue(':expire', $offerExpire, SQLITE3_TEXT); $stmt->bindValue(':expire', $offerExpire, SQLITE3_TEXT);
$stmt->bindValue(':price', $price, SQLITE3_FLOAT); $stmt->bindValue(':price', $price, SQLITE3_FLOAT);
$stmt->bindValue(':discount_price', $discountPrice, SQLITE3_FLOAT);
$stmt->bindValue(':bond', $bondReward, SQLITE3_TEXT); $stmt->bindValue(':bond', $bondReward, SQLITE3_TEXT);
$stmt->bindValue(':active', $isActive, SQLITE3_INTEGER); $stmt->bindValue(':active', $isActive, SQLITE3_INTEGER);
$stmt->bindValue(':updated', $now, SQLITE3_INTEGER); $stmt->bindValue(':updated', $now, SQLITE3_INTEGER);
$stmt->execute(); $stmt->execute();
$savedId = $id > 0 ? $id : $db->lastInsertRowID(); $savedId = $id > 0 ? $id : $db->lastInsertRowID();
success(['id' => $savedId], $id > 0 ? '配置已更新' : '配置已添加'); success(['id' => $savedId, 'discount_price' => $discountPrice], $id > 0 ? '配置已更新' : '配置已添加');
} }
/** /**
+10 -2
View File
@@ -1218,14 +1218,22 @@ if ($currentVersion !== $BUILD_VERSION) {
// ★ 盲盒随机语录 // ★ 盲盒随机语录
const quoteText = isBlindBox ? `<div style="font-size:12px;color:var(--accent);margin-top:6px;font-style:italic">${blindBoxQuotes[Math.floor(Math.random() * blindBoxQuotes.length)]}</div>` : ''; const quoteText = isBlindBox ? `<div style="font-size:12px;color:var(--accent);margin-top:6px;font-style:italic">${blindBoxQuotes[Math.floor(Math.random() * blindBoxQuotes.length)]}</div>` : '';
// 计算显示价格(折扣价格优先)
const displayPrice = product.discount_price > 0 ? product.discount_price : product.price;
const hasDiscount = product.discount_price > 0 && product.discount_price < product.price;
html += ` html += `
<div class="shop-product-card" data-product-id="${product.id}" data-price="${product.price}" data-bond-min="${product.bond_min}" data-bond-max="${product.bond_max}" data-stock="${product.stock}" onclick="selectShopProduct(this)" style="border:1px solid ${isSelected ? 'var(--accent)' : 'var(--border)'};border-radius:14px;padding:16px;margin-bottom:12px;background:${isSelected ? 'linear-gradient(135deg,rgba(63,185,80,0.08),rgba(88,166,255,0.06))' : 'transparent'};cursor:pointer;transition:all 0.2s ease;${isSoldOut ? 'opacity:0.72;' : ''}"> <div class="shop-product-card" data-product-id="${product.id}" data-price="${displayPrice}" data-bond-min="${product.bond_min}" data-bond-max="${product.bond_max}" data-stock="${product.stock}" onclick="selectShopProduct(this)" style="border:1px solid ${isSelected ? 'var(--accent)' : 'var(--border)'};border-radius:14px;padding:16px;margin-bottom:12px;background:${isSelected ? 'linear-gradient(135deg,rgba(63,185,80,0.08),rgba(88,166,255,0.06))' : 'transparent'};cursor:pointer;transition:all 0.2s ease;${isSoldOut ? 'opacity:0.72;' : ''}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-size:14px;color:var(--fg)">${product.item_name}</span> <span style="font-size:14px;color:var(--fg)">${product.item_name}</span>
<span style="font-size:12px;color:${stockColor};border:1px solid ${stockColor};border-radius:999px;padding:2px 10px">${stockText}</span> <span style="font-size:12px;color:${stockColor};border:1px solid ${stockColor};border-radius:999px;padding:2px 10px">${stockText}</span>
</div> </div>
<div style="display:flex;justify-content:space-between;align-items:baseline"> <div style="display:flex;justify-content:space-between;align-items:baseline">
<div><div style="font-size:28px;font-weight:800;color:var(--accent)">¥${product.price}</div><div style="font-size:12px;color:var(--dim)">支付金额</div></div> <div>
<div style="font-size:28px;font-weight:800;color:var(--accent)">¥${displayPrice}</div>
${hasDiscount ? `<div style="font-size:12px;color:var(--dim);text-decoration:line-through">原价 ¥${product.price}</div>` : ''}
<div style="font-size:12px;color:var(--dim)">支付金额</div>
</div>
<div style="font-size:22px;color:var(--green)">→ ${bondDisplay}</div> <div style="font-size:22px;color:var(--green)">→ ${bondDisplay}</div>
</div> </div>
${offerText} ${offerText}