From 170db69eb436e8de1df86a782dc50c303e2361c9 Mon Sep 17 00:00:00 2001 From: superlishunqin <852326703@qq.com> Date: Mon, 14 Jul 2025 05:06:05 +0800 Subject: [PATCH] version_1 --- app/__init__.py | 4 + app/static/css/cart.css | 52 ++++- app/static/css/checkout.css | 289 ++++++++++++++++++++++++-- app/static/js/cart.js | 109 +++++++++- app/static/js/checkout.js | 260 ++++++++++++++++++++--- app/templates/admin/product_form.html | 4 +- app/templates/base.html | 4 +- app/templates/cart/index.html | 24 ++- app/templates/order/checkout.html | 74 +++++-- docker/Dockerfile | 49 +++++ docker/README.md | 72 +++++++ docker/deploy.sh | 41 ++++ docker/docker-compose.yml | 56 +++++ docker/test.sh | 21 ++ requirements.txt | 1 + update_config.py | 27 +++ 16 files changed, 1003 insertions(+), 84 deletions(-) create mode 100644 docker/README.md create mode 100755 docker/deploy.sh create mode 100755 docker/test.sh create mode 100644 update_config.py diff --git a/app/__init__.py b/app/__init__.py index cc1dc88..0c49691 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,6 @@ from flask import Flask from config.database import init_db +from app.utils.email_service import mail from config.config import Config def create_app(config_name=None): @@ -9,6 +10,9 @@ def create_app(config_name=None): # 初始化数据库 init_db(app) + # 初始化邮件服务 + mail.init_app(app) + # 注册蓝图 from app.views.auth import auth_bp from app.views.main import main_bp diff --git a/app/static/css/cart.css b/app/static/css/cart.css index 665396c..5e290a4 100644 --- a/app/static/css/cart.css +++ b/app/static/css/cart.css @@ -7,7 +7,57 @@ } .quantity-input { - width: 60px; + width: 50px !important; + min-width: 50px !important; + padding: 4px 8px !important; + text-align: center !important; + border-radius: 0 !important; + -moz-appearance: textfield !important; + background-color: #fff !important; + border: 1px solid #ced4da !important; +} + +.quantity-input:focus { + border-color: #86b7fe !important; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important; + outline: 0 !important; +} + +.quantity-input::-webkit-outer-spin-button, +.quantity-input::-webkit-inner-spin-button { + -webkit-appearance: none !important; + margin: 0 !important; +} + +.quantity-input::-webkit-outer-spin-button, +.quantity-input::-webkit-inner-spin-button { + -webkit-appearance: none !important; + margin: 0 !important; +} + +.quantity-control { + width: 120px; + margin: 0 auto; +} + +.quantity-btn { + width: 30px; + height: 30px; + padding: 0; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.quantity-btn:hover:not(:disabled) { + background-color: #e9ecef; + border-color: #adb5bd; +} + +.quantity-btn:disabled { + opacity: 0.5; + cursor: not-allowed; } .item-checkbox { diff --git a/app/static/css/checkout.css b/app/static/css/checkout.css index aefe9d2..92e5316 100644 --- a/app/static/css/checkout.css +++ b/app/static/css/checkout.css @@ -3,22 +3,239 @@ margin-bottom: 1.5rem; } +/* 地址选择区域美化 */ +.address-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + .address-card { + position: relative; cursor: pointer; transition: all 0.3s ease; border: 2px solid #e9ecef; + border-radius: 12px; + overflow: hidden; + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .address-card:hover { border-color: #007bff; - box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1); + box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15); + transform: translateY(-2px); } .address-card.selected { border-color: #007bff; - background-color: #e7f3ff; + background: linear-gradient(135deg, #e7f3ff 0%, #cce7ff 100%); + box-shadow: 0 4px 16px rgba(0, 123, 255, 0.2); + transform: translateY(-2px); } +.address-card.selected::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #007bff, #0056b3); +} + +.address-card-body { + padding: 1.25rem; + position: relative; +} + +.address-header { + display: flex; + justify-content: between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.address-receiver { + flex: 1; +} + +.receiver-name { + font-size: 1.1rem; + font-weight: 600; + color: #2c3e50; + margin-bottom: 0.25rem; + display: flex; + align-items: center; +} + +.receiver-name i { + margin-right: 0.5rem; + color: #007bff; +} + +.receiver-phone { + color: #6c757d; + font-size: 0.9rem; + margin-bottom: 0; + display: flex; + align-items: center; +} + +.receiver-phone i { + margin-right: 0.5rem; + color: #28a745; +} + +.address-content { + background: rgba(255,255,255,0.7); + padding: 0.75rem; + border-radius: 8px; + margin-bottom: 0.75rem; + border-left: 3px solid #e9ecef; + transition: all 0.3s ease; +} + +.address-card.selected .address-content { + border-left-color: #007bff; + background: rgba(255,255,255,0.9); +} + +.address-text { + color: #495057; + margin-bottom: 0; + line-height: 1.5; + display: flex; + align-items: flex-start; +} + +.address-text i { + margin-right: 0.5rem; + margin-top: 0.2rem; + color: #6c757d; + flex-shrink: 0; +} + +.address-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.default-badge { + background: linear-gradient(45deg, #ff6b6b, #ee5a52); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + display: inline-flex; + align-items: center; + box-shadow: 0 2px 4px rgba(255,107,107,0.3); +} + +.default-badge i { + margin-right: 0.25rem; +} + +/* 自定义单选按钮样式 */ +.custom-radio { + position: relative; + width: 24px; + height: 24px; +} + +.custom-radio input[type="radio"] { + position: absolute; + opacity: 0; + width: 100%; + height: 100%; + margin: 0; + cursor: pointer; +} + +.custom-radio .radio-mark { + position: absolute; + top: 0; + left: 0; + width: 24px; + height: 24px; + background: #fff; + border: 2px solid #ddd; + border-radius: 50%; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.custom-radio input[type="radio"]:checked + .radio-mark { + border-color: #007bff; + background: #007bff; + box-shadow: 0 0 0 3px rgba(0,123,255,0.2); +} + +.custom-radio .radio-mark::after { + content: ''; + width: 8px; + height: 8px; + border-radius: 50%; + background: white; + opacity: 0; + transform: scale(0); + transition: all 0.2s ease; +} + +.custom-radio input[type="radio"]:checked + .radio-mark::after { + opacity: 1; + transform: scale(1); +} + +/* 新增地址按钮美化 */ +.add-address-btn { + background: linear-gradient(45deg, #28a745, #20c997); + border: none; + color: white; + padding: 0.5rem 1rem; + border-radius: 25px; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(40,167,69,0.3); +} + +.add-address-btn:hover { + background: linear-gradient(45deg, #218838, #1e7e34); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(40,167,69,0.4); + color: white; +} + +.add-address-btn i { + margin-right: 0.5rem; +} + +/* 卡片头部美化 */ +.checkout-section .card-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.25rem; +} + +.checkout-section .card-header h5 { + margin-bottom: 0; + color: #495057; + font-weight: 600; +} + +.checkout-section .card-header i { + margin-right: 0.5rem; + color: #007bff; + font-size: 1.1rem; +} + +/* 其他原有样式保持不变 */ .product-item { padding: 1rem 0; border-bottom: 1px solid #e9ecef; @@ -79,12 +296,13 @@ /* 响应式设计 */ @media (max-width: 768px) { - .checkout-section .col-md-4, - .checkout-section .col-md-3 { - margin-bottom: 1rem; + .address-container { + grid-template-columns: 1fr; + gap: 0.75rem; } - .address-card { + .checkout-section .col-md-4, + .checkout-section .col-md-3 { margin-bottom: 1rem; } @@ -92,6 +310,14 @@ .product-item .col-md-6 { margin-bottom: 0.5rem; } + + .address-card-body { + padding: 1rem; + } + + .receiver-name { + font-size: 1rem; + } } /* 动画效果 */ @@ -106,27 +332,39 @@ } } +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + .alert { animation: fadeIn 0.3s ease; } +.address-card { + animation: slideIn 0.4s ease; +} + +.address-card:nth-child(2) { + animation-delay: 0.1s; +} + +.address-card:nth-child(3) { + animation-delay: 0.2s; +} + /* 按钮样式 */ .btn-lg { padding: 0.75rem 1.5rem; font-size: 1.1rem; } -/* 卡片头部样式 */ -.card-header h5 { - margin-bottom: 0; - color: #495057; -} - -.card-header i { - margin-right: 0.5rem; - color: #007bff; -} - /* 表单标签样式 */ .form-check-label { cursor: pointer; @@ -162,3 +400,20 @@ .breadcrumb-item + .breadcrumb-item::before { color: #6c757d; } + +/* 加载状态动画 */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(0, 123, 255, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(0, 123, 255, 0); + } +} + +.address-card.loading { + animation: pulse 1.5s infinite; +} diff --git a/app/static/js/cart.js b/app/static/js/cart.js index a2bc0fd..79479f9 100644 --- a/app/static/js/cart.js +++ b/app/static/js/cart.js @@ -75,11 +75,13 @@ function updateTotalPrice() { // 修改数量 function changeQuantity(cartId, delta) { - const input = document.querySelector(`[data-cart-id="${cartId}"]`); + const input = document.querySelector(`[data-cart-id="${cartId}"].quantity-input`); + if (!input) return; + const currentValue = parseInt(input.value); const newValue = currentValue + delta; - - if (newValue >= 1 && newValue <= parseInt(input.max)) { + + if (newValue >= 1) { updateQuantity(cartId, newValue); } } @@ -103,25 +105,39 @@ function updateQuantity(cartId, quantity) { if (data.success) { // 更新页面显示 const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); - cartItem.querySelector('.quantity-input').value = quantity; + const quantityInput = cartItem.querySelector('.quantity-input'); + quantityInput.value = quantity; cartItem.querySelector('.item-total').textContent = data.item_total.toFixed(2); + // 简单的按钮状态更新 + const minusBtn = cartItem.querySelector('button[onclick*="-1"]'); + const plusBtn = cartItem.querySelector('button[onclick*="1"]'); + + if (minusBtn) { + minusBtn.disabled = quantity <= 1; + } + + // 暂时不限制最大值,避免选择器错误 + if (plusBtn) { + plusBtn.disabled = false; + } + // 更新总价 updateTotalPrice(); // 更新全局购物车数量 updateCartBadge(data.cart_count); + // 显示成功消息 showSuccessMessage('数量更新成功'); } else { - alert(data.message); - // 恢复原始值 + alert(data.message || '更新失败'); location.reload(); } }) .catch(error => { console.error('Error:', error); - alert('更新失败'); + alert('网络错误,请重试'); location.reload(); }); } @@ -217,3 +233,82 @@ function checkout() { window.location.href = `/cart/checkout?${params.toString()}`; } + +// 显示成功消息 +function showSuccessMessage(message) { + const alert = document.createElement('div'); + alert.className = 'alert alert-success alert-dismissible fade show position-fixed'; + alert.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;'; + alert.innerHTML = ` + ${message} + + `; + + document.body.appendChild(alert); + + setTimeout(() => { + if (alert.parentNode) { + alert.remove(); + } + }, 3000); +} + +// 更新购物车徽章 +// 验证数量输入 +function validateQuantityInput(input) { + // 只允许数字 + input.value = input.value.replace(/[^0-9]/g, ""); + + // 如果为空或0,显示1 + if (input.value === "" || parseInt(input.value) < 1) { + input.value = "1"; + } + + // 检查最大值 + const maxStock = parseInt(input.getAttribute("max")) || 999; + if (parseInt(input.value) > maxStock) { + input.value = maxStock.toString(); + } +} + +// 从输入框更新数量 +function updateQuantityFromInput(cartId, input) { + let quantity = parseInt(input.value); + + // 验证输入 + if (isNaN(quantity) || quantity < 1) { + quantity = 1; + input.value = "1"; + } + + const maxStock = parseInt(input.getAttribute("max")) || 999; + if (quantity > maxStock) { + quantity = maxStock; + input.value = maxStock.toString(); + } + + // 更新数量 + updateQuantity(cartId, quantity); +} + +// 处理键盘事件 +function handleQuantityKeyPress(event, cartId, input) { + // 回车键确认 + if (event.key === "Enter") { + input.blur(); + return; + } + + // 只允许数字键 + if (!/[0-9]/.test(event.key) && !["Backspace", "Delete", "Tab", "Escape", "Enter", "Home", "End", "ArrowLeft", "ArrowRight"].includes(event.key)) { + event.preventDefault(); + } +} + +function updateCartBadge(count) { + const badge = document.querySelector('.cart-badge'); + if (badge) { + badge.textContent = count; + badge.style.display = count > 0 ? 'inline' : 'none'; + } +} diff --git a/app/static/js/checkout.js b/app/static/js/checkout.js index da4e686..192c4e4 100644 --- a/app/static/js/checkout.js +++ b/app/static/js/checkout.js @@ -15,65 +15,145 @@ document.addEventListener('DOMContentLoaded', function() { if (subtotalElement) { subtotal = parseFloat(subtotalElement.textContent.replace('¥', '')); } + + // 初始化地址卡片动画 + initAddressAnimations(); }); -// 选择地址 +// 初始化地址卡片动画 +function initAddressAnimations() { + const addressCards = document.querySelectorAll('.address-card'); + addressCards.forEach((card, index) => { + card.style.animationDelay = `${index * 0.1}s`; + }); +} + +// 选择地址(优化版本) function selectAddress(addressId) { selectedAddressId = addressId; - // 更新UI - document.querySelectorAll('.address-card').forEach(card => { - card.classList.remove('selected'); - }); - - const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`); - if (selectedCard) { - selectedCard.classList.add('selected'); + // 添加加载状态 + const clickedCard = document.querySelector(`[data-address-id="${addressId}"]`); + if (clickedCard) { + clickedCard.classList.add('loading'); } - // 更新单选按钮 - const radioButton = document.querySelector(`input[value="${addressId}"]`); - if (radioButton) { - radioButton.checked = true; - } + // 延迟执行UI更新,给用户视觉反馈 + setTimeout(() => { + // 更新UI + document.querySelectorAll('.address-card').forEach(card => { + card.classList.remove('selected', 'loading'); + }); + + const selectedCard = document.querySelector(`[data-address-id="${addressId}"]`); + if (selectedCard) { + selectedCard.classList.add('selected'); + + // 添加选中动画效果 + selectedCard.style.animation = 'none'; + selectedCard.offsetHeight; // 触发重排 + selectedCard.style.animation = 'slideIn 0.4s ease'; + } + + // 更新单选按钮 + const radioButton = document.querySelector(`input[value="${addressId}"]`); + if (radioButton) { + radioButton.checked = true; + + // 触发单选按钮动画 + const radioMark = radioButton.nextElementSibling; + if (radioMark) { + radioMark.style.transform = 'scale(1.1)'; + setTimeout(() => { + radioMark.style.transform = 'scale(1)'; + }, 200); + } + } + + // 显示成功提示 + showSelectionFeedback('地址选择成功'); + }, 150); } -// 更新运费 +// 显示选择反馈 +function showSelectionFeedback(message) { + // 创建临时提示元素 + const feedback = document.createElement('div'); + feedback.className = 'position-fixed top-0 start-50 translate-middle-x mt-3'; + feedback.style.zIndex = '9999'; + feedback.innerHTML = ` + + `; + + document.body.appendChild(feedback); + + // 3秒后自动移除 + setTimeout(() => { + if (feedback.parentNode) { + feedback.remove(); + } + }, 3000); +} + +// 更新运费(优化版本) function updateShippingFee() { const shippingMethodElement = document.querySelector('input[name="shipping_method"]:checked'); if (!shippingMethodElement) return; const shippingMethod = shippingMethodElement.value; let fee = 0; + let description = ''; switch(shippingMethod) { case 'express': fee = 10; + description = '次日达服务'; break; case 'same_day': fee = 20; + description = '当日达服务'; break; default: fee = 0; + description = '标准配送'; } + // 添加动画效果更新价格 const shippingFeeElement = document.getElementById('shippingFee'); const totalAmountElement = document.getElementById('totalAmount'); - if (shippingFeeElement) { - shippingFeeElement.textContent = `¥${fee.toFixed(2)}`; - } - - if (totalAmountElement) { - totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`; + if (shippingFeeElement && totalAmountElement) { + // 淡出效果 + shippingFeeElement.style.opacity = '0.5'; + totalAmountElement.style.opacity = '0.5'; + + setTimeout(() => { + shippingFeeElement.textContent = `¥${fee.toFixed(2)}`; + totalAmountElement.textContent = `¥${(subtotal + fee).toFixed(2)}`; + + // 淡入效果 + shippingFeeElement.style.opacity = '1'; + totalAmountElement.style.opacity = '1'; + + // 显示配送方式反馈 + if (fee > 0) { + showSelectionFeedback(`已选择${description} (+¥${fee})`); + } else { + showSelectionFeedback(`已选择${description} (免费)`); + } + }, 200); } } -// 提交订单 +// 提交订单(优化版本) function submitOrder() { // 验证地址选择 if (!selectedAddressId) { showAlert('请选择收货地址', 'warning'); + highlightAddressSection(); return; } @@ -84,11 +164,13 @@ function submitOrder() { if (!shippingMethodElement) { showAlert('请选择配送方式', 'warning'); + highlightSection('shipping'); return; } if (!paymentMethodElement) { showAlert('请选择支付方式', 'warning'); + highlightSection('payment'); return; } @@ -123,6 +205,10 @@ function submitOrder() { const originalText = submitBtn.innerHTML; submitBtn.innerHTML = ' 提交中...'; submitBtn.disabled = true; + submitBtn.classList.add('loading'); + + // 添加页面加载遮罩 + showLoadingOverlay(); // 提交订单 fetch('/order/create', { @@ -139,23 +225,141 @@ function submitOrder() { return response.json(); }) .then(data => { + hideLoadingOverlay(); + if (data.success) { showAlert('订单创建成功!正在跳转到支付页面...', 'success'); + + // 添加成功动画 + submitBtn.innerHTML = ' 订单创建成功'; + submitBtn.classList.add('btn-success'); + submitBtn.classList.remove('btn-danger'); + setTimeout(() => { window.location.href = `/order/pay/${data.payment_sn}`; }, 1500); } else { showAlert(data.message || '订单创建失败', 'error'); - // 恢复按钮状态 - submitBtn.innerHTML = originalText; - submitBtn.disabled = false; + restoreSubmitButton(submitBtn, originalText); } }) .catch(error => { console.error('提交订单错误:', error); + hideLoadingOverlay(); showAlert('提交订单失败,请重试', 'error'); - // 恢复按钮状态 - submitBtn.innerHTML = originalText; - submitBtn.disabled = false; + restoreSubmitButton(submitBtn, originalText); }); } + +// 恢复提交按钮状态 +function restoreSubmitButton(submitBtn, originalText) { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + submitBtn.classList.remove('loading', 'btn-success'); + submitBtn.classList.add('btn-danger'); +} + +// 高亮地址选择区域 +function highlightAddressSection() { + const addressSection = document.querySelector('#addressList'); + if (addressSection) { + addressSection.style.border = '2px solid #dc3545'; + addressSection.style.borderRadius = '8px'; + addressSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + setTimeout(() => { + addressSection.style.border = ''; + }, 3000); + } +} + +// 高亮指定区域 +function highlightSection(sectionType) { + let selector = ''; + switch(sectionType) { + case 'shipping': + selector = 'input[name="shipping_method"]'; + break; + case 'payment': + selector = 'input[name="payment_method"]'; + break; + } + + if (selector) { + const element = document.querySelector(selector); + if (element) { + const section = element.closest('.card'); + if (section) { + section.style.border = '2px solid #dc3545'; + section.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + setTimeout(() => { + section.style.border = ''; + }, 3000); + } + } + } +} + +// 显示加载遮罩 +function showLoadingOverlay() { + const overlay = document.createElement('div'); + overlay.id = 'loadingOverlay'; + overlay.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center'; + overlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; + overlay.style.zIndex = '9999'; + overlay.innerHTML = ` +
+
+ 加载中... +
+
正在创建订单...
+

