Compare commits
2 Commits
d1b5e8c5cf
...
170db69eb4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
170db69eb4 | ||
![]() |
feeca344da |
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
@ -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 = `
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<i class="bi bi-hourglass-split"></i> 提交中...';
|
||||
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 = '<i class="bi bi-check-circle"></i> 订单创建成功';
|
||||
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 = `
|
||||
<div class="text-center text-white">
|
||||
<div class="spinner-border mb-3" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<h5>正在创建订单...</h5>
|
||||
<p>请稍候,不要关闭页面</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<i class="bi ${icon} me-2"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 5秒后自动移除
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
@ -261,14 +261,14 @@
|
||||
<div class="mb-3">
|
||||
<label for="single_stock" class="form-label">库存数量</label>
|
||||
<input type="number" class="form-control" id="single_stock"
|
||||
name="single_stock" min="0" value="0">
|
||||
name="single_stock" min="0" value="{{ product.inventory[0].stock if product and product.inventory else 0 }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="warning_stock" class="form-label">预警库存</label>
|
||||
<input type="number" class="form-control" id="warning_stock"
|
||||
name="warning_stock" min="0" value="10">
|
||||
name="warning_stock" min="0" value="{{ product.inventory[0].warning_stock if product and product.inventory else 10 }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,8 +106,8 @@
|
||||
<div class="col-md-6">
|
||||
<h6>联系我们</h6>
|
||||
<p class="text-muted">
|
||||
<i class="bi bi-envelope"></i> service@taibai-mall.com<br>
|
||||
<i class="bi bi-telephone"></i> 400-888-8888
|
||||
<i class="bi bi-envelope"></i> 392437483@qq.com<br>
|
||||
<i class="bi bi-telephone"></i> 17753825492
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -99,17 +99,25 @@
|
||||
|
||||
<!-- 数量 -->
|
||||
<div class="col-2 text-center">
|
||||
<div class="input-group" style="width: 100px; margin: 0 auto;">
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||||
<div class="quantity-control d-flex align-items-center justify-content-center">
|
||||
<button class="btn btn-outline-secondary btn-sm quantity-btn" type="button"
|
||||
onclick="changeQuantity({{ item.id }}, -1)"
|
||||
{% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}>-</button>
|
||||
<input type="number" class="form-control form-control-sm text-center quantity-input"
|
||||
value="{{ item.quantity }}" min="1" max="{{ item.get_stock() }}"
|
||||
data-cart-id="{{ item.id }}" onchange="updateQuantity({{ item.id }}, this.value)"
|
||||
{% if not item.is_available() or item.quantity <= 1 %}disabled{% endif %}>
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
<input type="text" class="form-control form-control-sm text-center quantity-input mx-1"
|
||||
value="{{ item.quantity }}"
|
||||
data-cart-id="{{ item.id }}"
|
||||
data-max-stock="{{ item.get_stock() }}"
|
||||
oninput="validateQuantityInput(this)"
|
||||
onblur="updateQuantityFromInput({{ item.id }}, this)"
|
||||
onkeydown="handleQuantityKeyPress(event, {{ item.id }}, this)"
|
||||
{% if not item.is_available() %}disabled{% endif %}>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button"
|
||||
<button class="btn btn-outline-secondary btn-sm quantity-btn" type="button"
|
||||
onclick="changeQuantity({{ item.id }}, 1)"
|
||||
{% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}>+</button>
|
||||
{% if not item.is_available() or item.quantity >= item.get_stock() %}disabled{% endif %}>
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<small class="text-muted">小计:¥<span class="item-total">{{ "%.2f"|format(item.get_total_price()) }}</span></small>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -20,33 +21,68 @@
|
||||
<!-- 收货地址 -->
|
||||
<div class="card checkout-section">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="bi bi-geo-alt"></i> 收货地址</h5>
|
||||
<a href="{{ url_for('address.add') }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-plus"></i> 新增地址
|
||||
<h5><i class="bi bi-geo-alt-fill"></i> 收货地址</h5>
|
||||
<a href="{{ url_for('address.add') }}" class="btn add-address-btn">
|
||||
<i class="bi bi-plus-circle"></i> 新增地址
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row" id="addressList">
|
||||
<div class="address-container" id="addressList">
|
||||
{% for address in addresses %}
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card address-card {% if address.is_default %}selected{% endif %}"
|
||||
data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">{{ address.receiver_name }}</h6>
|
||||
<p class="text-muted mb-1">{{ address.receiver_phone }}</p>
|
||||
<p class="mb-0">{{ address.get_full_address() }}</p>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="address_id"
|
||||
value="{{ address.id }}" {% if address.is_default %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="address-card {% if address.is_default %}selected{% endif %}"
|
||||
data-address-id="{{ address.id }}" onclick="selectAddress({{ address.id }})">
|
||||
<div class="address-card-body">
|
||||
<div class="address-header">
|
||||
<div class="address-receiver">
|
||||
<h6 class="receiver-name">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
{{ address.receiver_name }}
|
||||
</h6>
|
||||
<p class="receiver-phone">
|
||||
<i class="bi bi-telephone-fill"></i>
|
||||
{{ address.receiver_phone }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="custom-radio">
|
||||
<input type="radio" name="address_id" value="{{ address.id }}"
|
||||
{% if address.is_default %}checked{% endif %}>
|
||||
<span class="radio-mark"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address-content">
|
||||
<p class="address-text">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
{{ address.get_full_address() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="address-footer">
|
||||
{% if address.is_default %}
|
||||
<span class="default-badge">
|
||||
<i class="bi bi-star-fill"></i>
|
||||
默认地址
|
||||
</span>
|
||||
{% else %}
|
||||
<span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not addresses %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-4">
|
||||
<i class="bi bi-geo-alt text-muted" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mt-3">暂无收货地址</h5>
|
||||
<p class="text-muted">请先添加收货地址,然后继续下单</p>
|
||||
<a href="{{ url_for('address.add') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> 立即添加
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -54,7 +90,7 @@
|
||||
<!-- 商品信息 -->
|
||||
<div class="card checkout-section">
|
||||
<div class="card-header">
|
||||
<h5><i class="bi bi-box"></i> 商品信息</h5>
|
||||
<h5><i class="bi bi-box-seam"></i> 商品信息</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for item in cart_items %}
|
||||
|
@ -65,7 +65,7 @@ def send_verification_email(email, code, code_type):
|
||||
|
||||
<div style="text-align: center; color: #666; font-size: 12px;">
|
||||
<p>此邮件由系统自动发送,请勿回复。</p>
|
||||
<p>© 2024 太白购物平台 版权所有</p>
|
||||
<p>© 2025 太白购物平台 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -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"]
|
72
docker/README.md
Normal file
72
docker/README.md
Normal file
@ -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
|
41
docker/deploy.sh
Executable file
41
docker/deploy.sh
Executable file
@ -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"
|
@ -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:
|
21
docker/test.sh
Executable file
21
docker/test.sh
Executable file
@ -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
|
@ -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
|
||||
|
27
update_config.py
Normal file
27
update_config.py
Normal file
@ -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} 以支持环境变量")
|
Loading…
x
Reference in New Issue
Block a user