This commit is contained in:
aaron 2025-08-11 13:19:45 +08:00
parent df3be587d8
commit 4ff8980268
7 changed files with 1144 additions and 59 deletions

View File

@ -29,6 +29,16 @@
</div>
</div>
<!-- 导航选项卡 -->
<div class="tabs-section">
<div class="tabs-nav">
<button class="tab-btn active" onclick="showTab('orders')">订单管理</button>
<button class="tab-btn" onclick="showTab('coupons')">优惠码管理</button>
</div>
</div>
<!-- 订单管理标签页 -->
<div id="ordersTab" class="tab-content active">
<div class="orders-section">
<div class="section-header">
<h2>订单列表</h2>
@ -69,6 +79,87 @@
</table>
</div>
</div>
</div>
<!-- 优惠码管理标签页 -->
<div id="couponsTab" class="tab-content">
<div class="coupons-section">
<div class="section-header">
<h2>优惠码管理</h2>
<button onclick="showCreateCouponModal()" class="btn-primary">+ 创建优惠码</button>
</div>
<div class="coupons-table-container">
<table class="coupons-table" id="couponsTable">
<thead>
<tr>
<th>优惠码</th>
<th>名称</th>
<th>折扣</th>
<th>类型</th>
<th>使用情况</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="couponsTableBody">
<tr>
<td colspan="8" class="loading">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 创建优惠码模态框 -->
<div id="createCouponModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>创建优惠码</h3>
<span class="close" onclick="closeCreateCouponModal()">&times;</span>
</div>
<div class="modal-body">
<div class="coupon-form">
<div class="form-group">
<label for="couponCode">优惠码:*</label>
<input type="text" id="couponCode" placeholder="请输入优惠码(建议大写字母+数字)" required>
</div>
<div class="form-group">
<label for="couponName">名称:*</label>
<input type="text" id="couponName" placeholder="请输入优惠码名称" required>
</div>
<div class="form-group">
<label for="discountType">折扣类型:*</label>
<select id="discountType" onchange="onDiscountTypeChange()" required>
<option value="percentage">百分比折扣</option>
<option value="fixed">固定金额</option>
</select>
</div>
<div class="form-group">
<label for="discountValue">折扣值:*</label>
<input type="number" id="discountValue" min="0" step="0.01" placeholder="请输入折扣值" required>
<small id="discountHint" class="form-hint">输入百分比数值10 表示10%折扣</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="isReusable" onchange="onReusableChange()">
可重复使用
</label>
</div>
<div class="form-group" id="maxUsesGroup" style="display: none;">
<label for="maxUses">最大使用次数:</label>
<input type="number" id="maxUses" min="1" value="1" placeholder="请输入最大使用次数">
</div>
<div class="form-actions">
<button onclick="createCoupon()" class="btn-primary">创建优惠码</button>
<button onclick="closeCreateCouponModal()" class="btn-secondary">取消</button>
</div>
</div>
</div>
</div>
</div>
<!-- 发货模态框 -->
<div id="shippingModal" class="modal" style="display: none;">

View File