请稍候,不要关闭页面

+
+ `; + + document.body.appendChild(overlay); +} + +// 隐藏加载遮罩 +function hideLoadingOverlay() { + const overlay = document.getElementById('loadingOverlay'); + if (overlay) { + overlay.remove(); + } +} + +// 增强的提示函数 +function showAlert(message, type = 'info') { + const alertClass = { + 'success': 'alert-success', + 'error': 'alert-danger', + 'warning': 'alert-warning', + 'info': 'alert-info' + }[type] || 'alert-info'; + + const icon = { + 'success': 'bi-check-circle', + 'error': 'bi-exclamation-triangle', + 'warning': 'bi-exclamation-triangle', + 'info': 'bi-info-circle' + }[type] || 'bi-info-circle'; + + const alertDiv = document.createElement('div'); + alertDiv.className = `alert ${alertClass} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`; + alertDiv.style.zIndex = '10000'; + alertDiv.innerHTML = ` + + ${message} + + `; + + document.body.appendChild(alertDiv); + + // 5秒后自动移除 + setTimeout(() => { + if (alertDiv.parentNode) { + alertDiv.remove(); + } + }, 5000); +} diff --git a/app/templates/admin/product_form.html b/app/templates/admin/product_form.html index 95e7d13..a539a00 100644 --- a/app/templates/admin/product_form.html +++ b/app/templates/admin/product_form.html @@ -261,14 +261,14 @@
+ name="single_stock" min="0" value="{{ product.inventory[0].stock if product and product.inventory else 0 }}">
+ name="warning_stock" min="0" value="{{ product.inventory[0].warning_stock if product and product.inventory else 10 }}">
diff --git a/app/templates/base.html b/app/templates/base.html index deca345..4eac9d5 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -106,8 +106,8 @@
联系我们

- service@taibai-mall.com
- 400-888-8888 + 392437483@qq.com
+ 17753825492

diff --git a/app/templates/cart/index.html b/app/templates/cart/index.html index 8a5f23a..ec780bb 100644 --- a/app/templates/cart/index.html +++ b/app/templates/cart/index.html @@ -99,17 +99,25 @@
-
- - + + + - + {% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}> + +
小计:¥{{ "%.2f"|format(item.get_total_price()) }} diff --git a/app/templates/order/checkout.html b/app/templates/order/checkout.html index 446de1c..e96c889 100644 --- a/app/templates/order/checkout.html +++ b/app/templates/order/checkout.html @@ -3,6 +3,7 @@ {% block head %} + {% endblock %} {% block content %} @@ -20,33 +21,68 @@
-
+
{% for address in addresses %} -
-
-
-
-
-
{{ address.receiver_name }}
-

{{ address.receiver_phone }}

-

{{ address.get_full_address() }}

-
-
- -
+
+
+
+
+
+ + {{ address.receiver_name }} +
+

+ + {{ address.receiver_phone }} +

+
+ + +
+
+ +
+

+ + {{ address.get_full_address() }} +

+
+ +
{% endfor %} + + {% if not addresses %} +
+
+ +
暂无收货地址
+

请先添加收货地址,然后继续下单

+ + 立即添加 + +
+
+ {% endif %}
@@ -54,7 +90,7 @@
-
商品信息
+
商品信息
{% for item in cart_items %} diff --git a/docker/Dockerfile b/docker/Dockerfile index e69de29..4e9f867 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -0,0 +1,49 @@ +# 使用Python 3.9作为基础镜像以匹配开发环境 +FROM python:3.9-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + FLASK_APP=run.py \ + FLASK_ENV=production + +# 创建并配置国内镜像源 +RUN echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm main" > /etc/apt/sources.list && \ + echo "deb https://mirrors.ustc.edu.cn/debian/ bookworm-updates main" >> /etc/apt/sources.list && \ + echo "deb https://mirrors.ustc.edu.cn/debian-security/ bookworm-security main" >> /etc/apt/sources.list + +# 安装系统依赖(包括libmagic) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + default-libmysqlclient-dev \ + libmagic1 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# 使用阿里云pip镜像源(更稳定) +RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \ + pip config set install.trusted-host mirrors.aliyun.com + +# 安装Python依赖 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制项目文件 +COPY . . + +# 运行配置更新脚本 +RUN python update_config.py + +# 创建非root用户运行应用 +RUN useradd -m appuser +RUN chown -R appuser:appuser /app +USER appuser + +# 暴露自定义端口50400 +EXPOSE 50400 + +# 启动命令 +CMD ["gunicorn", "--bind", "0.0.0.0:50400", "run:app"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..7c47567 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,72 @@ +# 太白购物商城 Docker 部署指南 + +## 前置条件 + +确保您的服务器上已安装: +- Docker (20.10.0+) +- Docker Compose (2.0.0+) + +## 部署步骤 + +### 1. 克隆项目到服务器 + +git clone <项目仓库URL> shopping-platform +cd shopping-platform + +### 2. 配置数据库连接 + +您有两个选择: + +#### 选项1:使用Docker内置MySQL数据库 +编辑 docker/docker-compose.yml,取消以下环境变量的注释: + +- MYSQL_HOST=db +- MYSQL_USER=shopping_user +- MYSQL_PASSWORD=shopping_password +- MYSQL_DB=shopping_db +- MYSQL_PORT=3306 + +#### 选项2:使用现有外部数据库 +默认配置使用外部数据库(27.124.22.104)。保持docker-compose.yml中的数据库环境变量为注释状态。 + +### 3. 修改密钥和敏感信息 +编辑 docker/docker-compose.yml,更改SECRET_KEY为安全的密钥。 + +### 4. 构建并启动应用 + +cd docker +docker-compose up -d --build + +### 5. 验证部署 +访问 http://服务器IP:50400 确认应用是否正常运行。 + +## 维护命令 + +### 查看日志 +docker-compose logs -f app + +### 重启应用 +docker-compose restart app + +### 完全重新部署 +docker-compose down +docker-compose up -d --build + +### 停止所有服务 +docker-compose down + +## 端口说明 +- 应用端口:50400 +- MySQL端口:3366 (避免与主机MySQL冲突) + +## 疑难解答 + +### 应用无法连接到数据库 +1. 检查数据库连接配置 +2. 查看应用日志:docker-compose logs app + +### 端口冲突 +修改 docker-compose.yml 中的端口映射 + +### 权限问题 +确保当前用户有权限访问Docker diff --git a/docker/deploy.sh b/docker/deploy.sh new file mode 100755 index 0000000..80d3b3f --- /dev/null +++ b/docker/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +echo "🚀 开始部署太白购物商城..." + +# 检查Docker是否已安装 +if ! command -v docker &> /dev/null; then + echo "❌ Docker未安装。请先安装Docker。" + exit 1 +fi + +if ! command -v docker-compose &> /dev/null; then + echo "❌ Docker Compose未安装。请先安装Docker Compose。" + exit 1 +fi + +# 确保gunicorn在依赖中 +grep -q "gunicorn" ../requirements.txt || echo "gunicorn==20.1.0" >> ../requirements.txt + +# 构建并启动应用 +echo "🔨 构建Docker镜像..." +docker-compose build + +echo "🚀 启动服务..." +docker-compose up -d + +# 等待几秒钟让服务启动 +sleep 5 + +# 检查服务状态 +echo "📊 检查服务状态..." +docker-compose ps + +echo "" +echo "✅ 部署完成!" +echo "🌐 应用地址: http://localhost:50400" +echo "" +echo "📋 常用命令:" +echo " 查看日志: docker-compose logs -f app" +echo " 重启应用: docker-compose restart app" +echo " 停止应用: docker-compose down" +echo " 查看状态: docker-compose ps" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e69de29..8c93cce 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3.8' + +services: + app: + build: + context: .. + dockerfile: docker/Dockerfile + restart: always + ports: + - "50400:50400" + depends_on: + - db + environment: + # 应用配置 + - FLASK_CONFIG=production + - SECRET_KEY=change-me-in-production + + # 数据库配置 - 两种选择之一: + + # 选项1:使用Docker内置MySQL(取消下面的注释启用) + # - MYSQL_HOST=db + # - MYSQL_USER=taibai + # - MYSQL_PASSWORD=taibaishopping + # - MYSQL_DB=shopping_db + # - MYSQL_PORT=3306 + + # 选项2:使用现有外部数据库(默认) + # 保持上面的选项1注释即可使用配置文件中的默认设置 + volumes: + - ../logs:/app/logs + - ../app/static/uploads:/app/app/static/uploads + networks: + - app-network + + db: + image: mysql:8.0 + restart: always + environment: + - MYSQL_DATABASE=online_shopping + - MYSQL_USER=taibai + - MYSQL_PASSWORD=taibaishopping + - MYSQL_ROOT_PASSWORD=root_password_here + volumes: + - mysql-data:/var/lib/mysql + ports: + - "3366:3306" # 使用3366避免与主机MySQL端口冲突 + networks: + - app-network + command: --default-authentication-plugin=mysql_native_password + +networks: + app-network: + driver: bridge + +volumes: + mysql-data: diff --git a/docker/test.sh b/docker/test.sh new file mode 100755 index 0000000..c8cd3e9 --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +echo "🧪 测试Docker部署..." + +# 检查应用是否响应 +echo "⏳ 等待应用启动..." +sleep 10 + +# 测试应用是否可访问 +if curl -f http://localhost:50400 > /dev/null 2>&1; then + echo "✅ 应用运行正常!" + echo "🌐 访问地址: http://localhost:50400" +else + echo "❌ 应用无法访问,请检查日志:" + echo "docker-compose logs app" +fi + +# 显示当前运行的容器 +echo "" +echo "📋 当前运行的容器:" +docker-compose ps diff --git a/requirements.txt b/requirements.txt index 59aead6..0b68e57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ Flask-Mail==0.9.1 cos-python-sdk-v5==1.9.24 Pillow==10.0.1 python-magic==0.4.27 +gunicorn==20.1.0 diff --git a/update_config.py b/update_config.py new file mode 100644 index 0000000..ecb175b --- /dev/null +++ b/update_config.py @@ -0,0 +1,27 @@ +import os + +config_file = 'config/config.py' + +with open(config_file, 'r') as f: + content = f.read() + +# 修改数据库配置部分,使其支持环境变量 +modified_content = content.replace( + ''' # 数据库配置 + MYSQL_HOST = '27.124.22.104' + MYSQL_USER = 'taibai' + MYSQL_PASSWORD = 'taibaishopping' + MYSQL_DB = 'online_shopping' + MYSQL_PORT = 3306''', + ''' # 数据库配置 + MYSQL_HOST = os.environ.get('MYSQL_HOST', '27.124.22.104') + MYSQL_USER = os.environ.get('MYSQL_USER', 'taibai') + MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD', 'taibaishopping') + MYSQL_DB = os.environ.get('MYSQL_DB', 'online_shopping') + MYSQL_PORT = int(os.environ.get('MYSQL_PORT', '3306'))''' +) + +with open(config_file, 'w') as f: + f.write(modified_content) + +print(f"✅ 成功更新 {config_file} 以支持环境变量")