@ -46,6 +46,230 @@ header p {
/* 管理员导航 - 已移除 */
/* 选项卡导航 */
.tabs-section {
margin-bottom: 30px;
}
.tabs-nav {
display: flex;
gap: 5px;
border-bottom: 2px solid #333;
}
.tab-btn {
padding: 12px 25px;
background: linear-gradient(145deg, #2d2d2d, #3d3d3d);
color: #b0b0b0;
border: none;
border-radius: 10px 10px 0 0;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
border-bottom: 3px solid transparent;
}
.tab-btn:hover {
background: linear-gradient(145deg, #3d3d3d, #4d4d4d);
color: #ffffff;
}
.tab-btn.active {
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
color: #ffd700;
border-bottom: 3px solid #ffd700;
box-shadow: 0 5px 15px rgba(255, 215, 0, 0.2);
}
/* 选项卡内容 */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 优惠码表格和订单表格共用样式 */
.coupons-section,
.orders-section {
background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
border-radius: 15px;
padding: 30px;
border: 1px solid #444;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.coupons-table-container,
.orders-table-container {
overflow-x: auto;
background: #0a0a0a;
border-radius: 10px;
border: 1px solid #333;
}
.coupons-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.coupons-table th {
background: linear-gradient(145deg, #2d2d2d, #3d3d3d);
color: #ffd700;
padding: 15px 12px;
text-align: left;
font-weight: bold;
border-bottom: 2px solid #444;
position: sticky;
top: 0;
z-index: 10;
}
.coupons-table td {
padding: 12px;
border-bottom: 1px solid #333;
color: #e0e0e0;
vertical-align: middle;
}
.coupons-table tbody tr {
transition: background-color 0.3s ease;
}
.coupons-table tbody tr:hover {
background-color: rgba(255, 215, 0, 0.05);
}
/* 优惠码状态标签 */
.coupon-active {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1px solid #28a745;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.coupon-inactive {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1px solid #dc3545;
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
/* 优惠码类型标签 */
.coupon-type {
display: inline-block;
padding: 3px 8px;
border-radius: 8px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.coupon-percentage {
background: rgba(0, 123, 255, 0.2);
color: #007bff;
border: 1px solid #007bff;
}
.coupon-fixed {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1px solid #ffc107;
}
/* 优惠码表单样式 */
.coupon-form .form-group {
margin-bottom: 20px;
}
.coupon-form label {
display: block;
margin-bottom: 8px;
color: #ffffff;
font-weight: 600;
}
.coupon-form input,
.coupon-form select {
width: 100%;
padding: 12px;
background: #0a0a0a;
border: 2px solid #444;
border-radius: 8px;
color: #e0e0e0;
font-size: 14px;
}
.coupon-form input:focus,
.coupon-form select:focus {
outline: none;
border-color: #ffd700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.2);
}
.form-hint {
display: block;
margin-top: 5px;
color: #888;
font-size: 12px;
}
/* 复选框样式 */
.coupon-form input[type="checkbox"] {
width: auto;
margin-right: 8px;
transform: scale(1.2);
}
.coupon-form label:has(input[type="checkbox"]) {
display: flex;
align-items: center;
cursor: pointer;
}
/* 按钮样式增强 */
.btn-toggle {
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-weight: bold;
margin-right: 5px;
transition: all 0.3s ease;
border: none;
}
.btn-enable {
background: linear-gradient(135deg, #28a745, #20c997);
color: #fff;
}
.btn-enable:hover {
background: linear-gradient(135deg, #20c997, #17a2b8);
transform: translateY(-1px);
}
.btn-disable {
background: linear-gradient(135deg, #6c757d, #5a6268);
color: #fff;
}
.btn-disable:hover {
background: linear-gradient(135deg, #5a6268, #495057);
transform: translateY(-1px);
}
/* 管理员导航 - 已移除 */
/* 统计卡片 */
.stats-section {
display: grid;

View File

@ -293,6 +293,11 @@ main {
font-weight: normal;
}
.coupon-discount-line {
color: #007bff !important;
font-weight: 600;
}
.subtotal-line {
color: #ffc107;
text-decoration: line-through;
@ -312,6 +317,96 @@ main {
color: #ffd700;
}
/* 优惠码输入区域 */
.coupon-section {
margin-top: 20px;
padding: 20px;
background: linear-gradient(145deg, #0f1419, #1a2332);
border-radius: 12px;
border: 1px solid #2c3e50;
}
.coupon-input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.coupon-input-group input {
flex: 1;
padding: 12px 15px;
border: 2px solid #444;
border-radius: 8px;
background: #0a0a0a;
color: #e0e0e0;
font-size: 14px;
transition: all 0.3s ease;
text-transform: uppercase;
}
.coupon-input-group input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 15px rgba(0, 123, 255, 0.2);
background: #1a1a1a;
}
.coupon-input-group input::placeholder {
color: #666;
text-transform: none;
}
.coupon-input-group button {
padding: 12px 20px;
background: linear-gradient(135deg, #007bff, #0056b3);
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
.coupon-input-group button:hover {
background: linear-gradient(135deg, #0056b3, #004085);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 123, 255, 0.3);
}
.coupon-input-group button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.coupon-message {
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.coupon-message.success {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
border: 1px solid #28a745;
}
.coupon-message.error {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
border: 1px solid #dc3545;
}
.coupon-message.info {
background: rgba(0, 123, 255, 0.1);
color: #007bff;
border: 1px solid #007bff;
}
/* 优惠提示样式 */
.discount-tips {
margin-top: 20px;

View File

@ -61,6 +61,10 @@
<span>优惠:</span>
<span id="discount-text">$0.00 USDT (无优惠)</span>
</div>
<div class="price-line coupon-discount-line" id="coupon-discount-line" style="display: none;">
<span>优惠码:</span>
<span id="coupon-discount-text">$0.00 USDT</span>
</div>
<div class="price-line subtotal-line" id="subtotal-line">
<span>小计:</span>
<span id="subtotal-price">$99.99 USDT</span>
@ -70,6 +74,15 @@
<span id="total-price">$99.99 USDT</span>
</div>
</div>
<!-- 优惠码输入 -->
<div class="coupon-section">
<div class="coupon-input-group">
<input type="text" id="coupon-code" placeholder="输入优惠码(可选)" maxlength="20">
<button type="button" id="apply-coupon-btn" onclick="applyCoupon()">使用</button>
</div>
<div id="coupon-message" class="coupon-message" style="display: none;"></div>
</div>
</div>
<!-- 配送信息分割线 -->

View File

@ -1,19 +1,47 @@
// 订单管理页面脚本
let currentOrders = [];
let currentCoupons = [];
let currentOrderId = null;
// DOM 元素
const ordersTableBody = document.getElementById('ordersTableBody');
const couponsTableBody = document.getElementById('couponsTableBody');
const searchInput = document.getElementById('searchOrder');
const statusFilter = document.getElementById('statusFilter');
const orderDetailModal = document.getElementById('orderDetailModal');
const shippingModal = document.getElementById('shippingModal');
const createCouponModal = document.getElementById('createCouponModal');
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadOrders();
loadCoupons();
});
// 选项卡切换功能
function showTab(tabName) {
// 隐藏所有选项卡内容
const tabs = document.querySelectorAll('.tab-content');
tabs.forEach(tab => tab.classList.remove('active'));
// 移除所有按钮的活动状态
const buttons = document.querySelectorAll('.tab-btn');
buttons.forEach(btn => btn.classList.remove('active'));
// 显示选中的选项卡内容
document.getElementById(tabName + 'Tab').classList.add('active');
// 激活相应按钮
event.target.classList.add('active');
// 根据选项卡加载数据
if (tabName === 'coupons') {
loadCoupons();
} else if (tabName === 'orders') {
loadOrders();
}
}
// 加载订单列表
async function loadOrders(search = '', status = '') {
try {
@ -430,6 +458,258 @@ function formatDate(dateString) {
});
}
// 加载优惠码列表
async function loadCoupons() {
try {
showCouponsLoading();
const response = await fetch('/api/admin/coupons');
const data = await response.json();
if (response.ok) {
currentCoupons = data.coupons;
updateCouponsTable(data.coupons);
} else {
throw new Error(data.error || '加载优惠码失败');
}
} catch (error) {
console.error('Load coupons error:', error);
showCouponsError('加载优惠码失败: ' + error.message);
}
}
// 更新优惠码表格
function updateCouponsTable(coupons) {
if (!coupons || coupons.length === 0) {
couponsTableBody.innerHTML = '<tr><td colspan="8" class="loading">暂无优惠码数据</td></tr>';
return;
}
couponsTableBody.innerHTML = coupons.map(coupon => `
<tr>
<td>
<strong style="font-family: monospace; color: #ffd700;">${coupon.code}</strong>
</td>
<td>${coupon.name}</td>
<td>
<strong style="color: #28a745;">
${coupon.discount_type === 'percentage' ? coupon.discount_value + '%' : '$' + coupon.discount_value.toFixed(2)}
</strong>
</td>
<td>
<span class="coupon-type coupon-${coupon.discount_type}">
${coupon.discount_type === 'percentage' ? '百分比' : '固定金额'}
</span>
</td>
<td>
<div style="font-size: 13px;">
已使用: <strong>${coupon.used_count}</strong> / ${coupon.max_uses === 999999 ? '无限' : coupon.max_uses}<br>
<small style="color: #888;">${coupon.is_reusable ? '可重复使用' : '一次性使用'}</small>
</div>
</td>
<td>
<span class="status-badge ${coupon.is_active ? 'coupon-active' : 'coupon-inactive'}">
${coupon.is_active ? '启用' : '禁用'}
</span>
</td>
<td>
${formatDate(coupon.created_at)}
</td>
<td>
<button class="action-btn ${coupon.is_active ? 'btn-disable' : 'btn-enable'}"
onclick="toggleCouponStatus(${coupon.id}, ${!coupon.is_active})"
title="${coupon.is_active ? '禁用' : '启用'}优惠码">
${coupon.is_active ? '禁用' : '启用'}
</button>
<button class="action-btn btn-delete" onclick="confirmDeleteCoupon(${coupon.id})" title="删除优惠码">
删除
</button>
</td>
</tr>
`).join('');
}
// 显示创建优惠码模态框
function showCreateCouponModal() {
// 清空表单
document.getElementById('couponCode').value = '';
document.getElementById('couponName').value = '';
document.getElementById('discountType').value = 'percentage';
document.getElementById('discountValue').value = '';
document.getElementById('isReusable').checked = false;
document.getElementById('maxUses').value = '1';
document.getElementById('maxUsesGroup').style.display = 'none';
onDiscountTypeChange(); // 更新提示文本
createCouponModal.style.display = 'flex';
document.getElementById('couponCode').focus();
}
// 关闭创建优惠码模态框
function closeCreateCouponModal() {
createCouponModal.style.display = 'none';
}
// 折扣类型变化处理
function onDiscountTypeChange() {
const discountType = document.getElementById('discountType').value;
const hint = document.getElementById('discountHint');
if (discountType === 'percentage') {
hint.textContent = '输入百分比数值10 表示10%折扣';
} else {
hint.textContent = '输入固定金额5 表示减5美元';
}
}
// 可重复使用变化处理
function onReusableChange() {
const isReusable = document.getElementById('isReusable').checked;
const maxUsesGroup = document.getElementById('maxUsesGroup');
if (isReusable) {
maxUsesGroup.style.display = 'block';
document.getElementById('maxUses').value = '10'; // 默认10次
} else {
maxUsesGroup.style.display = 'none';
document.getElementById('maxUses').value = '1';
}
}
// 创建优惠码
async function createCoupon() {
const code = document.getElementById('couponCode').value.trim();
const name = document.getElementById('couponName').value.trim();
const discountType = document.getElementById('discountType').value;
const discountValue = parseFloat(document.getElementById('discountValue').value);
const isReusable = document.getElementById('isReusable').checked;
const maxUses = parseInt(document.getElementById('maxUses').value);
// 验证表单
if (!code || !name || !discountValue) {
alert('请填写所有必填字段');
return;
}
if (discountValue <= 0) {
alert('折扣值必须大于0');
return;
}
if (discountType === 'percentage' && discountValue > 100) {
alert('百分比折扣不能超过100%');
return;
}
if (isReusable && (!maxUses || maxUses < 1)) {
alert('最大使用次数必须大于0');
return;
}
try {
const response = await fetch('/api/admin/coupons', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: code,
name: name,
discount_type: discountType,
discount_value: discountValue,
is_reusable: isReusable,
max_uses: maxUses
})
});
const result = await response.json();
if (response.ok) {
alert('优惠码创建成功!');
closeCreateCouponModal();
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '创建优惠码失败');
}
} catch (error) {
console.error('Create coupon error:', error);
alert('创建优惠码失败: ' + error.message);
}
}
// 切换优惠码状态
async function toggleCouponStatus(couponId, isActive) {
try {
const response = await fetch(`/api/admin/coupons/${couponId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
is_active: isActive
})
});
const result = await response.json();
if (response.ok) {
alert(`优惠码已${isActive ? '启用' : '禁用'}`);
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '更新优惠码状态失败');
}
} catch (error) {
console.error('Toggle coupon status error:', error);
alert('更新优惠码状态失败: ' + error.message);
}
}
// 确认删除优惠码
function confirmDeleteCoupon(couponId) {
const coupon = currentCoupons.find(c => c.id === couponId);
if (!coupon) {
alert('优惠码未找到');
return;
}
const confirmMessage = `确认删除以下优惠码吗?\n\n优惠码:${coupon.code}\n名称:${coupon.name}\n已使用次数:${coupon.used_count}\n\n⚠️ 此操作不可撤销!`;
if (confirm(confirmMessage)) {
deleteCoupon(couponId);
}
}
// 删除优惠码
async function deleteCoupon(couponId) {
try {
const response = await fetch(`/api/admin/coupons/${couponId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
alert('优惠码已成功删除!');
loadCoupons(); // 重新加载优惠码列表
} else {
throw new Error(result.error || '删除优惠码失败');
}
} catch (error) {
console.error('Delete coupon error:', error);
alert('删除优惠码失败: ' + error.message);
}
}
// 显示优惠码加载状态
function showCouponsLoading() {
couponsTableBody.innerHTML = '<tr><td colspan="8" class="loading">加载中...</td></tr>';
}
// 显示优惠码错误信息
function showCouponsError(message) {
couponsTableBody.innerHTML = `<tr><td colspan="8" class="loading" style="color: #ff4757;">错误: ${message}</td></tr>`;
}
// 确认删除订单
function confirmDeleteOrder(orderId) {
const order = currentOrders.find(o => o.order_id === orderId);

View File

@ -1,6 +1,7 @@
// 全局变量
let currentProduct = null;
let currentOrder = null;
let appliedCoupon = null; // 当前应用的优惠码
// DOM 元素
const productPrice = document.getElementById('product-price');
@ -99,6 +100,24 @@ function setupEventListeners() {
}
});
// 优惠码输入监听
document.getElementById('coupon-code').addEventListener('input', function(e) {
// 自动转换为大写
e.target.value = e.target.value.toUpperCase();
// 清除已应用的优惠码状态
if (appliedCoupon) {
clearAppliedCoupon();
}
});
// 优惠码回车键应用
document.getElementById('coupon-code').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
applyCoupon();
}
});
// 支付状态检查
document.getElementById('check-status-btn').addEventListener('click', checkPaymentStatus);
@ -118,41 +137,61 @@ function updateTotalPrice() {
const unitPrice = currentProduct.price;
const originalTotal = unitPrice * quantity;
// 计算折扣
let discount = 0;
let discountText = '';
let finalTotal = originalTotal;
// 计算数量折扣
let quantityDiscount = 0;
let quantityDiscountText = '';
let afterQuantityDiscount = originalTotal;
if (quantity >= 5) {
discount = 0.1; // 9折优惠10%
discountText = '5个及以上 9折优惠';
finalTotal = originalTotal * 0.9;
quantityDiscount = originalTotal * 0.1; // 9折优惠10%
quantityDiscountText = '5个及以上 9折优惠';
afterQuantityDiscount = originalTotal * 0.9;
} else if (quantity >= 2) {
discount = 0.05; // 9.5折优惠5%
discountText = '2个 9.5折优惠';
finalTotal = originalTotal * 0.95;
quantityDiscount = originalTotal * 0.05; // 9.5折优惠5%
quantityDiscountText = '2个 9.5折优惠';
afterQuantityDiscount = originalTotal * 0.95;
}
// 计算优惠码折扣
let couponDiscount = 0;
let finalTotal = afterQuantityDiscount;
if (appliedCoupon) {
if (appliedCoupon.discount_type === 'percentage') {
couponDiscount = afterQuantityDiscount * (appliedCoupon.discount_value / 100);
} else if (appliedCoupon.discount_type === 'fixed') {
couponDiscount = Math.min(appliedCoupon.discount_value, afterQuantityDiscount);
}
finalTotal = afterQuantityDiscount - couponDiscount;
}
// 更新显示
const discountLine = document.getElementById('discount-line');
const subtotalLine = document.getElementById('subtotal-line');
const couponDiscountLine = document.getElementById('coupon-discount-line');
const discountTextElement = document.getElementById('discount-text');
const subtotalPriceElement = document.getElementById('subtotal-price');
const couponDiscountTextElement = document.getElementById('coupon-discount-text');
// 始终显示优惠和小计信息
if (discount > 0) {
// 显示优惠信息
const discountAmount = originalTotal - finalTotal;
discountTextElement.textContent = `-$${discountAmount.toFixed(2)} USDT (${discountText})`;
// 数量折扣显示
if (quantityDiscount > 0) {
discountTextElement.textContent = `-$${quantityDiscount.toFixed(2)} USDT (${quantityDiscountText})`;
subtotalPriceElement.textContent = `$${originalTotal.toFixed(2)} USDT`;
discountLine.classList.remove('no-discount');
} else {
// 显示无优惠
discountTextElement.textContent = `$0.00 USDT (无优惠)`;
subtotalPriceElement.textContent = `$${originalTotal.toFixed(2)} USDT`;
discountLine.classList.add('no-discount');
}
// 优惠码折扣显示
if (appliedCoupon && couponDiscount > 0) {
couponDiscountTextElement.textContent = `-$${couponDiscount.toFixed(2)} USDT (${appliedCoupon.name})`;
couponDiscountLine.style.display = 'flex';
} else {
couponDiscountLine.style.display = 'none';
}
discountLine.style.display = 'flex';
subtotalLine.style.display = 'flex';
@ -221,6 +260,87 @@ function validatePhoneField(phoneInput) {
}
}
// 应用优惠码
async function applyCoupon() {
const couponCode = document.getElementById('coupon-code').value.trim();
const applyBtn = document.getElementById('apply-coupon-btn');
const messageEl = document.getElementById('coupon-message');
if (!couponCode) {
showCouponMessage('请输入优惠码', 'error');
return;
}
// 禁用按钮
applyBtn.disabled = true;
applyBtn.textContent = '验证中...';
try {
const response = await fetch('/api/coupons/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: couponCode })
});
const result = await response.json();
if (response.ok && result.valid) {
// 优惠码有效,应用优惠
appliedCoupon = result.coupon;
updateTotalPrice();
// 更新UI状态
document.getElementById('coupon-code').readOnly = true;
applyBtn.textContent = '移除';
applyBtn.onclick = clearAppliedCoupon;
const discountText = result.coupon.discount_type === 'percentage'
? `${result.coupon.discount_value}%折扣`
: `$${result.coupon.discount_value}减免`;
showCouponMessage(`✓ 优惠码"${result.coupon.name}"已应用 (${discountText})`, 'success');
} else {
showCouponMessage(result.message || '优惠码无效', 'error');
}
} catch (error) {
console.error('Apply coupon error:', error);
showCouponMessage('验证优惠码失败,请稍后重试', 'error');
} finally {
if (!appliedCoupon) {
applyBtn.disabled = false;
applyBtn.textContent = '使用';
}
}
}
// 清除已应用的优惠码
function clearAppliedCoupon() {
appliedCoupon = null;
updateTotalPrice();
// 重置UI状态
document.getElementById('coupon-code').readOnly = false;
document.getElementById('coupon-code').value = '';
const applyBtn = document.getElementById('apply-coupon-btn');
applyBtn.disabled = false;
applyBtn.textContent = '使用';
applyBtn.onclick = applyCoupon;
showCouponMessage('优惠码已移除', 'info');
setTimeout(() => {
document.getElementById('coupon-message').style.display = 'none';
}, 2000);
}
// 显示优惠码消息
function showCouponMessage(message, type = 'info') {
const messageEl = document.getElementById('coupon-message');
messageEl.textContent = message;
messageEl.className = `coupon-message ${type}`;
messageEl.style.display = 'block';
}
// 验证数量
function validateQuantity() {
const value = parseInt(quantityInput.value);
@ -264,7 +384,8 @@ async function handleOrderSubmit(e) {
customer_name: formData.get('customer_name').trim(),
customer_email: formData.get('customer_email').trim(),
customer_phone: formData.get('customer_phone').trim(),
shipping_address: formData.get('shipping_address').trim()
shipping_address: formData.get('shipping_address').trim(),
coupon_code: appliedCoupon ? appliedCoupon.code : null
};
// 验证必填字段
@ -608,7 +729,8 @@ async function handleShippingOrderSubmit(e) {
customer_name: formData.get('customer_name').trim(),
customer_email: formData.get('customer_email').trim(),
customer_phone: formData.get('customer_phone').trim(),
shipping_address: formData.get('shipping_address').trim()
shipping_address: formData.get('shipping_address').trim(),
coupon_code: appliedCoupon ? appliedCoupon.code : null
};
// 验证必填字段

274
server.js
View File

@ -36,9 +36,36 @@ db.serialize(() => {
shipping_status TEXT DEFAULT 'pending',
tracking_number TEXT,
shipping_notes TEXT,
coupon_code TEXT,
coupon_discount REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// 创建优惠码表
db.run(`CREATE TABLE IF NOT EXISTS coupons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
discount_type TEXT NOT NULL, -- 'percentage' 'fixed'
discount_value REAL NOT NULL,
is_reusable INTEGER DEFAULT 0, -- 0:一次性 1:可重复使用
used_count INTEGER DEFAULT 0,
max_uses INTEGER DEFAULT 1,
is_active INTEGER DEFAULT 1, -- 0:禁用 1:启用
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// 创建优惠码使用记录表
db.run(`CREATE TABLE IF NOT EXISTS coupon_uses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
coupon_id INTEGER NOT NULL,
order_id TEXT NOT NULL,
used_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (coupon_id) REFERENCES coupons(id),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
)`);
});
// UPay 配置
@ -85,7 +112,8 @@ app.post('/api/orders', async (req, res) => {
customer_name,
customer_email,
customer_phone,
shipping_address
shipping_address,
coupon_code
} = req.body;
// 验证产品
@ -99,12 +127,53 @@ app.post('/api/orders', async (req, res) => {
const expectedUnitPrice = product.price;
const originalTotal = expectedUnitPrice * quantity;
let expectedTotal = originalTotal;
let quantityDiscount = 0;
let couponDiscount = 0;
let appliedCoupon = null;
// 服务器端折扣计算验证
// 数量折扣计算
if (quantity >= 5) {
expectedTotal = originalTotal * 0.9; // 9折
quantityDiscount = originalTotal * 0.1; // 9折优惠10%
expectedTotal = originalTotal * 0.9;
} else if (quantity >= 2) {
expectedTotal = originalTotal * 0.95; // 9.5折
quantityDiscount = originalTotal * 0.05; // 9.5折优惠5%
expectedTotal = originalTotal * 0.95;
}
// 处理优惠码
if (coupon_code) {
const coupon = await new Promise((resolve, reject) => {
db.get(`
SELECT * FROM coupons
WHERE code = ? AND is_active = 1
`, [coupon_code.toUpperCase()], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
if (coupon) {
// 检查使用次数限制
if (!coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.status(400).json({ error: '优惠码已被使用' });
}
if (coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.status(400).json({ error: '优惠码使用次数已达上限' });
}
// 计算优惠码折扣(基于数量折扣后的价格)
if (coupon.discount_type === 'percentage') {
couponDiscount = expectedTotal * (coupon.discount_value / 100);
} else if (coupon.discount_type === 'fixed') {
couponDiscount = Math.min(coupon.discount_value, expectedTotal);
}
expectedTotal -= couponDiscount;
appliedCoupon = coupon;
} else {
return res.status(400).json({ error: '优惠码不存在或已失效' });
}
}
// 价格验证(允许小数点误差)
@ -117,23 +186,41 @@ app.post('/api/orders', async (req, res) => {
// 保存订单到数据库
const stmt = db.prepare(`
INSERT INTO orders (order_id, product_name, quantity, unit_price, total_amount,
customer_name, customer_email, customer_phone, shipping_address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
customer_name, customer_email, customer_phone, shipping_address,
coupon_code, coupon_discount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run([
order_id, product.name, quantity, expectedTotal / quantity, expectedTotal,
customer_name, customer_email, customer_phone, shipping_address
customer_name, customer_email, customer_phone, shipping_address,
appliedCoupon ? appliedCoupon.code : null,
couponDiscount
], function(err) {
if (err) {
console.error(err);
return res.status(500).json({ error: '订单创建失败' });
}
// 如果使用了优惠码,更新使用次数和记录
if (appliedCoupon) {
db.run(`
UPDATE coupons
SET used_count = used_count + 1, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [appliedCoupon.id]);
db.run(`
INSERT INTO coupon_uses (coupon_id, order_id)
VALUES (?, ?)
`, [appliedCoupon.id, order_id]);
}
res.json({
success: true,
order_id,
total_amount: expectedTotal,
coupon_discount: couponDiscount,
message: '订单创建成功'
});
});
@ -411,6 +498,179 @@ app.put('/api/admin/orders/:order_id/shipping', (req, res) => {
);
});
// 验证优惠码
app.post('/api/coupons/validate', (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: '请输入优惠码' });
}
db.get(`
SELECT * FROM coupons
WHERE code = ? AND is_active = 1
`, [code], (err, coupon) => {
if (err) {
console.error('Query coupon error:', err);
return res.status(500).json({ error: '查询优惠码失败' });
}
if (!coupon) {
return res.json({ valid: false, message: '优惠码不存在或已失效' });
}
// 检查使用次数限制
if (!coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.json({ valid: false, message: '优惠码已被使用' });
}
if (coupon.is_reusable && coupon.used_count >= coupon.max_uses) {
return res.json({ valid: false, message: '优惠码使用次数已达上限' });
}
res.json({
valid: true,
coupon: {
id: coupon.id,
code: coupon.code,
name: coupon.name,
discount_type: coupon.discount_type,
discount_value: coupon.discount_value
}
});
});
});
// 获取所有优惠码(管理员)
app.get('/api/admin/coupons', (req, res) => {
const { limit = 50, offset = 0 } = req.query;
db.all(`
SELECT * FROM coupons
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`, [parseInt(limit), parseInt(offset)], (err, coupons) => {
if (err) {
console.error('Query coupons error:', err);
return res.status(500).json({ error: '查询优惠码失败' });
}
res.json({ coupons });
});
});
// 创建优惠码(管理员)
app.post('/api/admin/coupons', (req, res) => {
const {
code,
name,
discount_type,
discount_value,
is_reusable,
max_uses
} = req.body;
// 验证必填字段
if (!code || !name || !discount_type || discount_value === undefined) {
return res.status(400).json({ error: '请填写所有必填字段' });
}
// 验证折扣类型
if (!['percentage', 'fixed'].includes(discount_type)) {
return res.status(400).json({ error: '无效的折扣类型' });
}
// 验证折扣值
if (discount_value <= 0) {
return res.status(400).json({ error: '折扣值必须大于0' });
}
if (discount_type === 'percentage' && discount_value > 100) {
return res.status(400).json({ error: '百分比折扣不能超过100%' });
}
const stmt = db.prepare(`
INSERT INTO coupons (code, name, discount_type, discount_value, is_reusable, max_uses)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run([
code.toUpperCase(),
name,
discount_type,
discount_value,
is_reusable ? 1 : 0,
max_uses || 1
], function(err) {
if (err) {
console.error('Create coupon error:', err);
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(400).json({ error: '优惠码已存在' });
}
return res.status(500).json({ error: '创建优惠码失败' });
}
res.json({
success: true,
message: '优惠码创建成功',
coupon_id: this.lastID
});
});
stmt.finalize();
});
// 更新优惠码状态(管理员)
app.put('/api/admin/coupons/:id', (req, res) => {
const { id } = req.params;
const { is_active } = req.body;
if (is_active === undefined) {
return res.status(400).json({ error: '请提供状态参数' });
}
db.run(`
UPDATE coupons
SET is_active = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [is_active ? 1 : 0, id], function(err) {
if (err) {
console.error('Update coupon error:', err);
return res.status(500).json({ error: '更新优惠码失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '优惠码未找到' });
}
res.json({
success: true,
message: '优惠码状态更新成功'
});
});
});
// 删除优惠码(管理员)
app.delete('/api/admin/coupons/:id', (req, res) => {
const { id } = req.params;
db.run('DELETE FROM coupons WHERE id = ?', [id], function(err) {
if (err) {
console.error('Delete coupon error:', err);
return res.status(500).json({ error: '删除优惠码失败' });
}
if (this.changes === 0) {
return res.status(404).json({ error: '优惠码未找到' });
}
res.json({
success: true,
message: '优惠码已删除'
});
});
});
// 删除订单(管理员)
app.delete('/api/admin/orders/:order_id', (req, res) => {
const { order_id } = req.params;