================================================================================ 项目代码导出文件 ================================================================================ 项目名称: 基于Python的线上电商系统 导出时间: 2025-07-04 03:35:45 项目路径: /Users/lishunqin/Desktop/Online_shopping_platform 文件总数: 83 ================================================================================ 📁 文件目录: -------------------------------------------------- README.md (0.0 KB) app/__init__.py (2.5 KB) app/forms.py (5.2 KB) app/models/__init__.py (0.8 KB) app/models/address.py (2.8 KB) app/models/admin.py (1.8 KB) app/models/cart.py (4.5 KB) app/models/operation_log.py (1.8 KB) app/models/order.py (6.7 KB) app/models/payment.py (2.3 KB) app/models/product.py (9.4 KB) app/models/review.py (2.2 KB) app/models/user.py (1.7 KB) app/models/verification.py (1.8 KB) app/static/js/city_data.js (54.3 KB) app/templates/admin/base.html (7.7 KB) app/templates/admin/categories.html (26.3 KB) app/templates/admin/dashboard.html (7.9 KB) app/templates/admin/login.html (4.5 KB) app/templates/admin/orders.html (0.0 KB) app/templates/admin/product_form.html (32.9 KB) app/templates/admin/products.html (17.6 KB) app/templates/admin/profile.html (5.6 KB) app/templates/admin/users.html (0.0 KB) app/templates/base.html (11.4 KB) app/templates/cart/index.html (16.2 KB) app/templates/common/footer.html (0.0 KB) app/templates/common/header.html (0.0 KB) app/templates/common/pagination.html (0.0 KB) app/templates/index.html (9.5 KB) app/templates/order/checkout.html (14.9 KB) app/templates/order/detail.html (14.0 KB) app/templates/order/pay.html (10.5 KB) app/templates/product/detail.html (24.7 KB) app/templates/product/list.html (13.6 KB) app/templates/test_upload.html (14.5 KB) app/templates/user/address_form.html (14.4 KB) app/templates/user/addresses.html (7.4 KB) app/templates/user/login.html (2.3 KB) app/templates/user/orders.html (13.9 KB) app/templates/user/profile.html (26.5 KB) app/templates/user/register.html (9.4 KB) app/utils/__init__.py (0.0 KB) app/utils/auth.py (0.0 KB) app/utils/cos_client.py (7.7 KB) app/utils/cos_upload.py (0.0 KB) app/utils/database.py (1.0 KB) app/utils/decorators.py (10.0 KB) app/utils/email_service.py (2.4 KB) app/utils/file_upload.py (12.4 KB) app/utils/helpers.py (0.0 KB) app/utils/sms.py (0.0 KB) app/utils/wechat_pay.py (0.0 KB) app/views/__init__.py (0.0 KB) app/views/address.py (8.0 KB) app/views/admin.py (7.7 KB) app/views/auth.py (4.8 KB) app/views/cart.py (7.4 KB) app/views/main.py (6.4 KB) app/views/order.py (10.7 KB) app/views/payment.py (6.6 KB) app/views/product.py (23.2 KB) app/views/upload.py (5.3 KB) app/views/user.py (1.0 KB) check_avatar.py (0.6 KB) config/__init__.py (0.0 KB) config/config.py (1.5 KB) config/cos_config.py (2.7 KB) config/database.py (0.8 KB) create_admin.py (5.2 KB) create_sample_categories.py (4.7 KB) create_sample_specs.py (2.0 KB) create_test_order.py (2.8 KB) create_test_user.py (2.0 KB) docker/.dockerignore (0.0 KB) docker/Dockerfile (0.0 KB) docker/docker-compose.yml (0.0 KB) export_code.py (9.9 KB) requirements.txt (0.2 KB) run.py (0.2 KB) test_cos_connection.py (6.5 KB) test_db_connection.py (3.3 KB) test_email_detailed.py (8.3 KB) ================================================================================ 🔸============================================================================== 📄 文件: README.md 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/__init__.py 📊 大小: 2545 bytes (2.49 KB) 🕒 修改时间: 2025-07-04 02:28:26 🔸============================================================================== """ Flask应用工厂 """ from flask import Flask from flask_mail import Mail from config.config import Config from config.database import db import re # 初始化邮件服务 mail = Mail() def create_app(config_name='default'): app = Flask(__name__) # 加载配置 app.config.from_object(Config) # 初始化数据库 db.init_app(app) # 初始化邮件服务 mail.init_app(app) # 注册自定义过滤器 register_filters(app) # 注册蓝图 register_blueprints(app) # 创建数据库表 with app.app_context(): try: db.create_all() print("✅ 数据库表创建/同步成功") except Exception as e: print(f"❌ 数据库表创建失败: {str(e)}") return app def register_filters(app): """注册自定义过滤器""" @app.template_filter('nl2br') def nl2br_filter(text): """将换行符转换为HTML
标签""" if not text: return '' # 将换行符替换为
标签 return text.replace('\n', '
') @app.template_filter('truncate_chars') def truncate_chars_filter(text, length=50): """截断字符串""" if not text: return '' if len(text) <= length: return text return text[:length] + '...' def register_blueprints(app): """注册蓝图""" from app.views.main import main_bp from app.views.auth import auth_bp from app.views.user import user_bp from app.views.admin import admin_bp from app.views.product import product_bp from app.views.cart import cart_bp from app.views.address import address_bp from app.views.order import order_bp from app.views.payment import payment_bp app.register_blueprint(main_bp) app.register_blueprint(auth_bp) app.register_blueprint(user_bp) app.register_blueprint(admin_bp) app.register_blueprint(product_bp) app.register_blueprint(cart_bp) app.register_blueprint(address_bp) app.register_blueprint(order_bp) app.register_blueprint(payment_bp) # 修复:正确注册upload蓝图并设置URL前缀 try: from app.views.upload import upload_bp app.register_blueprint(upload_bp, url_prefix='/upload') # 添加URL前缀 print("✅ 上传功能蓝图注册成功") except ImportError as e: print(f"⚠️ 上传功能暂时不可用: {str(e)}") print("✅ 商品管理蓝图注册成功") print("✅ 购物车蓝图注册成功") 🔸============================================================================== 📄 文件: app/forms.py 📊 大小: 5318 bytes (5.19 KB) 🕒 修改时间: 2025-07-04 03:19:30 🔸============================================================================== from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Length, Email, ValidationError, Regexp, EqualTo from app.models.user import User from wtforms import TextAreaField, SelectField, DecimalField, IntegerField, HiddenField class LoginForm(FlaskForm): username = StringField('用户名/手机号/邮箱', validators=[ DataRequired(message='请输入用户名、手机号或邮箱'), Length(min=3, max=50, message='长度必须在3-50个字符之间') ]) password = PasswordField('密码', validators=[ DataRequired(message='请输入密码'), Length(min=6, max=20, message='密码长度必须在6-20个字符之间') ]) remember_me = BooleanField('记住我') submit = SubmitField('登录') class RegisterForm(FlaskForm): username = StringField('用户名', validators=[ DataRequired(message='请输入用户名'), Length(min=3, max=20, message='用户名长度必须在3-20个字符之间'), Regexp(r'^[a-zA-Z0-9_]+$', message='用户名只能包含字母、数字和下划线') ]) email = StringField('邮箱', validators=[ DataRequired(message='请输入邮箱'), Email(message='请输入有效的邮箱地址') ]) email_code = StringField('邮箱验证码', validators=[ DataRequired(message='请输入邮箱验证码'), Length(min=6, max=6, message='验证码为6位数字') ]) phone = StringField('手机号', validators=[ DataRequired(message='请输入手机号'), Regexp(r'^1[3-9]\d{9}$', message='请输入有效的手机号') ]) password = PasswordField('密码', validators=[ DataRequired(message='请输入密码'), Length(min=6, max=20, message='密码长度必须在6-20个字符之间'), Regexp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{6,}$', message='密码必须包含至少一个字母和一个数字') ]) confirm_password = PasswordField('确认密码', validators=[ DataRequired(message='请确认密码'), EqualTo('password', message='两次输入的密码不一致') ]) submit = SubmitField('注册') def validate_username(self, username): user = User.query.filter_by(username=username.data).first() if user: raise ValidationError('用户名已存在') def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user: raise ValidationError('邮箱已被注册') def validate_phone(self, phone): user = User.query.filter_by(phone=phone.data).first() if user: raise ValidationError('手机号已被注册') class SendEmailCodeForm(FlaskForm): """发送邮箱验证码表单""" email = StringField('邮箱', validators=[ DataRequired(message='请输入邮箱'), Email(message='请输入有效的邮箱地址') ]) submit = SubmitField('发送验证码') class AddressForm(FlaskForm): """地址表单""" receiver_name = StringField('收货人', validators=[ DataRequired(message='请输入收货人姓名'), Length(min=2, max=20, message='收货人姓名长度必须在2-20个字符之间') ]) receiver_phone = StringField('手机号', validators=[ DataRequired(message='请输入手机号'), Regexp(r'^1[3-9]\d{9}$', message='请输入有效的手机号') ]) province = SelectField('省份', validators=[ DataRequired(message='请选择省份') ], choices=[]) city = SelectField('城市', validators=[ DataRequired(message='请选择城市') ], choices=[]) district = SelectField('区县', validators=[ DataRequired(message='请选择区县') ], choices=[]) detail_address = StringField('详细地址', validators=[ DataRequired(message='请输入详细地址'), Length(min=5, max=200, message='详细地址长度必须在5-200个字符之间') ]) postal_code = StringField('邮政编码', validators=[ Length(max=10, message='邮政编码长度不能超过10个字符') ]) is_default = BooleanField('设为默认地址') submit = SubmitField('保存地址') class CheckoutForm(FlaskForm): """结算表单""" address_id = SelectField('收货地址', validators=[ DataRequired(message='请选择收货地址') ], coerce=int, choices=[]) shipping_method = SelectField('配送方式', validators=[ DataRequired(message='请选择配送方式') ], choices=[ ('standard', '标准配送(免费)'), ('express', '次日达(+10元)'), ('same_day', '当日达(+20元)') ], default='standard') payment_method = SelectField('支付方式', validators=[ DataRequired(message='请选择支付方式') ], choices=[ ('wechat', '微信支付'), ('alipay', '支付宝'), ('bank', '银行卡支付') ], default='wechat') remark = TextAreaField('订单备注', validators=[ Length(max=200, message='备注长度不能超过200个字符') ]) selected_items = HiddenField('选中商品') submit = SubmitField('提交订单') 🔸============================================================================== 📄 文件: app/models/__init__.py 📊 大小: 822 bytes (0.80 KB) 🕒 修改时间: 2025-07-04 01:56:55 🔸============================================================================== from app.models.user import User from app.models.verification import EmailVerification from app.models.admin import AdminUser from app.models.operation_log import OperationLog from app.models.product import Category, Product, ProductImage, SpecName, SpecValue, ProductInventory, InventoryLog, ProductSpecRelation from app.models.cart import Cart from app.models.address import UserAddress from app.models.order import Order, OrderItem, ShippingInfo from app.models.payment import Payment from app.models.review import Review __all__ = [ 'User', 'EmailVerification', 'AdminUser', 'OperationLog', 'Category', 'Product', 'ProductImage', 'SpecName', 'SpecValue', 'ProductInventory', 'InventoryLog', 'ProductSpecRelation', 'Cart', 'UserAddress', 'Order', 'OrderItem', 'ShippingInfo', 'Payment', 'Review' ] 🔸============================================================================== 📄 文件: app/models/address.py 📊 大小: 2868 bytes (2.80 KB) 🕒 修改时间: 2025-07-04 01:56:15 🔸============================================================================== """ 用户地址模型 """ from datetime import datetime from config.database import db class UserAddress(db.Model): """用户地址模型""" __tablename__ = 'user_addresses' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) receiver_name = db.Column(db.String(50), nullable=False) receiver_phone = db.Column(db.String(20), nullable=False) province = db.Column(db.String(50), nullable=False) city = db.Column(db.String(50), nullable=False) district = db.Column(db.String(50), nullable=False) detail_address = db.Column(db.String(200), nullable=False) postal_code = db.Column(db.String(10)) is_default = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='addresses') def get_full_address(self): """获取完整地址""" return f"{self.province} {self.city} {self.district} {self.detail_address}" def to_dict(self): """转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'receiver_name': self.receiver_name, 'receiver_phone': self.receiver_phone, 'province': self.province, 'city': self.city, 'district': self.district, 'detail_address': self.detail_address, 'postal_code': self.postal_code, 'full_address': self.get_full_address(), 'is_default': self.is_default, 'created_at': self.created_at.isoformat() if self.created_at else None } @classmethod def set_default_address(cls, user_id, address_id): """设置默认地址""" try: # 先取消所有默认地址 cls.query.filter_by(user_id=user_id).update({'is_default': 0}) # 设置新的默认地址 address = cls.query.filter_by(id=address_id, user_id=user_id).first() if address: address.is_default = 1 db.session.commit() return True return False except Exception: db.session.rollback() return False @classmethod def get_default_address(cls, user_id): """获取默认地址""" return cls.query.filter_by(user_id=user_id, is_default=1).first() @classmethod def get_user_addresses(cls, user_id): """获取用户所有地址""" return cls.query.filter_by(user_id=user_id).order_by( cls.is_default.desc(), cls.created_at.desc() ).all() def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/admin.py 📊 大小: 1814 bytes (1.77 KB) 🕒 修改时间: 2025-07-03 05:49:59 🔸============================================================================== """ 管理员模型 """ from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from config.database import db class AdminUser(db.Model): __tablename__ = 'admin_users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) real_name = db.Column(db.String(50)) email = db.Column(db.String(100)) phone = db.Column(db.String(20)) role = db.Column(db.String(20), default='admin') status = db.Column(db.Integer, default=1) # 0-禁用 1-正常 last_login_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def set_password(self, password): """设置密码""" self.password_hash = generate_password_hash(password) def check_password(self, password): """验证密码""" return check_password_hash(self.password_hash, password) def update_last_login(self): """更新最后登录时间""" self.last_login_at = datetime.utcnow() db.session.commit() def to_dict(self): """转换为字典""" return { 'id': self.id, 'username': self.username, 'real_name': self.real_name, 'email': self.email, 'phone': self.phone, 'role': self.role, 'status': self.status, 'last_login_at': self.last_login_at.isoformat() if self.last_login_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/cart.py 📊 大小: 4608 bytes (4.50 KB) 🕒 修改时间: 2025-07-03 15:24:11 🔸============================================================================== """ 购物车模型 """ from datetime import datetime from config.database import db from app.models.product import Product, ProductInventory class Cart(db.Model): """购物车模型""" __tablename__ = 'cart' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) sku_code = db.Column(db.String(100)) spec_combination = db.Column(db.String(255)) quantity = db.Column(db.Integer, nullable=False, default=1) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='cart_items') product = db.relationship('Product', backref='cart_items') def get_sku_info(self): """获取SKU信息""" if self.sku_code: return ProductInventory.query.filter_by(sku_code=self.sku_code).first() else: # 如果没有SKU,返回默认库存信息 return ProductInventory.query.filter_by( product_id=self.product_id, is_default=1 ).first() def get_price(self): """获取商品价格""" sku_info = self.get_sku_info() if sku_info: return sku_info.get_final_price() return float(self.product.price) if self.product and self.product.price else 0 def get_total_price(self): """获取小计金额""" return self.get_price() * self.quantity def get_stock(self): """获取库存数量""" sku_info = self.get_sku_info() return sku_info.stock if sku_info else 0 def is_available(self): """检查商品是否可用""" # 检查商品是否上架 if not self.product or self.product.status != 1: return False # 检查库存 if self.get_stock() < self.quantity: return False return True def to_dict(self): """转换为字典""" sku_info = self.get_sku_info() return { 'id': self.id, 'user_id': self.user_id, 'product_id': self.product_id, 'product_name': self.product.name if self.product else '', 'product_image': self.product.main_image if self.product else '', 'brand': self.product.brand if self.product else '', 'sku_code': self.sku_code, 'spec_combination': self.spec_combination, 'quantity': self.quantity, 'price': self.get_price(), 'total_price': self.get_total_price(), 'stock': self.get_stock(), 'is_available': self.is_available(), 'created_at': self.created_at.isoformat() if self.created_at else None } @classmethod def add_to_cart(cls, user_id, product_id, sku_code=None, spec_combination=None, quantity=1): """添加商品到购物车""" # 检查是否已存在相同商品 existing_item = cls.query.filter_by( user_id=user_id, product_id=product_id, sku_code=sku_code ).first() if existing_item: # 更新数量 existing_item.quantity += quantity existing_item.updated_at = datetime.utcnow() db.session.commit() return existing_item else: # 创建新记录 cart_item = cls( user_id=user_id, product_id=product_id, sku_code=sku_code, spec_combination=spec_combination, quantity=quantity ) db.session.add(cart_item) db.session.commit() return cart_item @classmethod def get_user_cart(cls, user_id): """获取用户购物车""" return cls.query.filter_by(user_id=user_id)\ .order_by(cls.created_at.desc()).all() @classmethod def get_cart_count(cls, user_id): """获取购物车商品数量""" return cls.query.filter_by(user_id=user_id).count() @classmethod def get_cart_total(cls, user_id): """获取购物车总金额""" cart_items = cls.get_user_cart(user_id) total = 0 for item in cart_items: if item.is_available(): total += item.get_total_price() return total def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/operation_log.py 📊 大小: 1850 bytes (1.81 KB) 🕒 修改时间: 2025-07-03 05:50:29 🔸============================================================================== """ 操作日志模型 """ from datetime import datetime from config.database import db import json class OperationLog(db.Model): __tablename__ = 'operation_logs' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer) user_type = db.Column(db.Integer) # 1-普通用户 2-管理员 action = db.Column(db.String(100), nullable=False) resource_type = db.Column(db.String(50)) resource_id = db.Column(db.Integer) ip_address = db.Column(db.String(45)) user_agent = db.Column(db.Text) request_data = db.Column(db.JSON) created_at = db.Column(db.DateTime, default=datetime.utcnow) @classmethod def create_log(cls, user_id=None, user_type=1, action='', resource_type=None, resource_id=None, ip_address=None, user_agent=None, request_data=None): """创建操作日志""" log = cls( user_id=user_id, user_type=user_type, action=action, resource_type=resource_type, resource_id=resource_id, ip_address=ip_address, user_agent=user_agent, request_data=request_data ) db.session.add(log) db.session.commit() return log def to_dict(self): """转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'user_type': self.user_type, 'action': self.action, 'resource_type': self.resource_type, 'resource_id': self.resource_id, 'ip_address': self.ip_address, 'user_agent': self.user_agent, 'request_data': self.request_data, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/order.py 📊 大小: 6846 bytes (6.69 KB) 🕒 修改时间: 2025-07-04 02:50:35 🔸============================================================================== """ 订单模型 """ from datetime import datetime, timedelta import json from config.database import db from app.models.user import User from app.models.product import Product class Order(db.Model): """订单模型""" __tablename__ = 'orders' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) order_sn = db.Column(db.String(50), unique=True, nullable=False) total_amount = db.Column(db.Numeric(10, 2), nullable=False) actual_amount = db.Column(db.Numeric(10, 2), nullable=False) shipping_fee = db.Column(db.Numeric(10, 2), default=0) status = db.Column(db.Integer, default=1) # 1-待支付 2-待发货 3-待收货 4-待评价 5-已完成 6-已取消 7-退款中 payment_method = db.Column(db.String(20)) shipping_method = db.Column(db.String(50)) receiver_info = db.Column(db.Text) # JSON格式存储收货人信息 remark = db.Column(db.Text) shipped_at = db.Column(db.DateTime) received_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='orders') order_items = db.relationship('OrderItem', backref='order', cascade='all, delete-orphan') # 状态常量 STATUS_PENDING_PAYMENT = 1 # 待支付 STATUS_PENDING_SHIPMENT = 2 # 待发货 STATUS_SHIPPED = 3 # 待收货 STATUS_PENDING_REVIEW = 4 # 待评价 STATUS_COMPLETED = 5 # 已完成 STATUS_CANCELLED = 6 # 已取消 STATUS_REFUNDING = 7 # 退款中 STATUS_CHOICES = { STATUS_PENDING_PAYMENT: '待支付', STATUS_PENDING_SHIPMENT: '待发货', STATUS_SHIPPED: '待收货', STATUS_PENDING_REVIEW: '待评价', STATUS_COMPLETED: '已完成', STATUS_CANCELLED: '已取消', STATUS_REFUNDING: '退款中' } def get_status_text(self): """获取状态文本""" return self.STATUS_CHOICES.get(self.status, '未知状态') def get_receiver_info(self): """获取收货人信息""" if self.receiver_info: try: return json.loads(self.receiver_info) except: return {} return {} def set_receiver_info(self, info): """设置收货人信息""" if isinstance(info, dict): self.receiver_info = json.dumps(info, ensure_ascii=False) def is_expired(self): """检查订单是否已过期(15分钟未支付)""" if self.status == self.STATUS_PENDING_PAYMENT: expire_time = self.created_at + timedelta(minutes=15) return datetime.utcnow() > expire_time return False def can_cancel(self): """检查是否可以取消""" return self.status in [self.STATUS_PENDING_PAYMENT, self.STATUS_PENDING_SHIPMENT] def can_pay(self): """检查是否可以支付""" return self.status == self.STATUS_PENDING_PAYMENT and not self.is_expired() def can_confirm_receipt(self): """检查是否可以确认收货""" return self.status == self.STATUS_SHIPPED def to_dict(self): """转换为字典""" return { 'id': self.id, 'order_sn': self.order_sn, 'total_amount': float(self.total_amount), 'actual_amount': float(self.actual_amount), 'shipping_fee': float(self.shipping_fee), 'status': self.status, 'status_text': self.get_status_text(), 'payment_method': self.payment_method, 'shipping_method': self.shipping_method, 'receiver_info': self.get_receiver_info(), 'remark': self.remark, 'can_cancel': self.can_cancel(), 'can_pay': self.can_pay(), 'can_confirm_receipt': self.can_confirm_receipt(), 'is_expired': self.is_expired(), 'created_at': self.created_at.isoformat() if self.created_at else None, 'shipped_at': self.shipped_at.isoformat() if self.shipped_at else None, 'received_at': self.received_at.isoformat() if self.received_at else None } @classmethod def generate_order_sn(cls): """生成订单号""" import time import random timestamp = str(int(time.time())) random_str = str(random.randint(100000, 999999)) return f"TB{timestamp}{random_str}" def __repr__(self): return f'' class OrderItem(db.Model): """订单商品明细模型""" __tablename__ = 'order_items' id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) sku_code = db.Column(db.String(100)) product_name = db.Column(db.String(200), nullable=False) product_image = db.Column(db.String(255)) spec_combination = db.Column(db.String(255)) price = db.Column(db.Numeric(10, 2), nullable=False) quantity = db.Column(db.Integer, nullable=False) total_price = db.Column(db.Numeric(10, 2), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关联关系 product = db.relationship('Product', backref='order_items') def to_dict(self): """转换为字典""" return { 'id': self.id, 'product_id': self.product_id, 'product_name': self.product_name, 'product_image': self.product_image, 'spec_combination': self.spec_combination, 'price': float(self.price), 'quantity': self.quantity, 'total_price': float(self.total_price) } def __repr__(self): return f'' class ShippingInfo(db.Model): """物流信息模型""" __tablename__ = 'shipping_info' id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) shipping_company = db.Column(db.String(50)) tracking_number = db.Column(db.String(100)) shipping_status = db.Column(db.Integer, default=1) # 1-已发货 2-运输中 3-已送达 shipping_address = db.Column(db.Text) estimated_delivery = db.Column(db.DateTime) actual_delivery = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 order = db.relationship('Order', backref='shipping_info') def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/payment.py 📊 大小: 2370 bytes (2.31 KB) 🕒 修改时间: 2025-07-04 01:56:46 🔸============================================================================== """ 支付模型 """ from datetime import datetime from config.database import db class Payment(db.Model): """支付记录模型""" __tablename__ = 'payments' id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) payment_sn = db.Column(db.String(64), unique=True, nullable=False) payment_method = db.Column(db.String(20), nullable=False) amount = db.Column(db.Numeric(10, 2), nullable=False) status = db.Column(db.Integer, default=1) # 1-待支付 2-支付成功 3-支付失败 4-已退款 third_party_sn = db.Column(db.String(100)) # 第三方支付流水号 callback_data = db.Column(db.Text) # 支付回调数据 paid_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 关联关系 order = db.relationship('Order', backref='payments') # 状态常量 STATUS_PENDING = 1 # 待支付 STATUS_SUCCESS = 2 # 支付成功 STATUS_FAILED = 3 # 支付失败 STATUS_REFUNDED = 4 # 已退款 STATUS_CHOICES = { STATUS_PENDING: '待支付', STATUS_SUCCESS: '支付成功', STATUS_FAILED: '支付失败', STATUS_REFUNDED: '已退款' } def get_status_text(self): """获取状态文本""" return self.STATUS_CHOICES.get(self.status, '未知状态') def to_dict(self): """转换为字典""" return { 'id': self.id, 'payment_sn': self.payment_sn, 'payment_method': self.payment_method, 'amount': float(self.amount), 'status': self.status, 'status_text': self.get_status_text(), 'third_party_sn': self.third_party_sn, 'paid_at': self.paid_at.isoformat() if self.paid_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None } @classmethod def generate_payment_sn(cls): """生成支付流水号""" import time import random timestamp = str(int(time.time())) random_str = str(random.randint(100000, 999999)) return f"PAY{timestamp}{random_str}" def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/product.py 📊 大小: 9669 bytes (9.44 KB) 🕒 修改时间: 2025-07-03 07:03:27 🔸============================================================================== """ 商品相关模型 """ from datetime import datetime from config.database import db import json class Category(db.Model): """商品分类模型""" __tablename__ = 'categories' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) parent_id = db.Column(db.Integer, default=0) level = db.Column(db.Integer, default=1) sort_order = db.Column(db.Integer, default=0) icon_url = db.Column(db.String(255)) is_active = db.Column(db.Integer, default=1) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def to_dict(self): return { 'id': self.id, 'name': self.name, 'parent_id': self.parent_id, 'level': self.level, 'sort_order': self.sort_order, 'icon_url': self.icon_url, 'is_active': self.is_active, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' class SpecName(db.Model): """规格名称模型""" __tablename__ = 'spec_names' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) sort_order = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=datetime.utcnow) def to_dict(self): return { 'id': self.id, 'name': self.name, 'sort_order': self.sort_order } def __repr__(self): return f'' class SpecValue(db.Model): """规格值模型""" __tablename__ = 'spec_values' id = db.Column(db.Integer, primary_key=True) spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False) value = db.Column(db.String(100), nullable=False) sort_order = db.Column(db.Integer, default=0) created_at = db.Column(db.DateTime, default=datetime.utcnow) spec_name = db.relationship('SpecName', backref='values') def to_dict(self): return { 'id': self.id, 'spec_name_id': self.spec_name_id, 'value': self.value, 'sort_order': self.sort_order } def __repr__(self): return f'' class Product(db.Model): """商品模型""" __tablename__ = 'products' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(200), nullable=False) category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False) brand = db.Column(db.String(100)) price = db.Column(db.Numeric(10, 2), nullable=False) original_price = db.Column(db.Numeric(10, 2)) description = db.Column(db.Text) main_image = db.Column(db.String(255)) status = db.Column(db.Integer, default=1) # 0-下架 1-上架 has_specs = db.Column(db.Integer, default=0) # 0-无规格 1-有规格 sales_count = db.Column(db.Integer, default=0) view_count = db.Column(db.Integer, default=0) weight = db.Column(db.Numeric(8, 2)) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) category = db.relationship('Category', backref='products') def to_dict(self): return { 'id': self.id, 'name': self.name, 'category_id': self.category_id, 'category_name': self.category.name if self.category else '', 'brand': self.brand, 'price': float(self.price) if self.price else 0, 'original_price': float(self.original_price) if self.original_price else None, 'description': self.description, 'main_image': self.main_image, 'status': self.status, 'has_specs': self.has_specs, 'sales_count': self.sales_count, 'view_count': self.view_count, 'weight': float(self.weight) if self.weight else None, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } def __repr__(self): return f'' class ProductImage(db.Model): """商品图片模型""" __tablename__ = 'product_images' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) image_url = db.Column(db.String(255), nullable=False) sort_order = db.Column(db.Integer, default=0) is_main = db.Column(db.Integer, default=0) # 0-否 1-是 created_at = db.Column(db.DateTime, default=datetime.utcnow) product = db.relationship('Product', backref='images') def to_dict(self): return { 'id': self.id, 'product_id': self.product_id, 'image_url': self.image_url, 'sort_order': self.sort_order, 'is_main': self.is_main, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' class ProductSpecRelation(db.Model): """商品规格关联模型""" __tablename__ = 'product_spec_relations' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) spec_name_id = db.Column(db.Integer, db.ForeignKey('spec_names.id'), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) product = db.relationship('Product', backref='spec_relations') spec_name = db.relationship('SpecName') def __repr__(self): return f'' class ProductInventory(db.Model): """商品库存模型(SKU)""" __tablename__ = 'product_inventory' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) sku_code = db.Column(db.String(100), unique=True, nullable=False) spec_combination = db.Column(db.JSON) price_adjustment = db.Column(db.Numeric(10, 2), default=0) stock = db.Column(db.Integer, nullable=False, default=0) warning_stock = db.Column(db.Integer, default=10) is_default = db.Column(db.Integer, default=0) status = db.Column(db.Integer, default=1) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) product = db.relationship('Product', backref='inventory') def get_final_price(self): """获取最终价格""" base_price = float(self.product.price) if self.product and self.product.price else 0 adjustment = float(self.price_adjustment) if self.price_adjustment else 0 return base_price + adjustment def to_dict(self): return { 'id': self.id, 'product_id': self.product_id, 'sku_code': self.sku_code, 'spec_combination': self.spec_combination, 'price_adjustment': float(self.price_adjustment) if self.price_adjustment else 0, 'final_price': self.get_final_price(), 'stock': self.stock, 'warning_stock': self.warning_stock, 'is_default': self.is_default, 'status': self.status, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' class InventoryLog(db.Model): """库存变更日志模型""" __tablename__ = 'inventory_logs' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) sku_code = db.Column(db.String(100), nullable=False) change_type = db.Column(db.Integer, nullable=False) # 1-入库 2-出库 3-调整 change_quantity = db.Column(db.Integer, nullable=False) before_stock = db.Column(db.Integer, nullable=False) after_stock = db.Column(db.Integer, nullable=False) related_order_id = db.Column(db.Integer) remark = db.Column(db.String(255)) created_at = db.Column(db.DateTime, default=datetime.utcnow) product = db.relationship('Product') @classmethod def create_log(cls, product_id, sku_code, change_type, change_quantity, before_stock, after_stock, related_order_id=None, remark=None): """创建库存变更日志""" log = cls( product_id=product_id, sku_code=sku_code, change_type=change_type, change_quantity=change_quantity, before_stock=before_stock, after_stock=after_stock, related_order_id=related_order_id, remark=remark ) db.session.add(log) db.session.commit() return log def to_dict(self): return { 'id': self.id, 'product_id': self.product_id, 'sku_code': self.sku_code, 'change_type': self.change_type, 'change_quantity': self.change_quantity, 'before_stock': self.before_stock, 'after_stock': self.after_stock, 'related_order_id': self.related_order_id, 'remark': self.remark, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/review.py 📊 大小: 2204 bytes (2.15 KB) 🕒 修改时间: 2025-07-04 02:45:14 🔸============================================================================== """ 评价模型 """ from datetime import datetime import json from config.database import db class Review(db.Model): """商品评价模型""" __tablename__ = 'reviews' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False) order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False) rating = db.Column(db.Integer, nullable=False) # 1-5星 content = db.Column(db.Text) images = db.Column(db.Text) # JSON格式存储图片URLs is_anonymous = db.Column(db.Integer, default=0) status = db.Column(db.Integer, default=1) # 0-隐藏 1-显示 created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关联关系 user = db.relationship('User', backref='reviews') product = db.relationship('Product', backref='reviews') order = db.relationship('Order', backref='reviews') def get_images(self): """获取评价图片列表""" if self.images: try: return json.loads(self.images) except: return [] return [] def set_images(self, image_list): """设置评价图片""" if isinstance(image_list, list): self.images = json.dumps(image_list) def get_rating_stars(self): """获取星级显示""" return '★' * self.rating + '☆' * (5 - self.rating) def to_dict(self): """转换为字典""" return { 'id': self.id, 'user_id': self.user_id, 'username': self.user.username if not self.is_anonymous else '匿名用户', 'product_id': self.product_id, 'order_id': self.order_id, 'rating': self.rating, 'rating_stars': self.get_rating_stars(), 'content': self.content, 'images': self.get_images(), 'is_anonymous': self.is_anonymous, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/user.py 📊 大小: 1785 bytes (1.74 KB) 🕒 修改时间: 2025-07-03 04:43:31 🔸============================================================================== """ 用户模型 """ from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from config.database import db # 确保从正确位置导入 class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) phone = db.Column(db.String(20), unique=True) email = db.Column(db.String(100), unique=True) password_hash = db.Column(db.String(255), nullable=False) nickname = db.Column(db.String(50)) avatar_url = db.Column(db.String(255)) gender = db.Column(db.Integer, default=0) birthday = db.Column(db.Date) status = db.Column(db.Integer, default=1) wechat_openid = db.Column(db.String(100)) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def set_password(self, password): """设置密码""" self.password_hash = generate_password_hash(password) def check_password(self, password): """验证密码""" return check_password_hash(self.password_hash, password) def to_dict(self): """转换为字典""" return { 'id': self.id, 'username': self.username, 'phone': self.phone, 'email': self.email, 'nickname': self.nickname, 'avatar_url': self.avatar_url, 'gender': self.gender, 'birthday': self.birthday.isoformat() if self.birthday else None, 'status': self.status, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' 🔸============================================================================== 📄 文件: app/models/verification.py 📊 大小: 1832 bytes (1.79 KB) 🕒 修改时间: 2025-07-03 03:40:15 🔸============================================================================== from datetime import datetime, timedelta from config.database import db import random import string class EmailVerification(db.Model): __tablename__ = 'email_verifications' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(100), nullable=False, index=True) code = db.Column(db.String(6), nullable=False) type = db.Column(db.SmallInteger, nullable=False) # 1-注册 2-登录 3-找回密码 is_used = db.Column(db.SmallInteger, default=0) # 0-未使用 1-已使用 expired_at = db.Column(db.DateTime, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) @staticmethod def generate_code(): """生成6位数字验证码""" return ''.join(random.choices(string.digits, k=6)) @classmethod def create_verification(cls, email, code_type, expire_minutes=10): """创建验证码记录""" code = cls.generate_code() expired_at = datetime.utcnow() + timedelta(minutes=expire_minutes) verification = cls( email=email, code=code, type=code_type, expired_at=expired_at ) db.session.add(verification) db.session.commit() return verification @classmethod def verify_code(cls, email, code, code_type): """验证验证码""" verification = cls.query.filter_by( email=email, code=code, type=code_type, is_used=0 ).filter( cls.expired_at > datetime.utcnow() ).first() if verification: verification.is_used = 1 db.session.commit() return True return False def is_expired(self): """检查是否过期""" return datetime.utcnow() > self.expired_at 🔸============================================================================== 📄 文件: app/static/js/city_data.js 📊 大小: 55620 bytes (54.32 KB) 🕒 修改时间: 2025-07-04 03:24:59 🔸============================================================================== // 中国省市区数据 const cityData = { '北京市': { '北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '门头沟区', '房山区', '通州区', '顺义区', '昌平区', '大兴区', '怀柔区', '平谷区', '密云区', '延庆区'] }, '上海市': { '上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区', '青浦区', '奉贤区', '崇明区'] }, '天津市': { '天津市': ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区', '滨海新区', '宁河区', '静海区', '蓟州区'] }, '重庆市': { '重庆市': ['万州区', '涪陵区', '渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '綦江区', '大足区', '渝北区', '巴南区', '黔江区', '长寿区', '江津区', '合川区', '永川区', '南川区', '璧山区', '铜梁区', '潼南区', '荣昌区', '开州区', '梁平区', '武隆区', '城口县', '丰都县', '垫江县', '忠县', '云阳县', '奉节县', '巫山县', '巫溪县', '石柱土家族自治县', '秀山土家族苗族自治县', '酉阳土家族苗族自治县', '彭水苗族土家族自治县'] }, '河北省': { '石家庄市': ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区', '井陉县', '正定县', '行唐县', '灵寿县', '高邑县', '深泽县', '赞皇县', '无极县', '平山县', '元氏县', '赵县', '辛集市', '晋州市', '新乐市'], '唐山市': ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦州市', '滦南县', '乐亭县', '迁西县', '玉田县', '遵化市', '迁安市'], '秦皇岛市': ['海港区', '山海关区', '北戴河区', '抚宁区', '青龙满族自治县', '昌黎县', '卢龙县'], '邯郸市': ['邯山区', '丛台区', '复兴区', '峰峰矿区', '肥乡区', '永年区', '临漳县', '成安县', '大名县', '涉县', '磁县', '邱县', '鸡泽县', '广平县', '馆陶县', '魏县', '曲周县', '武安市'], '邢台市': ['桥东区', '桥西区', '邢台县', '临城县', '内丘县', '柏乡县', '隆尧县', '任县', '南和县', '宁晋县', '巨鹿县', '新河县', '广宗县', '平乡县', '威县', '清河县', '临西县', '南宫市', '沙河市'], '保定市': ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县', '唐县', '高阳县', '容城县', '涞源县', '望都县', '安新县', '易县', '曲阳县', '蠡县', '顺平县', '博野县', '雄县', '涿州市', '定州市', '安国市', '高碑店市'], '张家口市': ['桥东区', '桥西区', '宣化区', '下花园区', '万全区', '崇礼区', '张北县', '康保县', '沽源县', '尚义县', '蔚县', '阳原县', '怀安县', '怀来县', '涿鹿县', '赤城县'], '承德市': ['双桥区', '双滦区', '鹰手营子矿区', '承德县', '兴隆县', '平泉市', '滦平县', '隆化县', '丰宁满族自治县', '宽城满族自治县', '围场满族蒙古族自治县'], '沧州市': ['新华区', '运河区', '沧县', '青县', '东光县', '海兴县', '盐山县', '肃宁县', '南皮县', '吴桥县', '献县', '孟村回族自治县', '泊头市', '任丘市', '黄骅市', '河间市'], '廊坊市': ['安次区', '广阳区', '固安县', '永清县', '香河县', '大城县', '文安县', '大厂回族自治县', '霸州市', '三河市'], '衡水市': ['桃城区', '冀州区', '枣强县', '武邑县', '武强县', '饶阳县', '安平县', '故城县', '景县', '阜城县', '深州市'] }, '山西省': { '太原市': ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县', '古交市'], '大同市': ['平城区', '云冈区', '新荣区', '左云县', '阳高县', '天镇县', '广灵县', '灵丘县', '浑源县', '云州区'], '阳泉市': ['城区', '矿区', '郊区', '平定县', '盂县'], '长治市': ['潞州区', '上党区', '屯留区', '潞城区', '襄垣县', '平顺县', '黎城县', '壶关县', '长子县', '武乡县', '沁县', '沁源县'], '晋城市': ['城区', '沁水县', '阳城县', '陵川县', '泽州县', '高平市'], '朔州市': ['朔城区', '平鲁区', '山阴县', '应县', '右玉县', '怀仁市'], '晋中市': ['榆次区', '榆社县', '左权县', '和顺县', '昔阳县', '寿阳县', '太谷县', '祁县', '平遥县', '灵石县', '介休市'], '运城市': ['盐湖区', '临猗县', '万荣县', '闻喜县', '稷山县', '新绛县', '绛县', '垣曲县', '夏县', '平陆县', '芮城县', '永济市', '河津市'], '忻州市': ['忻府区', '定襄县', '五台县', '代县', '繁峙县', '宁武县', '静乐县', '神池县', '五寨县', '岢岚县', '河曲县', '保德县', '偏关县', '原平市'], '临汾市': ['尧都区', '曲沃县', '翼城县', '襄汾县', '洪洞县', '古县', '安泽县', '浮山县', '吉县', '乡宁县', '大宁县', '隰县', '永和县', '蒲县', '汾西县', '侯马市', '霍州市'], '吕梁市': ['离石区', '文水县', '交城县', '兴县', '临县', '柳林县', '石楼县', '岚县', '方山县', '中阳县', '交口县', '孝义市', '汾阳市'] }, '内蒙古自治区': { '呼和浩特市': ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县', '武川县'], '包头市': ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县', '达尔罕茂明安联合旗'], '乌海市': ['海勃湾区', '海南区', '乌达区'], '赤峰市': ['红山区', '元宝山区', '松山区', '阿鲁科尔沁旗', '巴林左旗', '巴林右旗', '林西县', '克什克腾旗', '翁牛特旗', '喀喇沁旗', '宁城县', '敖汉旗'], '通辽市': ['科尔沁区', '科尔沁左翼中旗', '科尔沁左翼后旗', '开鲁县', '库伦旗', '奈曼旗', '扎鲁特旗', '霍林郭勒市'], '鄂尔多斯市': ['东胜区', '康巴什区', '达拉特旗', '准格尔旗', '鄂托克前旗', '鄂托克旗', '杭锦旗', '乌审旗', '伊金霍洛旗'], '呼伦贝尔市': ['海拉尔区', '扎赉诺尔区', '阿荣旗', '莫力达瓦达斡尔族自治旗', '鄂伦春自治旗', '鄂温克族自治旗', '陈巴尔虎旗', '新巴尔虎左旗', '新巴尔虎右旗', '满洲里市', '牙克石市', '扎兰屯市', '额尔古纳市', '根河市'], '巴彦淖尔市': ['临河区', '五原县', '磴口县', '乌拉特前旗', '乌拉特中旗', '乌拉特后旗', '杭锦后旗'], '乌兰察布市': ['集宁区', '卓资县', '化德县', '商都县', '兴和县', '凉城县', '察哈尔右翼前旗', '察哈尔右翼中旗', '察哈尔右翼后旗', '四子王旗', '丰镇市'], '兴安盟': ['乌兰浩特市', '阿尔山市', '科尔沁右翼前旗', '科尔沁右翼中旗', '扎赉特旗', '突泉县'], '锡林郭勒盟': ['锡林浩特市', '阿巴嘎旗', '苏尼特左旗', '苏尼特右旗', '东乌珠穆沁旗', '西乌珠穆沁旗', '太仆寺旗', '镶黄旗', '正镶白旗', '正蓝旗', '多伦县', '二连浩特市'], '阿拉善盟': ['阿拉善左旗', '阿拉善右旗', '额济纳旗'] }, '辽宁省': { '沈阳市': ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区', '辽中区', '康平县', '法库县', '新民市'], '大连市': ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区', '长海县', '瓦房店市', '庄河市'], '鞍山市': ['铁东区', '铁西区', '立山区', '千山区', '台安县', '岫岩满族自治县', '海城市'], '抚顺市': ['新抚区', '东洲区', '望花区', '顺城区', '抚顺县', '新宾满族自治县', '清原满族自治县'], '本溪市': ['平山区', '溪湖区', '明山区', '南芬区', '本溪满族自治县', '桓仁满族自治县'], '丹东市': ['元宝区', '振兴区', '振安区', '宽甸满族自治县', '东港市', '凤城市'], '锦州市': ['古塔区', '凌河区', '太和区', '黑山县', '义县', '凌海市', '北镇市'], '营口市': ['站前区', '西市区', '鲅鱼圈区', '老边区', '盖州市', '大石桥市'], '阜新市': ['海州区', '新邱区', '太平区', '清河门区', '细河区', '阜新蒙古族自治县', '彰武县'], '辽阳市': ['白塔区', '文圣区', '宏伟区', '弓长岭区', '太子河区', '辽阳县', '灯塔市'], '盘锦市': ['双台子区', '兴隆台区', '大洼区', '盘山县'], '铁岭市': ['银州区', '清河区', '铁岭县', '西丰县', '昌图县', '调兵山市', '开原市'], '朝阳市': ['双塔区', '龙城区', '朝阳县', '建平县', '喀喇沁左翼蒙古族自治县', '北票市', '凌源市'], '葫芦岛市': ['连山区', '龙港区', '南票区', '绥中县', '建昌县', '兴城市'] }, '吉林省': { '长春市': ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县', '榆树市', '德惠市'], '吉林市': ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市', '磐石市'], '四平市': ['铁西区', '铁东区', '梨树县', '伊通满族自治县', '公主岭市', '双辽市'], '辽源市': ['龙山区', '西安区', '东丰县', '东辽县'], '通化市': ['东昌区', '二道江区', '通化县', '辉南县', '柳河县', '梅河口市', '集安市'], '白山市': ['浑江区', '江源区', '抚松县', '靖宇县', '长白朝鲜族自治县', '临江市'], '松原市': ['宁江区', '前郭尔罗斯蒙古族自治县', '长岭县', '乾安县', '扶余市'], '白城市': ['洮北区', '镇赖县', '通榆县', '洮南市', '大安市'], '延边朝鲜族自治州': ['延吉市', '图们市', '敦化市', '珲春市', '龙井市', '和龙市', '汪清县', '安图县'] }, '黑龙江省': { '哈尔滨市': ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区', '依兰县', '方正县', '宾县', '巴彦县', '木兰县', '通河县', '延寿县', '尚志市', '五常市'], '齐齐哈尔市': ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区', '龙江县', '依安县', '泰来县', '甘南县', '富裕县', '克山县', '克东县', '拜泉县', '讷河市'], '鸡西市': ['鸡冠区', '恒山区', '滴道区', '梨树区', '城子河区', '麻山区', '鸡东县', '虎林市', '密山市'], '鹤岗市': ['向阳区', '工农区', '南山区', '兴安区', '东山区', '兴山区', '萝北县', '绥滨县'], '双鸭山市': ['尖山区', '岭东区', '四方台区', '宝山区', '集贤县', '友谊县', '宝清县', '饶河县'], '大庆市': ['萨尔图区', '龙凤区', '让胡路区', '红岗区', '大同区', '肇州县', '肇源县', '林甸县', '杜尔伯特蒙古族自治县'], '伊春市': ['伊春区', '南岔区', '友好区', '西林区', '翠峦区', '新青区', '美溪区', '金山屯区', '五营区', '乌马河区', '汤旺河区', '带岭区', '乌伊岭区', '红星区', '上甘岭区', '嘉荫县', '铁力市'], '佳木斯市': ['向阳区', '前进区', '东风区', '郊区', '桦南县', '桦川县', '汤原县', '抚远市', '同江市', '富锦市'], '七台河市': ['新兴区', '桃山区', '茄子河区', '勃利县'], '牡丹江市': ['东安区', '阳明区', '爱民区', '西安区', '林口县', '绥芬河市', '海林市', '宁安市', '穆棱市', '东宁市'], '黑河市': ['爱辉区', '嫩江县', '逊克县', '孙吴县', '北安市', '五大连池市'], '绥化市': ['北林区', '望奎县', '兰西县', '青冈县', '庆安县', '明水县', '绥棱县', '安达市', '肇东市', '海伦市'], '大兴安岭地区': ['呼玛县', '塔河县', '漠河市'] }, '江苏省': { '南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区', '高淳区'], '无锡市': ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'], '徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区', '丰县', '沛县', '睢宁县', '新沂市', '邳州市'], '常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'], '苏州市': ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '常熟市', '张家港市', '昆山市', '太仓市'], '南通市': ['崇川区', '港闸区', '通州区', '海安市', '如东县', '启东市', '如皋市', '海门市'], '连云港市': ['连云区', '海州区', '赣榆区', '东海县', '灌云县', '灌南县'], '淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区', '涟水县', '盱眙县', '金湖县'], '盐城市': ['亭湖区', '盐都区', '大丰区', '响水县', '滨海县', '阜宁县', '射阳县', '建湖县', '东台市'], '扬州市': ['广陵区', '邗江区', '江都区', '宝应县', '仪征市', '高邮市'], '镇江市': ['京口区', '润州区', '丹徒区', '丹阳市', '扬中市', '句容市'], '泰州市': ['海陵区', '高港区', '姜堰区', '兴化市', '靖江市', '泰兴市'], '宿迁市': ['宿城区', '宿豫区', '沭阳县', '泗阳县', '泗洪县'] }, '浙江省': { '杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区', '桐庐县', '淳安县', '建德市'], '宁波市': ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区', '象山县', '宁海县', '余姚市', '慈溪市'], '温州市': ['鹿城区', '龙湾区', '瓯海区', '洞头区', '永嘉县', '平阳县', '苍南县', '文成县', '泰顺县', '瑞安市', '乐清市'], '嘉兴市': ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'], '湖州市': ['吴兴区', '南浔区', '德清县', '长兴县', '安吉县'], '绍兴市': ['越城区', '柯桥区', '上虞区', '新昌县', '诸暨市', '嵊州市'], '金华市': ['婺城区', '金东区', '武义县', '浦江县', '磐安县', '兰溪市', '义乌市', '东阳市', '永康市'], '衢州市': ['柯城区', '衢江区', '常山县', '开化县', '龙游县', '江山市'], '舟山市': ['定海区', '普陀区', '岱山县', '嵊泗县'], '台州市': ['椒江区', '黄岩区', '路桥区', '三门县', '天台县', '仙居县', '温岭市', '临海市', '玉环市'], '丽水市': ['莲都区', '青田县', '缙云县', '遂昌县', '松阳县', '云和县', '庆元县', '景宁畲族自治县', '龙泉市'] }, '安徽省': { '合肥市': ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'], '芜湖市': ['镜湖区', '弋江区', '鸠江区', '三山区', '芜湖县', '繁昌县', '南陵县', '无为市'], '蚌埠市': ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'], '淮南市': ['大通区', '田家庵区', '谢家集区', '八公山区', '潘集区', '凤台县', '寿县'], '马鞍山市': ['花山区', '雨山区', '博望区', '当涂县', '含山县', '和县'], '淮北市': ['杜集区', '相山区', '烈山区', '濉溪县'], '铜陵市': ['铜官区', '义安区', '郊区', '枞阳县'], '安庆市': ['迎江区', '大观区', '宜秀区', '怀宁县', '潜山市', '太湖县', '宿松县', '望江县', '岳西县', '桐城市'], '黄山市': ['屯溪区', '黄山区', '徽州区', '歙县', '休宁县', '黟县', '祁门县'], '滁州市': ['琅琊区', '南谯区', '来安县', '全椒县', '定远县', '凤阳县', '天长市', '明光市'], '阜阳市': ['颍州区', '颍东区', '颍泉区', '临泉县', '太和县', '阜南县', '颍上县', '界首市'], '宿州市': ['埇桥区', '砀山县', '萧县', '灵璧县', '泗县'], '六安市': ['金安区', '裕安区', '叶集区', '霍邱县', '舒城县', '金寨县', '霍山县'], '亳州市': ['谯城区', '涡阳县', '蒙城县', '利辛县'], '池州市': ['贵池区', '东至县', '石台县', '青阳县'], '宣城市': ['宣州区', '郎溪县', '广德市', '泾县', '绩溪县', '旌德县', '宁国市'] }, '福建省': { '福州市': ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县', '闽清县', '永泰县', '平潭县', '福清市'], '厦门市': ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'], '莆田市': ['城厢区', '涵江区', '荔城区', '秀屿区', '仙游县'], '三明市': ['梅列区', '三元区', '明溪县', '清流县', '宁化县', '大田县', '尤溪县', '沙县', '将乐县', '泰宁县', '建宁县', '永安市'], '泉州市': ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县', '金门县', '石狮市', '晋江市', '南安市'], '漳州市': ['芗城区', '龙文区', '云霄县', '漳浦县', '诏安县', '长泰县', '东山县', '南靖县', '平和县', '华安县', '龙海市'], '南平市': ['延平区', '建阳区', '顺昌县', '浦城县', '光泽县', '松溪县', '政和县', '邵武市', '武夷山市', '建瓯市'], '龙岩市': ['新罗区', '永定区', '长汀县', '上杭县', '武平县', '连城县', '漳平市'], '宁德市': ['蕉城区', '霞浦县', '古田县', '屏南县', '寿宁县', '周宁县', '柘荣县', '福安市', '福鼎市'] }, '江西省': { '南昌市': ['东湖区', '西湖区', '青云谱区', '湾里区', '青山湖区', '新建区', '南昌县', '安义县', '进贤县'], '景德镇市': ['昌江区', '珠山区', '浮梁县', '乐平市'], '萍乡市': ['安源区', '湘东区', '莲花县', '上栗县', '芦溪县'], '九江市': ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县', '湖口县', '彭泽县', '瑞昌市', '共青城市', '庐山市'], '新余市': ['渝水区', '分宜县'], '鹰潭市': ['月湖区', '余江区', '贵溪市'], '赣州市': ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县', '龙南县', '定南县', '全南县', '宁都县', '于都县', '兴国县', '会昌县', '寻乌县', '石城县', '瑞金市'], '吉安市': ['吉州区', '青原区', '吉安县', '吉水县', '峡江县', '新干县', '永丰县', '泰和县', '遂川县', '万安县', '安福县', '永新县', '井冈山市'], '宜春市': ['袁州区', '奉新县', '万载县', '上高县', '宜丰县', '靖安县', '铜鼓县', '丰城市', '樟树市', '高安市'], '抚州市': ['临川区', '东乡区', '南城县', '黎川县', '南丰县', '崇仁县', '乐安县', '宜黄县', '金溪县', '资溪县', '广昌县'], '上饶市': ['信州区', '广丰区', '广信区', '玉山县', '铅山县', '横峰县', '弋阳县', '余干县', '鄱阳县', '万年县', '婺源县', '德兴市'] }, '山东省': { '济南市': ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区', '莱芜区', '钢城区', '平阴县', '商河县'], '青岛市': ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市', '平度市', '莱西市'], '淄博市': ['淄川区', '张店区', '博山区', '临淄区', '周村区', '桓台县', '高青县', '沂源县'], '枣庄市': ['市中区', '薛城区', '峄城区', '台儿庄区', '山亭区', '滕州市'], '东营市': ['东营区', '河口区', '垦利区', '利津县', '广饶县'], '烟台市': ['芝罘区', '福山区', '牟平区', '莱山区', '长岛县', '龙口市', '莱阳市', '莱州市', '蓬莱市', '招远市', '栖霞市', '海阳市'], '潍坊市': ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市', '寿光市', '安丘市', '高密市', '昌邑市'], '济宁市': ['任城区', '兖州区', '微山县', '鱼台县', '金乡县', '嘉祥县', '汶上县', '泗水县', '梁山县', '曲阜市', '邹城市'], '泰安市': ['泰山区', '岱岳区', '宁阳县', '东平县', '新泰市', '肥城市'], '威海市': ['环翠区', '文登区', '荣成市', '乳山市'], '日照市': ['东港区', '岚山区', '五莲县', '莒县'], '临沂市': ['兰山区', '罗庄区', '河东区', '沂南县', '郯城县', '沂水县', '兰陵县', '费县', '平邑县', '莒南县', '蒙阴县', '临沭县'], '德州市': ['德城区', '陵城区', '宁津县', '庆云县', '临邑县', '齐河县', '平原县', '夏津县', '武城县', '乐陵市', '禹城市'], '聊城市': ['东昌府区', '茌平区', '阳谷县', '莘县', '茌平县', '东阿县', '冠县', '高唐县', '临清市'], '滨州市': ['滨城区', '沾化区', '惠民县', '阳信县', '无棣县', '博兴县', '邹平市'], '菏泽市': ['牡丹区', '定陶区', '曹县', '单县', '成武县', '巨野县', '郓城县', '鄄城县', '东明县'] }, '河南省': { '郑州市': ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市', '新郑市', '登封市'], '开封市': ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县', '兰考县'], '洛阳市': ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '孟津县', '新安县', '栾川县', '嵩县', '汝阳县', '宜阳县', '洛宁县', '伊川县', '偃师市'], '平顶山市': ['新华区', '卫东区', '石龙区', '湛河区', '宝丰县', '叶县', '鲁山县', '郏县', '舞钢市', '汝州市'], '安阳市': ['文峰区', '北关区', '殷都区', '龙安区', '安阳县', '汤阴县', '滑县', '内黄县', '林州市'], '鹤壁市': ['鹤山区', '山城区', '淇滨区', '浚县', '淇县'], '新乡市': ['红旗区', '卫滨区', '凤泉区', '牧野区', '新乡县', '获嘉县', '原阳县', '延津县', '封丘县', '长垣市', '卫辉市', '辉县市'], '焦作市': ['解放区', '中站区', '马村区', '山阳区', '修武县', '博爱县', '武陟县', '温县', '沁阳市', '孟州市'], '濮阳市': ['华龙区', '清丰县', '南乐县', '范县', '台前县', '濮阳县'], '许昌市': ['魏都区', '建安区', '鄢陵县', '襄城县', '禹州市', '长葛市'], '漯河市': ['源汇区', '郾城区', '召陵区', '舞阳县', '临颍县'], '三门峡市': ['湖滨区', '陕州区', '渑池县', '卢氏县', '义马市', '灵宝市'], '南阳市': ['宛城区', '卧龙区', '南召县', '方城县', '西峡县', '镇平县', '内乡县', '淅川县', '社旗县', '唐河县', '新野县', '桐柏县', '邓州市'], '商丘市': ['梁园区', '睢阳区', '民权县', '睢县', '宁陵县', '柘城县', '虞城县', '夏邑县', '永城市'], '信阳市': ['浉河区', '平桥区', '罗山县', '光山县', '新县', '商城县', '固始县', '潢川县', '淮滨县', '息县'], '周口市': ['川汇区', '扶沟县', '西华县', '商水县', '沈丘县', '郸城县', '淮阳区', '太康县', '鹿邑县', '项城市'], '驻马店市': ['驿城区', '西平县', '上蔡县', '平舆县', '正阳县', '确山县', '泌阳县', '汝南县', '遂平县', '新蔡县'], '济源市': ['济源市'] }, '湖北省': { '武汉市': ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '汉南区', '蔡甸区', '江夏区', '黄陂区', '新洲区'], '黄石市': ['黄石港区', '西塞山区', '下陆区', '铁山区', '阳新县', '大冶市'], '十堰市': ['茅箭区', '张湾区', '郧阳区', '郧西县', '竹山县', '竹溪县', '房县', '丹江口市'], '宜昌市': ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '远安县', '兴山县', '秭归县', '长阳土家族自治县', '五峰土家族自治县', '宜都市', '当阳市', '枝江市'], '襄阳市': ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市', '宜城市'], '鄂州市': ['梁子湖区', '华容区', '鄂城区'], '荆门市': ['东宝区', '掇刀区', '京山市', '沙洋县', '钟祥市'], '孝感市': ['孝南区', '孝昌县', '大悟县', '云梦县', '应城市', '安陆市', '汉川市'], '荆州市': ['沙市区', '荆州区', '公安县', '监利县', '江陵县', '石首市', '洪湖市', '松滋市'], '黄冈市': ['黄州区', '团风县', '红安县', '罗田县', '英山县', '浠水县', '蕲春县', '黄梅县', '麻城市', '武穴市'], '咸宁市': ['咸安区', '嘉鱼县', '通城县', '崇阳县', '通山县', '赤壁市'], '随州市': ['曾都区', '随县', '广水市'], '恩施土家族苗族自治州': ['恩施市', '利川市', '建始县', '巴东县', '宣恩县', '咸丰县', '来凤县', '鹤峰县'], '仙桃市': ['仙桃市'], '潜江市': ['潜江市'], '天门市': ['天门市'], '神农架林区': ['神农架林区'] }, '湖南省': { '长沙市': ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '宁乡市', '浏阳市'], '株洲市': ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '攸县', '茶陵县', '炎陵县', '醴陵市'], '湘潭市': ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'], '衡阳市': ['珠晖区', '雁峰区', '石鼓区', '蒸湘区', '南岳区', '衡阳县', '衡南县', '衡山县', '衡东县', '祁东县', '耒阳市', '常宁市'], '邵阳市': ['双清区', '大祥区', '北塔区', '邵东市', '新邵县', '邵阳县', '隆回县', '洞口县', '绥宁县', '新宁县', '城步苗族自治县', '武冈市'], '岳阳市': ['岳阳楼区', '云溪区', '君山区', '岳阳县', '华容县', '湘阴县', '平江县', '汨罗市', '临湘市'], '常德市': ['武陵区', '鼎城区', '安乡县', '汉寿县', '澧县', '临澧县', '桃源县', '石门县', '津市市'], '张家界市': ['永定区', '武陵源区', '慈利县', '桑植县'], '益阳市': ['资阳区', '赫山区', '南县', '桃江县', '安化县', '沅江市'], '郴州市': ['北湖区', '苏仙区', '桂阳县', '宜章县', '永兴县', '嘉禾县', '临武县', '汝城县', '桂东县', '安仁县', '资兴市'], '永州市': ['零陵区', '冷水滩区', '祁阳县', '东安县', '双牌县', '道县', '江永县', '宁远县', '蓝山县', '新田县', '江华瑶族自治县'], '怀化市': ['鹤城区', '中方县', '沅陵县', '辰溪县', '溆浦县', '会同县', '麻阳苗族自治县', '新晃侗族自治县', '芷江侗族自治县', '靖州苗族侗族自治县', '通道侗族自治县', '洪江市'], '娄底市': ['娄星区', '双峰县', '新化县', '冷水江市', '涟源市'], '湘西土家族苗族自治州': ['吉首市', '泸溪县', '凤凰县', '花垣县', '保靖县', '古丈县', '永顺县', '龙山县'] }, '广东省': { '广州市': ['荔湾区', '越秀区', '海珠区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'], '深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区', '大鹏新区'], '珠海市': ['香洲区', '斗门区', '金湾区'], '汕头市': ['龙湖区', '金平区', '濠江区', '潮阳区', '潮南区', '澄海区', '南澳县'], '佛山市': ['禅城区', '南海区', '顺德区', '三水区', '高明区'], '韶关市': ['武江区', '浈江区', '曲江区', '始兴县', '仁化县', '翁源县', '乳源瑶族自治县', '新丰县', '乐昌市', '南雄市'], '湛江市': ['赤坎区', '霞山区', '坡头区', '麻章区', '遂溪县', '徐闻县', '廉江市', '雷州市', '吴川市'], '肇庆市': ['端州区', '鼎湖区', '高要区', '广宁县', '怀集县', '封开县', '德庆县', '四会市'], '江门市': ['蓬江区', '江海区', '新会区', '台山市', '开平市', '鹤山市', '恩平市'], '茂名市': ['茂南区', '电白区', '高州市', '化州市', '信宜市'], '惠州市': ['惠城区', '惠阳区', '博罗县', '惠东县', '龙门县'], '梅州市': ['梅江区', '梅县区', '大埔县', '丰顺县', '五华县', '平远县', '蕉岭县', '兴宁市'], '汕尾市': ['城区', '海丰县', '陆河县', '陆丰市'], '河源市': ['源城区', '紫金县', '龙川县', '连平县', '和平县', '东源县'], '阳江市': ['江城区', '阳东区', '阳西县', '阳春市'], '清远市': ['清城区', '清新区', '佛冈县', '阳山县', '连山壮族瑶族自治县', '连南瑶族自治县', '英德市', '连州市'], '东莞市': ['东莞市'], '中山市': ['中山市'], '潮州市': ['湘桥区', '潮安区', '饶平县'], '揭阳市': ['榕城区', '揭东区', '揭西县', '惠来县', '普宁市'], '云浮市': ['云城区', '云安区', '新兴县', '郁南县', '罗定市'] }, '广西壮族自治区': { '南宁市': ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县', '上林县', '宾阳县', '横县'], '柳州市': ['城中区', '鱼峰区', '柳南区', '柳北区', '柳江区', '柳城县', '鹿寨县', '融安县', '融水苗族自治县', '三江侗族自治县'], '桂林市': ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县', '兴安县', '永福县', '灌阳县', '龙胜各族自治县', '资源县', '平乐县', '荔浦市', '恭城瑶族自治县'], '梧州市': ['万秀区', '长洲区', '龙圩区', '苍梧县', '藤县', '蒙山县', '岑溪市'], '北海市': ['海城区', '银海区', '铁山港区', '合浦县'], '防城港市': ['港口区', '防城区', '上思县', '东兴市'], '钦州市': ['钦南区', '钦北区', '灵山县', '浦北县'], '贵港市': ['港北区', '港南区', '覃塘区', '平南县', '桂平市'], '玉林市': ['玉州区', '福绵区', '容县', '陆川县', '博白县', '兴业县', '北流市'], '百色市': ['右江区', '田阳区', '田东县', '平果市', '德保县', '那坡县', '凌云县', '乐业县', '田林县', '西林县', '隆林各族自治县', '靖西市'], '贺州市': ['八步区', '平桂区', '昭平县', '钟山县', '富川瑶族自治县'], '河池市': ['金城江区', '宜州区', '南丹县', '天峨县', '凤山县', '东兰县', '罗城仫佬族自治县', '环江毛南族自治县', '巴马瑶族自治县', '都安瑶族自治县', '大化瑶族自治县'], '来宾市': ['兴宾区', '忻城县', '象州县', '武宣县', '金秀瑶族自治县', '合山市'], '崇左市': ['江州区', '扶绥县', '宁明县', '龙州县', '大新县', '天等县', '凭祥市'] }, '海南省': { '海口市': ['秀英区', '龙华区', '琼山区', '美兰区'], '三亚市': ['海棠区', '吉阳区', '天涯区', '崖州区'], '三沙市': ['西沙群岛', '南沙群岛', '中沙群岛'], '儋州市': ['儋州市'], '五指山市': ['五指山市'], '琼海市': ['琼海市'], '文昌市': ['文昌市'], '万宁市': ['万宁市'], '东方市': ['东方市'], '定安县': ['定安县'], '屯昌县': ['屯昌县'], '澄迈县': ['澄迈县'], '临高县': ['临高县'], '白沙黎族自治县': ['白沙黎族自治县'], '昌江黎族自治县': ['昌江黎族自治县'], '乐东黎族自治县': ['乐东黎族自治县'], '陵水黎族自治县': ['陵水黎族自治县'], '保亭黎族苗族自治县': ['保亭黎族苗族自治县'], '琼中黎族苗族自治县': ['琼中黎族苗族自治县'] }, '四川省': { '成都市': ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市'], '自贡市': ['自流井区', '贡井区', '大安区', '沿滩区', '荣县', '富顺县'], '攀枝花市': ['东区', '西区', '仁和区', '米易县', '盐边县'], '泸州市': ['江阳区', '纳溪区', '龙马潭区', '泸县', '合江县', '叙永县', '古蔺县'], '德阳市': ['旌阳区', '罗江区', '中江县', '广汉市', '什邡市', '绵竹市'], '绵阳市': ['涪城区', '游仙区', '安州区', '三台县', '盐亭县', '梓潼县', '北川羌族自治县', '平武县', '江油市'], '广元市': ['利州区', '昭化区', '朝天区', '旺苍县', '青川县', '剑阁县', '苍溪县'], '遂宁市': ['船山区', '安居区', '蓬溪县', '射洪市', '大英县'], '内江市': ['市中区', '东兴区', '威远县', '资中县', '隆昌市'], '乐山市': ['市中区', '沙湾区', '五通桥区', '金口河区', '犍为县', '井研县', '夹江县', '沐川县', '峨边彝族自治县', '马边彝族自治县', '峨眉山市'], '南充市': ['顺庆区', '高坪区', '嘉陵区', '南部县', '营山县', '蓬安县', '仪陇县', '西充县', '阆中市'], '眉山市': ['东坡区', '彭山区', '仁寿县', '洪雅县', '丹棱县', '青神县'], '宜宾市': ['翠屏区', '南溪区', '叙州区', '江安县', '长宁县', '高县', '珙县', '筠连县', '兴文县', '屏山县'], '广安市': ['广安区', '前锋区', '岳池县', '武胜县', '邻水县', '华蓥市'], '达州市': ['通川区', '达川区', '宣汉县', '开江县', '大竹县', '渠县', '万源市'], '雅安市': ['雨城区', '名山区', '荥经县', '汉源县', '石棉县', '天全县', '芦山县', '宝兴县'], '巴中市': ['巴州区', '恩阳区', '通江县', '南江县', '平昌县'], '资阳市': ['雁江区', '安岳县', '乐至县'], '阿坝藏族羌族自治州': ['马尔康市', '汶川县', '理县', '茂县', '松潘县', '九寨沟县', '金川县', '小金县', '黑水县', '壤塘县', '阿坝县', '若尔盖县', '红原县'], '甘孜藏族自治州': ['康定市', '泸定县', '丹巴县', '九龙县', '雅江县', '道孚县', '炉霍县', '甘孜县', '新龙县', '德格县', '白玉县', '石渠县', '色达县', '理塘县', '巴塘县', '乡城县', '稻城县', '得荣县'], '凉山彝族自治州': ['西昌市', '木里藏族自治县', '盐源县', '德昌县', '会理市', '会东县', '宁南县', '普格县', '布拖县', '金阳县', '昭觉县', '喜德县', '冕宁县', '越西县', '甘洛县', '美姑县', '雷波县'] }, '贵州省': { '贵阳市': ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '开阳县', '息烽县', '修文县', '清镇市'], '六盘水市': ['钟山区', '六枝特区', '水城区', '盘州市'], '遵义市': ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县', '务川仡佬族苗族自治县', '凤冈县', '湄潭县', '余庆县', '习水县', '赤水市', '仁怀市'], '安顺市': ['西秀区', '平坝区', '普定县', '镇宁布依族苗族自治县', '关岭布依族苗族自治县', '紫云苗族布依族自治县'], '毕节市': ['七星关区', '大方县', '黔西市', '金沙县', '织金县', '纳雍县', '威宁彝族回族苗族自治县', '赫章县'], '铜仁市': ['碧江区', '万山区', '江口县', '玉屏侗族自治县', '石阡县', '思南县', '印江土家族苗族自治县', '德江县', '沿河土家族自治县', '松桃苗族自治县'], '黔西南布依族苗族自治州': ['兴义市', '兴仁市', '普安县', '晴隆县', '贞丰县', '望谟县', '册亨县', '安龙县'], '黔东南苗族侗族自治州': ['凯里市', '黄平县', '施秉县', '三穗县', '镇远县', '岑巩县', '天柱县', '锦屏县', '剑河县', '台江县', '黎平县', '榕江县', '从江县', '雷山县', '麻江县', '丹寨县'], '黔南布依族苗族自治州': ['都匀市', '福泉市', '荔波县', '贵定县', '瓮安县', '独山县', '平塘县', '罗甸县', '长顺县', '龙里县', '惠水县', '三都水族自治县'] }, '云南省': { '昆明市': ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县', '石林彝族自治县', '嵩明县', '禄劝彝族苗族自治县', '寻甸回族彝族自治县', '安宁市'], '曲靖市': ['麒麟区', '沾益区', '马龙区', '陆良县', '师宗县', '罗平县', '富源县', '会泽县', '宣威市'], '玉溪市': ['红塔区', '江川区', '澄江市', '通海县', '华宁县', '易门县', '峨山彝族自治县', '新平彝族傣族自治县', '元江哈尼族彝族傣族自治县'], '保山市': ['隆阳区', '施甸县', '龙陵县', '昌宁县', '腾冲市'], '昭通市': ['昭阳区', '鲁甸县', '巧家县', '盐津县', '大关县', '永善县', '绥江县', '镇雄县', '彝良县', '威信县', '水富市'], '丽江市': ['古城区', '玉龙纳西族自治县', '永胜县', '华坪县', '宁蒗彝族自治县'], '普洱市': ['思茅区', '宁洱哈尼族彝族自治县', '墨江哈尼族自治县', '景东彝族自治县', '景谷傣族彝族自治县', '镇沅彝族哈尼族拉祜族自治县', '江城哈尼族彝族自治县', '孟连傣族拉祜族佤族自治县', '澜沧拉祜族自治县', '西盟佤族自治县'], '临沧市': ['临翔区', '凤庆县', '云县', '永德县', '镇康县', '双江拉祜族佤族布朗族傣族自治县', '耿马傣族佤族自治县', '沧源佤族自治县'], '楚雄彝族自治州': ['楚雄市', '双柏县', '牟定县', '南华县', '姚安县', '大姚县', '永仁县', '元谋县', '武定县', '禄丰市'], '红河哈尼族彝族自治州': ['个旧市', '开远市', '蒙自市', '弥勒市', '屏边苗族自治县', '建水县', '石屏县', '泸西县', '元阳县', '红河县', '金平苗族瑶族傣族自治县', '绿春县', '河口瑶族自治县'], '文山壮族苗族自治州': ['文山市', '砚山县', '西畴县', '麻栗坡县', '马关县', '丘北县', '广南县', '富宁县'], '西双版纳傣族自治州': ['景洪市', '勐海县', '勐腊县'], '大理白族自治州': ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县', '永平县', '云龙县', '洱源县', '剑川县', '鹤庆县'], '德宏傣族景颇族自治州': ['瑞丽市', '芒市', '梁河县', '盈江县', '陇川县'], '怒江傈僳族自治州': ['泸水市', '福贡县', '贡山独龙族怒族自治县', '兰坪白族普米族自治县'], '迪庆藏族自治州': ['香格里拉市', '德钦县', '维西傈僳族自治县'] }, '西藏自治区': { '拉萨市': ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'], '日喀则市': ['桑珠孜区', '南木林县', '江孜县', '定日县', '萨迦县', '拉孜县', '昂仁县', '谢通门县', '白朗县', '仁布县', '康马县', '定结县', '仲巴县', '亚东县', '吉隆县', '聂拉木县', '萨嘎县', '岗巴县'], '昌都市': ['卡若区', '江达县', '贡觉县', '类乌齐县', '丁青县', '察雅县', '八宿县', '左贡县', '芒康县', '洛隆县', '边坝县'], '林芝市': ['巴宜区', '工布江达县', '米林县', '墨脱县', '波密县', '察隅县', '朗县'], '山南市': ['乃东区', '扎囊县', '贡嘎县', '桑日县', '琼结县', '曲松县', '措美县', '洛扎县', '加查县', '隆子县', '错那县', '浪卡子县'], '那曲市': ['色尼区', '嘉黎县', '比如县', '聂荣县', '安多县', '申扎县', '索县', '班戈县', '巴青县', '尼玛县', '双湖县'], '阿里地区': ['普兰县', '札达县', '噶尔县', '日土县', '革吉县', '改则县', '措勤县'] }, '陕西省': { '西安市': ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区', '鄠邑区', '蓝田县', '周至县'], '铜川市': ['王益区', '印台区', '耀州区', '宜君县'], '宝鸡市': ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县', '千阳县', '麟游县', '凤县', '太白县'], '咸阳市': ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县', '长武县', '旬邑县', '淳化县', '武功县', '兴平市', '彬州市'], '渭南市': ['临渭区', '华州区', '潼关县', '大荔县', '合阳县', '澄城县', '蒲城县', '白水县', '富平县', '韩城市', '华阴市'], '延安市': ['宝塔区', '安塞区', '延长县', '延川县', '志丹县', '吴起县', '甘泉县', '富县', '洛川县', '宜川县', '黄龙县', '黄陵县', '子长市'], '汉中市': ['汉台区', '南郑区', '城固县', '洋县', '西乡县', '勉县', '宁强县', '略阳县', '镇巴县', '留坝县', '佛坪县'], '榆林市': ['榆阳区', '横山区', '府谷县', '靖边县', '定边县', '绥德县', '米脂县', '佳县', '吴堡县', '清涧县', '子洲县', '神木市'], '安康市': ['汉滨区', '汉阴县', '石泉县', '宁陕县', '紫阳县', '岚皋县', '平利县', '镇坪县', '旬阳县', '白河县'], '商洛市': ['商州区', '洛南县', '丹凤县', '商南县', '山阳县', '镇安县', '柞水县'] }, '甘肃省': { '兰州市': ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'], '嘉峪关市': ['嘉峪关市'], '金昌市': ['金川区', '永昌县'], '白银市': ['白银区', '平川区', '靖远县', '会宁县', '景泰县'], '天水市': ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'], '武威市': ['凉州区', '民勤县', '古浪县', '天祝藏族自治县'], '张掖市': ['甘州区', '肃南裕固族自治县', '民乐县', '临泽县', '高台县', '山丹县'], '平凉市': ['崆峒区', '泾川县', '灵台县', '崇信县', '华亭市', '庄浪县', '静宁县'], '酒泉市': ['肃州区', '金塔县', '瓜州县', '肃北蒙古族自治县', '阿克塞哈萨克族自治县', '玉门市', '敦煌市'], '庆阳市': ['西峰区', '庆城县', '环县', '华池县', '合水县', '正宁县', '宁县', '镇原县'], '定西市': ['安定区', '通渭县', '陇西县', '渭源县', '临洮县', '漳县', '岷县'], '陇南市': ['武都区', '成县', '文县', '宕昌县', '康县', '西和县', '礼县', '徽县', '两当县'], '临夏回族自治州': ['临夏市', '临夏县', '康乐县', '永靖县', '广河县', '和政县', '东乡族自治县', '积石山保安族东乡族撒拉族自治县'], '甘南藏族自治州': ['合作市', '临潭县', '卓尼县', '舟曲县', '迭部县', '玛曲县', '碌曲县', '夏河县'] }, '青海省': { '西宁市': ['城东区', '城中区', '城西区', '城北区', '大通回族土族自治县', '湟中区', '湟源县'], '海东市': ['乐都区', '平安区', '民和回族土族自治县', '互助土族自治县', '化隆回族自治县', '循化撒拉族自治县'], '海北藏族自治州': ['门源回族自治县', '祁连县', '海晏县', '刚察县'], '黄南藏族自治州': ['同仁市', '尖扎县', '泽库县', '河南蒙古族自治县'], '海南藏族自治州': ['共和县', '同德县', '贵德县', '兴海县', '贵南县'], '果洛藏族自治州': ['玛沁县', '班玛县', '甘德县', '达日县', '久治县', '玛多县'], '玉树藏族自治州': ['玉树市', '杂多县', '称多县', '治多县', '囊谦县', '曲麻莱县'], '海西蒙古族藏族自治州': ['德令哈市', '格尔木市', '茫崖市', '乌兰县', '都兰县', '天峻县', '大柴旦行委'] }, '宁夏回族自治区': { '银川市': ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'], '石嘴山市': ['大武口区', '惠农区', '平罗县'], '吴忠市': ['利通区', '红寺堡区', '盐池县', '同心县', '青铜峡市'], '固原市': ['原州区', '西吉县', '隆德县', '泾源县', '彭阳县'], '中卫市': ['沙坡头区', '中宁县', '海原县'] }, '新疆维吾尔自治区': { '乌鲁木齐市': ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'], '克拉玛依市': ['独山子区', '克拉玛依区', '白碱滩区', '乌尔禾区'], '吐鲁番市': ['高昌区', '鄯善县', '托克逊县'], '哈密市': ['伊州区', '巴里坤哈萨克自治县', '伊吾县'], '昌吉回族自治州': ['昌吉市', '阜康市', '呼图壁县', '玛纳斯县', '奇台县', '吉木萨尔县', '木垒哈萨克自治县'], '博尔塔拉蒙古自治州': ['博乐市', '阿拉山口市', '精河县', '温泉县'], '巴音郭楞蒙古自治州': ['库尔勒市', '轮台县', '尉犁县', '若羌县', '且末县', '焉耆回族自治县', '和静县', '和硕县', '博湖县'], '阿克苏地区': ['阿克苏市', '温宿县', '库车市', '沙雅县', '新和县', '拜城县', '乌什县', '阿瓦提县', '柯坪县'], '克孜勒苏柯尔克孜自治州': ['阿图什市', '阿克陶县', '阿合奇县', '乌恰县'], '喀什地区': ['喀什市', '疏附县', '疏勒县', '英吉沙县', '泽普县', '莎车县', '叶城县', '麦盖提县', '岳普湖县', '伽师县', '巴楚县', '塔什库尔干塔吉克自治县'], '和田地区': ['和田市', '和田县', '墨玉县', '皮山县', '洛浦县', '策勒县', '于田县', '民丰县'], '伊犁哈萨克自治州': ['伊宁市', '奎屯市', '霍尔果斯市', '伊宁县', '察布查尔锡伯自治县', '霍城县', '巩留县', '新源县', '昭苏县', '特克斯县', '尼勒克县'], '塔城地区': ['塔城市', '乌苏市', '额敏县', '沙湾市', '托里县', '裕民县', '和布克赛尔蒙古自治县'], '阿勒泰地区': ['阿勒泰市', '布尔津县', '富蕴县', '福海县', '哈巴河县', '青河县', '吉木乃县'], '石河子市': ['石河子市'], '阿拉尔市': ['阿拉尔市'], '图木舒克市': ['图木舒克市'], '五家渠市': ['五家渠市'], '北屯市': ['北屯市'], '铁门关市': ['铁门关市'], '双河市': ['双河市'], '可克达拉市': ['可克达拉市'], '昆玉市': ['昆玉市'], '胡杨河市': ['胡杨河市'] }, '香港特别行政区': { '香港岛': ['中西区', '湾仔区', '东区', '南区'], '九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'], '新界': ['北区', '大埔区', '沙田区', '西贡区', '荃湾区', '屯门区', '元朗区', '葵青区', '离岛区'] }, '澳门特别行政区': { '澳门半岛': ['花地玛堂区', '圣安多尼堂区', '大堂区', '望德堂区', '风顺堂区'], '氹仔': ['氹仔'], '路环': ['路环'] }, '台湾省': { '台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'], '新北市': ['万里区', '金山区', '板桥区', '汐止区', '深坑区', '石碇区', '瑞芳区', '平溪区', '双溪区', '贡寮区', '新店区', '坪林区', '乌来区', '永和区', '中和区', '土城区', '三峡区', '树林区', '莺歌区', '三重区', '新庄区', '泰山区', '林口区', '芦洲区', '五股区', '八里区', '淡水区', '三芝区', '石门区'], '桃园市': ['中坜区', '平镇区', '龙潭区', '杨梅区', '新屋区', '观音区', '桃园区', '龟山区', '八德区', '大溪区', '复兴区', '大园区', '芦竹区'], '台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'], '台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '善化区', '大内区', '山上区', '新市区', '安定区'], '高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'], '基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'], '新竹市': ['东区', '北区', '香山区'], '嘉义市': ['东区', '西区'], '新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'], '苗栗县': ['竹南镇', '头份市', '三湾乡', '南庄乡', '狮潭乡', '后龙镇', '通霄镇', '苑里镇', '苗栗市', '造桥乡', '头屋乡', '公馆乡', '大湖乡', '泰安乡', '铜锣乡', '三义乡', '西湖乡', '卓兰镇'], '彰化县': ['彰化市', '芬园乡', '花坛乡', '秀水乡', '鹿港镇', '福兴乡', '线西乡', '和美镇', '伸港乡', '员林市', '社头乡', '永靖乡', '埔心乡', '溪湖镇', '大村乡', '埔盐乡', '田中镇', '北斗镇', '田尾乡', '埤头乡', '溪州乡', '竹塘乡', '二林镇', '大城乡', '芳苑乡', '二水乡'], '南投县': ['南投市', '中寮乡', '草屯镇', '国姓乡', '埔里镇', '仁爱乡', '名间乡', '集集镇', '水里乡', '鱼池乡', '信义乡', '竹山镇', '鹿谷乡'], '云林县': ['斗南镇', '大埤乡', '虎尾镇', '土库镇', '褒忠乡', '东势乡', '台西乡', '仑背乡', '麦寮乡', '斗六市', '林内乡', '古坑乡', '莿桐乡', '西螺镇', '二仑乡', '北港镇', '水林乡', '口湖乡', '四湖乡', '元长乡'], '嘉义县': ['番路乡', '梅山乡', '竹崎乡', '阿里山乡', '中埔乡', '大埔乡', '水上乡', '鹿草乡', '太保市', '朴子市', '东石乡', '六脚乡', '新港乡', '民雄乡', '大林镇', '溪口乡', '义竹乡', '布袋镇'], '屏东县': ['屏东市', '三地门乡', '雾台乡', '玛家乡', '九如乡', '里港乡', '高树乡', '盐埔乡', '长治乡', '麟洛乡', '竹田乡', '内埔乡', '万丹乡', '潮州镇', '泰武乡', '来义乡', '万峦乡', '崁顶乡', '新埤乡', '南州乡', '林边乡', '东港镇', '琉球乡', '佳冬乡', '新园乡', '枋寮乡', '枋山乡', '春日乡', '狮子乡', '车城乡', '牡丹乡', '恒春镇', '满州乡'], '宜兰县': ['宜兰市', '头城镇', '礁溪乡', '壮围乡', '员山乡', '罗东镇', '三星乡', '大同乡', '五结乡', '冬山乡', '苏澳镇', '南澳乡'], '花莲县': ['花莲市', '新城乡', '秀林乡', '吉安乡', '寿丰乡', '凤林镇', '光复乡', '丰滨乡', '瑞穗乡', '万荣乡', '玉里镇', '卓溪乡', '富里乡'], '台东县': ['台东市', '绿岛乡', '兰屿乡', '延平乡', '卑南乡', '鹿野乡', '关山镇', '海端乡', '池上乡', '东河乡', '成功镇', '长滨乡', '太麻里乡', '金峰乡', '大武乡', '达仁乡'], '澎湖县': ['马公市', '西屿乡', '望安乡', '七美乡', '白沙乡', '湖西乡'], '金门县': ['金沙镇', '金湖镇', '金宁乡', '金城镇', '烈屿乡', '乌坵乡'], '连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡'] } }; 🔸============================================================================== 📄 文件: app/templates/admin/base.html 📊 大小: 7904 bytes (7.72 KB) 🕒 修改时间: 2025-07-03 07:06:17 🔸============================================================================== {% block title %}太白购物商城 - 管理后台{% endblock %} {% block extra_css %}{% endblock %}

{% block page_title %}管理后台{% endblock %}

{% block page_description %}{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
{% block extra_js %}{% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/categories.html 📊 大小: 26972 bytes (26.34 KB) 🕒 修改时间: 2025-07-03 07:19:17 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}分类管理 - 太白购物商城管理后台{% endblock %} {% block page_title %}分类管理{% endblock %} {% block page_description %}商品分类层级管理{% endblock %} {% block extra_css %} {% endblock %} {% block content %}
添加新分类
点击上传图标
{% if categories %}
分类结构 {{ categories|length }}
{% set top_categories = categories | selectattr('parent_id', 'equalto', 0) | sort(attribute='sort_order') %} {% for category in top_categories %}
{% if category.icon_url %} {{ category.name }} {% else %}
{% endif %}
{{ category.name }}
ID: {{ category.id }} | 层级: {{ category.level }} | 排序: {{ category.sort_order }} | {% if category.is_active %} 启用 {% else %} 禁用 {% endif %}
{% if category.level < 3 %} {% endif %}
{% set level2_categories = categories | selectattr('parent_id', 'equalto', category.id) | sort(attribute='sort_order') %} {% if level2_categories %}
{% for child in level2_categories %}
{% if child.icon_url %} {{ child.name }} {% else %}
{% endif %}
{{ child.name }}
ID: {{ child.id }} | 层级: {{ child.level }} | 排序: {{ child.sort_order }} | {% if child.is_active %} 启用 {% else %} 禁用 {% endif %}
{% if child.level < 3 %} {% endif %}
{% set level3_categories = categories | selectattr('parent_id', 'equalto', child.id) | sort(attribute='sort_order') %} {% if level3_categories %}
{% for grandchild in level3_categories %}
{% if grandchild.icon_url %} {{ grandchild.name }} {% else %}
{% endif %}
{{ grandchild.name }}
ID: {{ grandchild.id }} | 层级: {{ grandchild.level }} | 排序: {{ grandchild.sort_order }} | {% if grandchild.is_active %} 启用 {% else %} 禁用 {% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
还没有创建任何分类

点击上方的"添加新分类"来创建第一个商品分类

{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/dashboard.html 📊 大小: 8105 bytes (7.92 KB) 🕒 修改时间: 2025-07-03 05:58:50 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}仪表板 - 太白购物商城管理后台{% endblock %} {% block page_title %}仪表板{% endblock %} {% block page_description %}系统概览和数据统计{% endblock %} {% block content %}

{{ stats.total_users or 0 }}

总用户数

{{ stats.active_users or 0 }}

活跃用户

{{ stats.total_admins or 0 }}

管理员数

{{ stats.recent_logs_count or 0 }}

7天操作数

用户注册趋势(最近7天)
系统状态
数据库连接 正常
文件存储 正常
邮件服务 正常
系统版本 v1.0.0
最近操作日志
{% if recent_logs %} {% for log in recent_logs %} {% endfor %} {% else %} {% endif %}
时间 操作者 操作类型 操作内容 IP地址
{{ log.created_at.strftime('%m-%d %H:%M') if log.created_at else '' }} {% if log.user_type == 2 %} 管理员 {% else %} 用户 {% endif %} {{ log.user_id }} {{ log.action }} {% if log.resource_type %} {{ log.resource_type }} {% if log.resource_id %}#{{ log.resource_id }}{% endif %} {% else %} - {% endif %} {{ log.ip_address or '-' }}
暂无操作日志
{% if recent_logs %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/login.html 📊 大小: 4628 bytes (4.52 KB) 🕒 修改时间: 2025-07-03 05:58:36 🔸============================================================================== 管理员登录 - 太白购物商城 🔸============================================================================== 📄 文件: app/templates/admin/orders.html 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/templates/admin/product_form.html 📊 大小: 33693 bytes (32.90 KB) 🕒 修改时间: 2025-07-03 15:08:21 🔸============================================================================== {% extends "admin/base.html" %} {% block title %} {% if product %}编辑商品{% else %}添加商品{% endif %} - 太白购物商城管理后台 {% endblock %} {% block page_title %} {% if product %}编辑商品{% else %}添加商品{% endif %} {% endblock %} {% block page_description %} 商品信息管理 {% endblock %} {% block extra_css %} {% endblock %} {% block content %}
{% if product %} {% endif %}
基本信息
¥
¥
库存管理
{% if product and product.inventory %}
当前库存信息:
{% for inventory in product.inventory %} {% endfor %}
SKU编码 规格组合 库存 价格 状态
{{ inventory.sku_code }} {% if inventory.spec_combination %} {% for key, value in inventory.spec_combination.items() %} {{ key }}:{{ value }}{% if not loop.last %}, {% endif %} {% endfor %} {% else %} 默认规格 {% endif %} {{ inventory.stock }} ¥{{ "%.2f"|format(inventory.get_final_price()) }} {% if inventory.status %} 启用 {% else %} 禁用 {% endif %}
{% endif %}
{% if product %}
商品图片
拖拽图片到这里或点击选择

支持 JPG、PNG、GIF 格式,单张图片不超过 5MB

上传中...
{% endif %}
操作
{% if product %} 返回列表 {% else %} 返回列表 {% endif %}
{% if product %}
商品信息
商品ID:{{ product.id }}
销量:{{ product.sales_count }}
浏览量:{{ product.view_count }}
创建时间:
{{ product.created_at.strftime('%Y-%m-%d %H:%M:%S') if product.created_at else '' }}
更新时间:
{{ product.updated_at.strftime('%Y-%m-%d %H:%M:%S') if product.updated_at else '' }}
{% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/products.html 📊 大小: 17989 bytes (17.57 KB) 🕒 修改时间: 2025-07-03 15:15:27 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}商品管理 - 太白购物商城管理后台{% endblock %} {% block page_title %}商品管理{% endblock %} {% block page_description %}商品信息管理{% endblock %} {% block content %}
{% if products.items %} {% for product in products.items %} {% endfor %} {% else %} {% endif %}
ID 商品图片 商品名称 分类 价格 库存 状态 销量 创建时间 操作
{{ product.id }} {% if product.main_image %} {% else %}
{% endif %}
{{ product.name[:40] }}{% if product.name|length > 40 %}...{% endif %} {% if product.brand %}
{{ product.brand }} {% endif %} {% if product.has_specs %}
多规格 {% endif %}
{{ product.category.name if product.category else '未分类' }} ¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %}
¥{{ "%.2f"|format(product.original_price) }} {% endif %}
{% set total_stock = product.inventory|sum(attribute='stock') if product.inventory else 0 %} {% set sku_count = product.inventory|length if product.inventory else 0 %}
{{ total_stock }} {% if sku_count > 1 %}
{{ sku_count }}个SKU {% endif %} {% if total_stock <= 0 %}
缺货 {% elif total_stock <= 10 %}
库存不足 {% endif %}
{% if product.status == 1 %} 上架 {% else %} 下架 {% endif %}
{{ product.sales_count }}
浏览:{{ product.view_count }}
{{ product.created_at.strftime('%m-%d') if product.created_at else '' }}
{{ product.created_at.strftime('%H:%M') if product.created_at else '' }}
编辑 {% if product.inventory %} {% endif %}

暂无商品数据

{% if search or category_id or status %} 清除筛选 {% endif %}
{% if products.pages > 1 %} {% endif %}
{% endblock %} {% block extra_js %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/profile.html 📊 大小: 5778 bytes (5.64 KB) 🕒 修改时间: 2025-07-03 05:59:09 🔸============================================================================== {% extends "admin/base.html" %} {% block title %}个人资料 - 太白购物商城管理后台{% endblock %} {% block page_title %}个人资料{% endblock %} {% block page_description %}管理员个人信息设置{% endblock %} {% block content %}
基本信息
用户名不可修改
账号信息
角色: {{ admin.role }}
状态: {% if admin.status == 1 %} 正常 {% else %} 禁用 {% endif %}
创建时间:
{{ admin.created_at.strftime('%Y-%m-%d %H:%M:%S') if admin.created_at else '' }}
最后登录:
{{ admin.last_login_at.strftime('%Y-%m-%d %H:%M:%S') if admin.last_login_at else '从未登录' }}
修改密码
密码长度至少6位,建议包含字母和数字
{% endblock %} 🔸============================================================================== 📄 文件: app/templates/admin/users.html 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/templates/base.html 📊 大小: 11658 bytes (11.38 KB) 🕒 修改时间: 2025-07-03 15:26:16 🔸============================================================================== {% block title %}太白购物商城{% endblock %} {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
{% for category, message in messages %} {% endfor %}
{% endif %} {% endwith %}
{% block content %}{% endblock %}
{% block scripts %}{% endblock %} 🔸============================================================================== 📄 文件: app/templates/cart/index.html 📊 大小: 16640 bytes (16.25 KB) 🕒 修改时间: 2025-07-03 15:25:13 🔸============================================================================== {% extends "base.html" %} {% block title %}购物车 - 太白购物商城{% endblock %} {% block content %}

我的购物车


{% if cart_items %}
商品信息
单价
数量
操作
{% for item in cart_items %}
{% if item.product.main_image %} {{ item.product.name }} {% else %}
{% endif %}
{{ item.product.name }}
{% if item.product.brand %} 品牌:{{ item.product.brand }}
{% endif %} {% if item.spec_combination %} 规格:{{ item.spec_combination }}
{% endif %} 库存:{{ item.get_stock() }}件 {% if not item.is_available() %}
{% if item.product.status != 1 %} 商品已下架 {% elif item.get_stock() < item.quantity %} 库存不足 {% endif %}
{% endif %}
¥{{ "%.2f"|format(item.get_price()) }}
小计:¥{{ "%.2f"|format(item.get_total_price()) }}
{% endfor %}
结算信息
已选商品: 0
商品总价: ¥0.00

应付总额: ¥0.00
7天无理由退换
全国包邮
正品保证
{% else %}

购物车是空的

快去选购您喜欢的商品吧!

去购物
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/common/footer.html 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/templates/common/header.html 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/templates/common/pagination.html 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/templates/index.html 📊 大小: 9706 bytes (9.48 KB) 🕒 修改时间: 2025-07-03 14:49:14 🔸============================================================================== {% extends "base.html" %} {% block title %}首页 - 太白购物商城{% endblock %} {% block content %}

欢迎来到太白购物商城

{% if user %}

你好,{{ user.nickname or user.username }}!开始您的购物之旅吧!

{% else %}

发现优质商品,享受便捷购物体验

立即注册 {% endif %}
{% if top_categories %}

商品分类


{% for category in top_categories %} {% endfor %}
{% endif %} {% if hot_products %}

热门商品

查看更多

{% for product in hot_products %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }}
{% endfor %}
{% endif %} {% if new_products %}

最新商品

查看更多

{% for product in new_products %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:50] }}{% if product.name|length > 50 %}...{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }}
{% endfor %}
{% endif %} {% if user %}

我的专区


个人中心
进入
我的订单
查看
购物车
查看
我的收藏
查看
{% endif %}

服务特色


精选商品

汇聚全球优质商品,品质保证,价格实惠

浏览商品
快速配送

全国包邮,快速配送,让您尽快收到心仪商品

了解更多
安全保障

正品保证,售后无忧,让您购物更放心

服务保障
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/checkout.html 📊 大小: 15245 bytes (14.89 KB) 🕒 修改时间: 2025-07-04 02:41:53 🔸============================================================================== {% extends "base.html" %} {% block title %}订单结算 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
收货地址
新增地址
{% for address in addresses %}
{{ address.receiver_name }}

{{ address.receiver_phone }}

{{ address.get_full_address() }}

{% endfor %}
商品信息
{% for item in cart_items %}
{{ item.product.name }}
{{ item.product.name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %} {% if item.product.brand %} {{ item.product.brand }} {% endif %}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.get_total_price()) }}
{% endfor %}
配送方式
支付方式
订单备注
订单摘要
商品总价: ¥{{ "%.2f"|format(total_amount) }}
运费: ¥{{ "%.2f"|format(shipping_fee) }}

应付总额: ¥{{ "%.2f"|format(final_amount) }}
点击"提交订单"表示您同意 《用户协议》
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/detail.html 📊 大小: 14300 bytes (13.96 KB) 🕒 修改时间: 2025-07-04 02:44:35 🔸============================================================================== {% extends "base.html" %} {% block title %}订单详情 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
订单状态
订单已提交

{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}

等待买家付款
{% if order.status >= 2 %}

已完成

{% else %}

请在15分钟内完成支付

{% endif %}
卖家发货
{% if order.status >= 3 %}

{{ order.shipped_at.strftime('%Y-%m-%d %H:%M:%S') if order.shipped_at else '已发货' }}

{% else %}

等待卖家发货

{% endif %}
确认收货
{% if order.status >= 4 %}

{{ order.received_at.strftime('%Y-%m-%d %H:%M:%S') if order.received_at else '已确认收货' }}

{% else %}

等待买家确认收货

{% endif %}
交易完成
{% if order.status == 5 %}

交易成功

{% else %}

等待交易完成

{% endif %}
商品信息
{% for item in order.order_items %}
{{ item.product_name }}
{{ item.product_name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %} 单价:¥{{ "%.2f"|format(item.price) }}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.total_price) }}
{% endfor %}
{% if order.shipping_info %}
物流信息
{% for shipping in order.shipping_info %}
物流公司: {{ shipping.shipping_company or '待发货' }}
快递单号: {{ shipping.tracking_number or '待发货' }}
{% endfor %}
{% endif %}
订单信息
订单号: {{ order.order_sn }}
下单时间: {{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
订单状态: {% if order.status == 1 %} {{ order.get_status_text() }} {% elif order.status == 2 %} {{ order.get_status_text() }} {% elif order.status == 3 %} {{ order.get_status_text() }} {% elif order.status == 5 %} {{ order.get_status_text() }} {% elif order.status == 6 %} {{ order.get_status_text() }} {% else %} {{ order.get_status_text() }} {% endif %}
支付方式: {{ order.payment_method or '未选择' }}
配送方式: {{ order.shipping_method or '标准配送' }}
{% if order.remark %}
备注: {{ order.remark }}
{% endif %}
收货信息
{% set receiver = order.get_receiver_info() %}
收货人: {{ receiver.receiver_name or '未知' }}
联系电话: {{ receiver.receiver_phone or '未知' }}
收货地址: {{ receiver.full_address or '未知' }}
费用明细
商品总价: ¥{{ "%.2f"|format(order.total_amount) }}
运费: ¥{{ "%.2f"|format(order.shipping_fee) }}

应付总额: ¥{{ "%.2f"|format(order.actual_amount) }}
{% if order.can_pay() %} 立即支付 {% endif %} {% if order.can_cancel() %} {% endif %} {% if order.can_confirm_receipt() %} {% endif %} {% if order.status == 4 %} 评价商品 {% endif %} 返回订单列表
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/order/pay.html 📊 大小: 10772 bytes (10.52 KB) 🕒 修改时间: 2025-07-04 02:42:37 🔸============================================================================== {% extends "base.html" %} {% block title %}订单支付 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}

订单支付

订单信息
订单号:{{ order.order_sn }}
¥{{ "%.2f"|format(order.actual_amount) }}
支付方式:{{ order.payment_method }}
14:59
{% if order.payment_method == 'wechat' %}
微信支付

请使用微信扫描二维码完成支付

{% endif %} {% if order.payment_method == 'alipay' %}
支付宝

正在跳转到支付宝...

{% endif %} {% if order.payment_method == 'bank' %}
银行卡支付

正在跳转到网银...

{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/product/detail.html 📊 大小: 25312 bytes (24.72 KB) 🕒 修改时间: 2025-07-03 15:31:39 🔸============================================================================== {% extends "base.html" %} {% block title %}{{ product.name }} - 太白购物商城{% endblock %} {% block content %}
{% if images %} {% if images|length > 1 %}
{% for image in images %}
{{ product.name }}
{% endfor %}
{% endif %} {% else %}
{% endif %}

{{ product.name }}

{% if product.brand %}

品牌:{{ product.brand }}

{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} 省{{ "%.0f"|format(((product.original_price - product.price) / product.original_price * 100)) }}% {% endif %}
销量:{{ product.sales_count }} | 浏览:{{ product.view_count }}
{% if inventory_list and inventory_list|length > 1 %}
选择规格:
{% set spec_groups = {} %} {% for sku in inventory_list %} {% if sku.spec_combination %} {% for spec_name, spec_value in sku.spec_combination.items() %} {% if spec_name not in spec_groups %} {% set _ = spec_groups.update({spec_name: []}) %} {% endif %} {% if spec_value not in spec_groups[spec_name] %} {% set _ = spec_groups[spec_name].append(spec_value) %} {% endif %} {% endfor %} {% endif %} {% endfor %} {% for spec_name, spec_values in spec_groups.items() %}
{% for spec_value in spec_values %} {% endfor %}
{% endfor %}
{% endif %}
库存: {% if inventory_list %} {{ inventory_list[0].stock if inventory_list|length == 1 else '请选择规格' }} {% else %} 暂无库存 {% endif %}
{% if product.weight %}
重量:{{ product.weight }}kg
{% endif %}
服务承诺:
  • 正品保证
  • 7天无理由退换
  • 全国包邮
  • 售后服务
{% if product.description %}
{{ product.description|replace('\n', '
')|safe }}
{% else %}

暂无详细描述

{% endif %}
{% if product.brand %} {% endif %} {% if product.weight %} {% endif %} {% if inventory_list %} {% endif %}
商品名称 {{ product.name }}
商品品牌 {{ product.brand }}
商品分类 {{ product.category.name }}
商品重量 {{ product.weight }}kg
上架时间 {{ product.created_at.strftime('%Y-%m-%d') }}
库存信息 {% if inventory_list|length == 1 %} {{ inventory_list[0].stock }}件 {% else %} 多规格商品,请选择具体规格查看库存 {% endif %}

评价功能开发中...

{% if recommended_products %}

相关推荐


{% for rec_product in recommended_products %} {% endfor %}
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/product/list.html 📊 大小: 13955 bytes (13.63 KB) 🕒 修改时间: 2025-07-03 14:45:55 🔸============================================================================== {% extends "base.html" %} {% block title %} {% if current_category %}{{ current_category.name }} - {% endif %} {% if search %}搜索"{{ search }}" - {% endif %} 商品列表 - 太白购物商城 {% endblock %} {% block content %}
商品搜索
{% if category_id %}{% endif %} {% if sort %}{% endif %}
商品分类
价格筛选
重置
{% if search %}{% endif %} {% if category_id %}{% endif %} {% if sort %}{% endif %}
{% if current_category %} {{ current_category.name }} {% elif search %} 搜索"{{ search }}" {% else %} 全部商品 {% endif %} (共{{ products.total }}个商品)
{% if products.items %}
{% for product in products.items %}
{% if product.main_image %} {{ product.name }} {% else %}
{% endif %}
{{ product.name[:60] }}{% if product.name|length > 60 %}...{% endif %}
{% if product.brand %}

{{ product.brand }}

{% endif %}
¥{{ "%.2f"|format(product.price) }} {% if product.original_price and product.original_price > product.price %} ¥{{ "%.2f"|format(product.original_price) }} {% endif %}
销量{{ product.sales_count }} 浏览{{ product.view_count }}
{% endfor %}
{% if products.pages > 1 %} {% endif %} {% else %}

暂无找到相关商品

请尝试调整搜索条件或浏览其他分类

返回全部商品
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/test_upload.html 📊 大小: 14836 bytes (14.49 KB) 🕒 修改时间: 2025-07-03 04:21:18 🔸============================================================================== COS上传测试

腾讯云COS上传测试

头像上传测试

通用图片上传测试
上传历史
COS上传测试

腾讯云COS上传测试

头像上传测试

通用图片上传测试
上传历史
🔸============================================================================== 📄 文件: app/templates/user/address_form.html 📊 大小: 14730 bytes (14.38 KB) 🕒 修改时间: 2025-07-04 03:22:24 🔸============================================================================== {% extends "base.html" %} {% block title %}{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %} - 太白购物商城{% endblock %} {% block content %}
{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %}
{{ form.hidden_tag() }}
{{ form.receiver_name(class="form-control") }} {% if form.receiver_name.errors %}
{{ form.receiver_name.errors[0] }}
{% endif %}
{{ form.receiver_phone(class="form-control") }} {% if form.receiver_phone.errors %}
{{ form.receiver_phone.errors[0] }}
{% endif %}
{% if form.province.errors %}
{{ form.province.errors[0] }}
{% endif %}
{% if form.city.errors %}
{{ form.city.errors[0] }}
{% endif %}
{% if form.district.errors %}
{{ form.district.errors[0] }}
{% endif %}
{{ form.detail_address(class="form-control", placeholder="街道、门牌号等详细信息") }} {% if form.detail_address.errors %}
{{ form.detail_address.errors[0] }}
{% endif %}
{{ form.postal_code(class="form-control", placeholder="选填") }} {% if form.postal_code.errors %}
{{ form.postal_code.errors[0] }}
{% endif %}
{{ form.is_default(class="form-check-input") }}
{{ form.submit(class="btn btn-primary") }} 取消
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/addresses.html 📊 大小: 7551 bytes (7.37 KB) 🕒 修改时间: 2025-07-04 02:41:21 🔸============================================================================== {% extends "base.html" %} {% block title %}收货地址 - 太白购物商城{% endblock %} {% block content %}
收货地址
添加地址
{% if addresses %}
{% for address in addresses %}
{{ address.receiver_name }} {% if address.is_default %} 默认 {% endif %}

{{ address.receiver_phone }}

{{ address.get_full_address() }}

{% if address.postal_code %}

邮编:{{ address.postal_code }}

{% endif %}
{% endfor %}
{% else %}
暂无收货地址

请添加您的收货地址,方便下单购物

添加地址
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/login.html 📊 大小: 2349 bytes (2.29 KB) 🕒 修改时间: 2025-07-03 03:01:24 🔸============================================================================== {% extends "base.html" %} {% block title %}用户登录 - 太白购物商城{% endblock %} {% block content %}

用户登录

{{ form.hidden_tag() }}
{{ form.username.label(class="form-label") }} {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} {% if form.username.errors %}
{% for error in form.username.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.password.label(class="form-label") }} {{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }} {% if form.password.errors %}
{% for error in form.password.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.remember_me(class="form-check-input") }} {{ form.remember_me.label(class="form-check-label") }}
{{ form.submit(class="btn btn-primary") }}

还没有账户? 立即注册

{% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/orders.html 📊 大小: 14280 bytes (13.95 KB) 🕒 修改时间: 2025-07-04 02:43:57 🔸============================================================================== {% extends "base.html" %} {% block title %}我的订单 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
我的订单
{% if orders.items %} {% for order in orders.items %}
订单号:{{ order.order_sn }}
下单时间:{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}
{% if order.status == 1 %} {{ order.get_status_text() }} {% elif order.status == 2 %} {{ order.get_status_text() }} {% elif order.status == 3 %} {{ order.get_status_text() }} {% elif order.status == 5 %} {{ order.get_status_text() }} {% elif order.status == 6 %} {{ order.get_status_text() }} {% else %} {{ order.get_status_text() }} {% endif %}
{% for item in order.order_items[:3] %}
{{ item.product_name }}
{{ item.product_name }}
{% if item.spec_combination %}

{{ item.spec_combination }}

{% endif %}
× {{ item.quantity }}
¥{{ "%.2f"|format(item.total_price) }}
{% endfor %} {% if order.order_items|length > 3 %}
还有 {{ order.order_items|length - 3 }} 件商品...
{% endif %}
{% endfor %} {% if orders.pages > 1 %} {% endif %} {% else %}
{% if current_status %} 暂无{{ orders.items[0].get_status_text() if orders.items else '该状态' }}订单 {% else %} 暂无订单 {% endif %}

您还没有任何订单,快去购物吧!

去购物
{% endif %}
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/profile.html 📊 大小: 27113 bytes (26.48 KB) 🕒 修改时间: 2025-07-04 02:45:53 🔸============================================================================== {% extends "base.html" %} {% block title %}个人中心 - 太白购物商城{% endblock %} {% block head %} {% endblock %} {% block content %}
基本信息
用户名: {{ user.username }}
昵称: {{ user.nickname or '未设置' }}
手机号: {{ user.phone or '未绑定' }}
邮箱: {{ user.email or '未绑定' }}
性别: {% if user.gender == 1 %}男 {% elif user.gender == 2 %}女 {% else %}未设置 {% endif %}
注册时间: {{ user.created_at.strftime('%Y-%m-%d %H:%M:%S') if user.created_at else '未知' }}
{% if user.avatar_url %} 头像 {% else %}
{% endif %}
上传中...
支持 JPG、PNG 格式,大小不超过 2MB
我的订单
查看所有订单
购物车
查看购物车
我的收藏
收藏的商品
收货地址
管理收货地址
{% endblock %} {% block scripts %} {% endblock %} 🔸============================================================================== 📄 文件: app/templates/user/register.html 📊 大小: 9627 bytes (9.40 KB) 🕒 修改时间: 2025-07-03 04:04:29 🔸============================================================================== {% extends "base.html" %} {% block title %}用户注册 - 太白购物商城{% endblock %} {% block content %}

用户注册

{{ form.hidden_tag() }}
{{ form.username.label(class="form-label") }} {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} {% if form.username.errors %}
{% for error in form.username.errors %} {{ error }} {% endfor %}
{% endif %}
用户名只能包含字母、数字和下划线,3-20个字符
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else ""), id="emailInput") }}
{% if form.email.errors %}
{% for error in form.email.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.email_code.label(class="form-label") }} {{ form.email_code(class="form-control" + (" is-invalid" if form.email_code.errors else ""), placeholder="请输入6位数字验证码") }} {% if form.email_code.errors %}
{% for error in form.email_code.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.phone.label(class="form-label") }} {{ form.phone(class="form-control" + (" is-invalid" if form.phone.errors else "")) }} {% if form.phone.errors %}
{% for error in form.phone.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.password.label(class="form-label") }} {{ form.password(class="form-control" + (" is-invalid" if form.password.errors else ""), id="passwordInput") }} {% if form.password.errors %}
{% for error in form.password.errors %} {{ error }} {% endfor %}
{% endif %}
密码必须包含至少一个字母和一个数字,6-20个字符
{{ form.confirm_password.label(class="form-label") }} {{ form.confirm_password(class="form-control" + (" is-invalid" if form.confirm_password.errors else ""), id="confirmPasswordInput") }} {% if form.confirm_password.errors %}
{% for error in form.confirm_password.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.submit(class="btn btn-success") }}

已有账户? 立即登录

{% endblock %} 🔸============================================================================== 📄 文件: app/utils/__init__.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/utils/auth.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/utils/cos_client.py 📊 大小: 7860 bytes (7.68 KB) 🕒 修改时间: 2025-07-03 04:22:19 🔸============================================================================== """ 腾讯云COS客户端工具 """ import sys import os import uuid import logging from datetime import datetime from qcloud_cos import CosConfig, CosS3Client from qcloud_cos.cos_exception import CosClientError, CosServiceError from config.cos_config import COSConfig # 配置日志 logging.basicConfig(level=logging.INFO, stream=sys.stdout) logger = logging.getLogger(__name__) class COSClient: """腾讯云COS客户端""" def __init__(self): """初始化COS客户端""" try: # 配置COS config = CosConfig( Region=COSConfig.REGION, SecretId=COSConfig.SECRET_ID, SecretKey=COSConfig.SECRET_KEY, Token=None, # 临时密钥需要传入Token,永久密钥不需要 Scheme='https' # 指定使用 http/https 协议来访问COS,默认为https ) # 创建客户端 self.client = CosS3Client(config) self.bucket = COSConfig.BUCKET_NAME logger.info("COS客户端初始化成功") except Exception as e: logger.error(f"COS客户端初始化失败: {str(e)}") raise def generate_file_key(self, folder_type, original_filename): """ 生成文件存储路径 Args: folder_type: 文件夹类型 (avatar, product, review, temp) original_filename: 原始文件名 Returns: str: 生成的文件路径 """ # 获取文件扩展名 file_ext = original_filename.rsplit('.', 1)[1].lower() if '.' in original_filename else '' # 生成唯一文件名 unique_filename = f"{uuid.uuid4().hex}.{file_ext}" if file_ext else uuid.uuid4().hex # 按日期分组 date_folder = datetime.now().strftime('%Y/%m/%d') # 获取存储路径前缀 folder_prefix = COSConfig.UPLOAD_FOLDERS.get(folder_type, COSConfig.UPLOAD_FOLDERS['temp']) # 组合完整路径 file_key = f"{folder_prefix}{date_folder}/{unique_filename}" return file_key def upload_file(self, file_obj, folder_type='temp', original_filename=None): """ 上传文件到COS Args: file_obj: 文件对象或文件路径 folder_type: 文件夹类型 original_filename: 原始文件名 Returns: dict: 上传结果 {'success': bool, 'file_key': str, 'url': str, 'error': str} """ try: # 生成文件路径 if original_filename is None: if hasattr(file_obj, 'filename'): original_filename = file_obj.filename else: original_filename = 'unknown' file_key = self.generate_file_key(folder_type, original_filename) # 上传文件 if hasattr(file_obj, 'read'): # 文件对象 response = self.client.put_object( Bucket=self.bucket, Body=file_obj, Key=file_key, StorageClass='STANDARD', EnableMD5=False ) else: # 文件路径 response = self.client.put_object_from_local_file( Bucket=self.bucket, LocalFilePath=file_obj, Key=file_key, EnableMD5=False ) # 生成访问URL file_url = COSConfig.get_full_url(file_key) logger.info(f"文件上传成功: {file_key}") return { 'success': True, 'file_key': file_key, 'url': file_url, 'etag': response['ETag'], 'error': None } except CosClientError as e: error_msg = f"COS客户端错误: {str(e)}" logger.error(error_msg) return { 'success': False, 'file_key': None, 'url': None, 'error': error_msg } except CosServiceError as e: error_msg = f"COS服务错误: {e.get_error_code()} - {e.get_error_msg()}" logger.error(error_msg) return { 'success': False, 'file_key': None, 'url': None, 'error': error_msg } except Exception as e: error_msg = f"上传失败: {str(e)}" logger.error(error_msg) return { 'success': False, 'file_key': None, 'url': None, 'error': error_msg } def delete_file(self, file_key): """ 删除COS中的文件 Args: file_key: 文件路径 Returns: dict: 删除结果 """ try: response = self.client.delete_object( Bucket=self.bucket, Key=file_key ) logger.info(f"文件删除成功: {file_key}") return { 'success': True, 'error': None } except Exception as e: error_msg = f"删除文件失败: {str(e)}" logger.error(error_msg) return { 'success': False, 'error': error_msg } def get_file_url(self, file_key, expires=3600): """ 获取文件访问URL(用于私有文件) Args: file_key: 文件路径 expires: 过期时间(秒) Returns: str: 预签名URL """ try: response = self.client.get_presigned_download_url( Bucket=self.bucket, Key=file_key, Expired=expires ) return response except Exception as e: logger.error(f"生成预签名URL失败: {str(e)}") return None def list_files(self, prefix='', max_keys=100): """ 列出存储桶中的文件 Args: prefix: 文件路径前缀 max_keys: 最大返回数量 Returns: list: 文件列表 """ try: response = self.client.list_objects( Bucket=self.bucket, Prefix=prefix, MaxKeys=max_keys ) files = [] if 'Contents' in response: for obj in response['Contents']: files.append({ 'key': obj['Key'], 'size': obj['Size'], 'last_modified': obj['LastModified'], 'url': COSConfig.get_full_url(obj['Key']) }) return files except Exception as e: logger.error(f"列出文件失败: {str(e)}") return [] def test_connection(self): """ 测试COS连接 Returns: dict: 测试结果 """ try: # 尝试列出存储桶 response = self.client.list_objects( Bucket=self.bucket, MaxKeys=1 ) return { 'success': True, 'message': 'COS连接测试成功', 'bucket': self.bucket, 'region': COSConfig.REGION } except Exception as e: return { 'success': False, 'message': f'COS连接测试失败: {str(e)}', 'bucket': self.bucket, 'region': COSConfig.REGION } # 创建全局COS客户端实例 cos_client = COSClient() 🔸============================================================================== 📄 文件: app/utils/cos_upload.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/utils/database.py 📊 大小: 1074 bytes (1.05 KB) 🕒 修改时间: 2025-07-03 04:26:13 🔸============================================================================== """ 数据库工具模块 """ from flask_sqlalchemy import SQLAlchemy import sys # 创建数据库实例 db = SQLAlchemy() def init_db(app): """初始化数据库""" db.init_app(app) try: with app.app_context(): # 测试数据库连接 result = db.session.execute(db.text('SELECT 1')) print("✅ 数据库连接成功") # 由于表已存在,我们只需要确保模型与数据库同步 # 不需要重新创建表 print("✅ 数据库初始化完成") except Exception as e: print(f"❌ 数据库初始化失败: {e}") print("请检查数据库配置和网络连接") # 在开发环境中不退出,允许继续运行 print("⚠️ 继续运行,但可能会有数据库相关问题") def test_connection(): """测试数据库连接""" try: result = db.session.execute(db.text('SELECT 1')) return True, "数据库连接正常" except Exception as e: return False, f"数据库连接失败: {str(e)}" 🔸============================================================================== 📄 文件: app/utils/decorators.py 📊 大小: 10277 bytes (10.04 KB) 🕒 修改时间: 2025-07-03 05:56:41 🔸============================================================================== """ 装饰器工具模块 提供登录验证、权限控制等装饰器功能 """ from functools import wraps from flask import session, redirect, url_for, flash, request, jsonify, g from app.models.user import User def login_required(f): """ 登录验证装饰器 用法: @app.route('/profile') @login_required def profile(): return render_template('profile.html') 功能: - 检查用户是否已登录 - 未登录用户重定向到登录页面 - 支持AJAX请求返回JSON响应 """ @wraps(f) def decorated_function(*args, **kwargs): # 检查session中是否有用户ID if 'user_id' not in session: # 如果是AJAX请求,返回JSON响应 if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ 'success': False, 'message': '请先登录', 'code': 'LOGIN_REQUIRED', 'redirect': url_for('auth.login') }), 401 # 普通HTTP请求,重定向到登录页 flash('请先登录后再访问该页面', 'warning') # 保存用户想要访问的页面,登录后可以重定向回来 session['next_url'] = request.url return redirect(url_for('auth.login')) # 将当前用户信息加载到g对象中,方便在视图函数中使用 try: g.current_user = User.query.get(session['user_id']) if not g.current_user or g.current_user.status != 1: # 用户不存在或被禁用,清除session session.pop('user_id', None) flash('账号状态异常,请重新登录', 'error') return redirect(url_for('auth.login')) except Exception as e: # 数据库查询出错,清除session session.pop('user_id', None) flash('登录状态异常,请重新登录', 'error') return redirect(url_for('auth.login')) return f(*args, **kwargs) return decorated_function def admin_required(f): """ 管理员权限验证装饰器 """ @wraps(f) def decorated_function(*args, **kwargs): from app.models.admin import AdminUser # 检查session中是否有管理员ID if 'admin_id' not in session: if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ 'success': False, 'message': '需要管理员权限', 'code': 'ADMIN_REQUIRED', 'redirect': url_for('admin.login') }), 403 flash('需要管理员权限才能访问', 'error') return redirect(url_for('admin.login')) # 加载管理员信息到g对象 try: g.current_admin = AdminUser.query.get(session['admin_id']) if not g.current_admin or g.current_admin.status != 1: # 管理员不存在或被禁用,清除session session.pop('admin_id', None) flash('管理员账号状态异常,请重新登录', 'error') return redirect(url_for('admin.login')) except Exception as e: # 数据库查询出错,清除session session.pop('admin_id', None) flash('登录状态异常,请重新登录', 'error') return redirect(url_for('admin.login')) return f(*args, **kwargs) return decorated_function def json_required(f): """ JSON请求验证装饰器 用法: @app.route('/api/upload', methods=['POST']) @json_required def api_upload(): data = request.get_json() return jsonify({'success': True}) 功能: - 确保请求是JSON格式 - 非JSON请求返回错误响应 """ @wraps(f) def decorated_function(*args, **kwargs): if not request.is_json: return jsonify({ 'success': False, 'message': '请求必须是JSON格式', 'code': 'JSON_REQUIRED' }), 400 return f(*args, **kwargs) return decorated_function def validate_file_upload(allowed_extensions=None, max_size=None): """ 文件上传验证装饰器 用法: @app.route('/upload') @validate_file_upload(allowed_extensions={'jpg', 'png'}, max_size=2*1024*1024) def upload_file(): file = request.files['file'] return jsonify({'success': True}) 参数: allowed_extensions: 允许的文件扩展名集合 max_size: 最大文件大小(字节) """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): # 检查是否有文件上传 if 'file' not in request.files: return jsonify({ 'success': False, 'message': '没有选择文件', 'code': 'NO_FILE' }), 400 file = request.files['file'] # 检查文件名 if file.filename == '': return jsonify({ 'success': False, 'message': '没有选择文件', 'code': 'NO_FILE' }), 400 # 检查文件扩展名 if allowed_extensions: file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else '' if file_ext not in allowed_extensions: return jsonify({ 'success': False, 'message': f'不支持的文件格式,只支持: {", ".join(allowed_extensions)}', 'code': 'INVALID_FILE_TYPE' }), 400 # 检查文件大小 if max_size: # 获取文件大小 file.seek(0, 2) # 移动到文件末尾 file_size = file.tell() file.seek(0) # 重置文件指针 if file_size > max_size: size_mb = max_size / 1024 / 1024 return jsonify({ 'success': False, 'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB', 'code': 'FILE_TOO_LARGE' }), 400 return f(*args, **kwargs) return decorated_function return decorator def rate_limit(max_requests=10, per_seconds=60): """ 简单的请求频率限制装饰器 用法: @app.route('/api/send-code') @rate_limit(max_requests=5, per_seconds=300) # 5分钟内最多5次请求 def send_verification_code(): return jsonify({'success': True}) 参数: max_requests: 最大请求次数 per_seconds: 时间窗口(秒) """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): # 这里可以实现基于IP或用户的请求频率限制 # 简单实现可以使用session或内存缓存 # 生产环境建议使用Redis # 获取客户端标识(IP地址或用户ID) client_id = request.remote_addr if 'user_id' in session: client_id = f"user_{session['user_id']}" # 这里应该实现真正的频率限制逻辑 # 暂时跳过,返回原函数 return f(*args, **kwargs) return decorated_function return decorator def log_operation(action, resource_type=None, resource_id=None): """ 操作日志记录装饰器 用法: @app.route('/admin/users/', methods=['DELETE']) @admin_required @log_operation('删除用户', 'user') def delete_user(user_id): # 删除用户逻辑 return jsonify({'success': True}) """ def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): from app.models.operation_log import OperationLog # 执行原函数 result = f(*args, **kwargs) # 记录操作日志 try: user_id = None user_type = 1 # 默认普通用户 # 检查是否是管理员操作 if 'admin_id' in session: user_id = session['admin_id'] user_type = 2 elif 'user_id' in session: user_id = session['user_id'] user_type = 1 # 获取资源ID(如果在URL参数中) actual_resource_id = resource_id if resource_type and not actual_resource_id: # 尝试从URL参数中获取资源ID for key, value in kwargs.items(): if key.endswith('_id'): actual_resource_id = value break # 准备请求数据 request_data = {} if request.method in ['POST', 'PUT', 'PATCH']: if request.is_json: request_data = request.get_json() or {} else: request_data = request.form.to_dict() # 记录日志 OperationLog.create_log( user_id=user_id, user_type=user_type, action=action, resource_type=resource_type, resource_id=actual_resource_id, ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent'), request_data=request_data if request_data else None ) except Exception as e: # 日志记录失败不影响主要功能 print(f"记录操作日志失败: {str(e)}") return result return decorated_function return decorator 🔸============================================================================== 📄 文件: app/utils/email_service.py 📊 大小: 2485 bytes (2.43 KB) 🕒 修改时间: 2025-07-03 03:35:36 🔸============================================================================== from flask import current_app from flask_mail import Mail, Message from threading import Thread mail = Mail() def send_async_email(app, msg): """异步发送邮件""" with app.app_context(): try: mail.send(msg) except Exception as e: print(f"邮件发送失败: {e}") def send_email(to, subject, template, **kwargs): """发送邮件""" app = current_app._get_current_object() msg = Message( subject=subject, recipients=[to], html=template, sender=current_app.config['MAIL_DEFAULT_SENDER'] ) # 异步发送 thr = Thread(target=send_async_email, args=[app, msg]) thr.start() return thr def send_verification_email(email, code, code_type): """发送验证码邮件""" type_map = { 1: '注册', 2: '登录', 3: '找回密码' } subject = f'【太白购物】{type_map.get(code_type, "验证")}验证码' html_template = f""" 验证码邮件

太白购物平台

您好!

您正在进行{type_map.get(code_type, "验证")}操作,验证码为:

{code}

验证码有效期为10分钟,请及时使用。

如果这不是您的操作,请忽略此邮件。

此邮件由系统自动发送,请勿回复。

© 2024 太白购物平台 版权所有

""" return send_email(email, subject, html_template) 🔸============================================================================== 📄 文件: app/utils/file_upload.py 📊 大小: 12684 bytes (12.39 KB) 🕒 修改时间: 2025-07-03 04:20:25 🔸============================================================================== """ 文件上传处理工具 """ import os import magic from PIL import Image from io import BytesIO from werkzeug.utils import secure_filename from config.cos_config import COSConfig from .cos_client import cos_client class FileUploadHandler: """文件上传处理器""" @staticmethod def validate_file(file_obj, file_type='image'): """ 验证文件 Args: file_obj: 文件对象 file_type: 文件类型 (image, file) Returns: dict: 验证结果 """ if not file_obj or not file_obj.filename: return {'valid': False, 'error': '请选择文件'} # 检查文件扩展名 filename = secure_filename(file_obj.filename) if '.' not in filename: return {'valid': False, 'error': '文件格式不正确'} file_ext = filename.rsplit('.', 1)[1].lower() if file_type == 'image': allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS max_size = COSConfig.MAX_IMAGE_SIZE else: allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS max_size = COSConfig.MAX_FILE_SIZE if file_ext not in allowed_extensions: return { 'valid': False, 'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}' } # 检查文件大小 file_obj.seek(0, 2) # 移动到文件末尾 file_size = file_obj.tell() file_obj.seek(0) # 重置文件指针 if file_size > max_size: max_size_mb = max_size / (1024 * 1024) return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'} # 验证文件内容类型(防止恶意文件) try: file_content = file_obj.read(1024) # 读取前1KB用于检测 file_obj.seek(0) # 重置文件指针 mime_type = magic.from_buffer(file_content, mime=True) if file_type == 'image' and not mime_type.startswith('image/'): return {'valid': False, 'error': '文件内容不是有效的图片格式'} except Exception: # 如果magic检测失败,继续处理(某些环境可能没有libmagic) pass return {'valid': True, 'filename': filename, 'size': file_size} @staticmethod def process_image(file_obj, max_width=1200, max_height=1200, quality=None): """ 处理图片(压缩、调整尺寸) Args: file_obj: 图片文件对象 max_width: 最大宽度 max_height: 最大高度 quality: 压缩质量 Returns: BytesIO: 处理后的图片数据 """ try: # 打开图片 image = Image.open(file_obj) # 转换RGBA到RGB(处理PNG透明背景) if image.mode in ('RGBA', 'LA', 'P'): background = Image.new('RGB', image.size, (255, 255, 255)) if image.mode == 'P': image = image.convert('RGBA') background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) image = background # 调整图片尺寸 if image.width > max_width or image.height > max_height: image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) # 保存处理后的图片 output = BytesIO() image.save( output, format='JPEG', quality=quality or COSConfig.IMAGE_QUALITY, optimize=True ) output.seek(0) return output except Exception as e: raise Exception(f"图片处理失败: {str(e)}") @staticmethod def upload_image(file_obj, folder_type='temp', process_image=True): """ 上传图片到COS Args: file_obj: 图片文件对象 folder_type: 存储文件夹类型 process_image: 是否处理图片 Returns: dict: 上传结果 """ # 验证文件 validation = FileUploadHandler.validate_file(file_obj, 'image') if not validation['valid']: return { 'success': False, 'error': validation['error'], 'url': None, 'file_key': None } try: # 处理图片 if process_image: processed_file = FileUploadHandler.process_image(file_obj) upload_file = processed_file else: file_obj.seek(0) upload_file = file_obj # 上传到COS result = cos_client.upload_file( upload_file, folder_type, validation['filename'] ) return result except Exception as e: return { 'success': False, 'error': f"上传失败: {str(e)}", 'url': None, 'file_key': None } @staticmethod def upload_file(file_obj, folder_type='temp'): """ 上传普通文件到COS Args: file_obj: 文件对象 folder_type: 存储文件夹类型 Returns: dict: 上传结果 """ # 验证文件 validation = FileUploadHandler.validate_file(file_obj, 'file') if not validation['valid']: return { 'success': False, 'error': validation['error'], 'url': None, 'file_key': None } try: file_obj.seek(0) # 上传到COS result = cos_client.upload_file( file_obj, folder_type, validation['filename'] ) return result except Exception as e: return { 'success': False, 'error': f"上传失败: {str(e)}", 'url': None, 'file_key': None } # 创建全局文件上传处理器实例 file_upload_handler = FileUploadHandler() """ 文件上传处理工具 """ import os import magic from PIL import Image from io import BytesIO from werkzeug.utils import secure_filename from config.cos_config import COSConfig from .cos_client import cos_client class FileUploadHandler: """文件上传处理器""" @staticmethod def validate_file(file_obj, file_type='image'): """ 验证文件 Args: file_obj: 文件对象 file_type: 文件类型 (image, file) Returns: dict: 验证结果 """ if not file_obj or not file_obj.filename: return {'valid': False, 'error': '请选择文件'} # 检查文件扩展名 filename = secure_filename(file_obj.filename) if '.' not in filename: return {'valid': False, 'error': '文件格式不正确'} file_ext = filename.rsplit('.', 1)[1].lower() if file_type == 'image': allowed_extensions = COSConfig.ALLOWED_IMAGE_EXTENSIONS max_size = COSConfig.MAX_IMAGE_SIZE else: allowed_extensions = COSConfig.ALLOWED_FILE_EXTENSIONS max_size = COSConfig.MAX_FILE_SIZE if file_ext not in allowed_extensions: return { 'valid': False, 'error': f'不支持的文件格式,支持格式: {", ".join(allowed_extensions)}' } # 检查文件大小 file_obj.seek(0, 2) # 移动到文件末尾 file_size = file_obj.tell() file_obj.seek(0) # 重置文件指针 if file_size > max_size: max_size_mb = max_size / (1024 * 1024) return {'valid': False, 'error': f'文件大小不能超过 {max_size_mb:.1f}MB'} # 验证文件内容类型(防止恶意文件) try: file_content = file_obj.read(1024) # 读取前1KB用于检测 file_obj.seek(0) # 重置文件指针 mime_type = magic.from_buffer(file_content, mime=True) if file_type == 'image' and not mime_type.startswith('image/'): return {'valid': False, 'error': '文件内容不是有效的图片格式'} except Exception: # 如果magic检测失败,继续处理(某些环境可能没有libmagic) pass return {'valid': True, 'filename': filename, 'size': file_size} @staticmethod def process_image(file_obj, max_width=1200, max_height=1200, quality=None): """ 处理图片(压缩、调整尺寸) Args: file_obj: 图片文件对象 max_width: 最大宽度 max_height: 最大高度 quality: 压缩质量 Returns: BytesIO: 处理后的图片数据 """ try: # 打开图片 image = Image.open(file_obj) # 转换RGBA到RGB(处理PNG透明背景) if image.mode in ('RGBA', 'LA', 'P'): background = Image.new('RGB', image.size, (255, 255, 255)) if image.mode == 'P': image = image.convert('RGBA') background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) image = background # 调整图片尺寸 if image.width > max_width or image.height > max_height: image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) # 保存处理后的图片 output = BytesIO() image.save( output, format='JPEG', quality=quality or COSConfig.IMAGE_QUALITY, optimize=True ) output.seek(0) return output except Exception as e: raise Exception(f"图片处理失败: {str(e)}") @staticmethod def upload_image(file_obj, folder_type='temp', process_image=True): """ 上传图片到COS Args: file_obj: 图片文件对象 folder_type: 存储文件夹类型 process_image: 是否处理图片 Returns: dict: 上传结果 """ # 验证文件 validation = FileUploadHandler.validate_file(file_obj, 'image') if not validation['valid']: return { 'success': False, 'error': validation['error'], 'url': None, 'file_key': None } try: # 处理图片 if process_image: processed_file = FileUploadHandler.process_image(file_obj) upload_file = processed_file else: file_obj.seek(0) upload_file = file_obj # 上传到COS result = cos_client.upload_file( upload_file, folder_type, validation['filename'] ) return result except Exception as e: return { 'success': False, 'error': f"上传失败: {str(e)}", 'url': None, 'file_key': None } @staticmethod def upload_file(file_obj, folder_type='temp'): """ 上传普通文件到COS Args: file_obj: 文件对象 folder_type: 存储文件夹类型 Returns: dict: 上传结果 """ # 验证文件 validation = FileUploadHandler.validate_file(file_obj, 'file') if not validation['valid']: return { 'success': False, 'error': validation['error'], 'url': None, 'file_key': None } try: file_obj.seek(0) # 上传到COS result = cos_client.upload_file( file_obj, folder_type, validation['filename'] ) return result except Exception as e: return { 'success': False, 'error': f"上传失败: {str(e)}", 'url': None, 'file_key': None } # 创建全局文件上传处理器实例 file_upload_handler = FileUploadHandler() 🔸============================================================================== 📄 文件: app/utils/helpers.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/utils/sms.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/utils/wechat_pay.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/views/__init__.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: app/views/address.py 📊 大小: 8172 bytes (7.98 KB) 🕒 修改时间: 2025-07-04 03:10:28 🔸============================================================================== """ 地址管理视图 """ from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash from app.models.address import UserAddress from app.models.user import User from app.forms import AddressForm from app.utils.decorators import login_required from config.database import db address_bp = Blueprint('address', __name__, url_prefix='/address') @address_bp.route('/') @login_required def index(): """地址管理页面""" user_id = session['user_id'] addresses = UserAddress.get_user_addresses(user_id) return render_template('user/addresses.html', addresses=addresses) @address_bp.route('/add', methods=['GET', 'POST']) @login_required def add(): """添加地址""" form = AddressForm() if request.method == 'POST': # 手动验证必填字段 if not all([ form.receiver_name.data, form.receiver_phone.data, form.province.data, form.city.data, form.district.data, form.detail_address.data ]): flash('请填写所有必填信息', 'error') return render_template('user/address_form.html', form=form, action='add') # 验证手机号格式 import re if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data): flash('请输入有效的手机号', 'error') return render_template('user/address_form.html', form=form, action='add') try: user_id = session['user_id'] # 如果是第一个地址或设为默认,处理默认地址 if form.is_default.data or not UserAddress.query.filter_by(user_id=user_id).first(): UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0}) is_default = 1 else: is_default = 0 address = UserAddress( user_id=user_id, receiver_name=form.receiver_name.data.strip(), receiver_phone=form.receiver_phone.data.strip(), province=form.province.data.strip(), city=form.city.data.strip(), district=form.district.data.strip(), detail_address=form.detail_address.data.strip(), postal_code=form.postal_code.data.strip() if form.postal_code.data else None, is_default=is_default ) db.session.add(address) db.session.commit() flash('地址添加成功', 'success') return redirect(url_for('address.index')) except Exception as e: db.session.rollback() flash(f'添加失败: {str(e)}', 'error') return render_template('user/address_form.html', form=form, action='add') @address_bp.route('/edit/', methods=['GET', 'POST']) @login_required def edit(address_id): """编辑地址""" user_id = session['user_id'] address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first_or_404() form = AddressForm() if request.method == 'GET': # 预填充表单数据 form.receiver_name.data = address.receiver_name form.receiver_phone.data = address.receiver_phone form.province.data = address.province form.city.data = address.city form.district.data = address.district form.detail_address.data = address.detail_address form.postal_code.data = address.postal_code form.is_default.data = bool(address.is_default) elif request.method == 'POST': # 手动验证必填字段 if not all([ form.receiver_name.data, form.receiver_phone.data, form.province.data, form.city.data, form.district.data, form.detail_address.data ]): flash('请填写所有必填信息', 'error') return render_template('user/address_form.html', form=form, action='edit', address=address) # 验证手机号格式 import re if not re.match(r'^1[3-9]\d{9}$', form.receiver_phone.data): flash('请输入有效的手机号', 'error') return render_template('user/address_form.html', form=form, action='edit', address=address) try: # 如果设为默认地址,先取消其他默认地址 if form.is_default.data and not address.is_default: UserAddress.query.filter_by(user_id=user_id).update({'is_default': 0}) address.is_default = 1 elif not form.is_default.data and address.is_default: # 如果取消当前默认地址,需要检查是否还有其他地址 other_addresses = UserAddress.query.filter( UserAddress.user_id == user_id, UserAddress.id != address_id ).first() if other_addresses: address.is_default = 0 else: flash('至少需要保留一个默认地址', 'warning') return render_template('user/address_form.html', form=form, action='edit', address=address) # 更新地址信息 address.receiver_name = form.receiver_name.data.strip() address.receiver_phone = form.receiver_phone.data.strip() address.province = form.province.data.strip() address.city = form.city.data.strip() address.district = form.district.data.strip() address.detail_address = form.detail_address.data.strip() address.postal_code = form.postal_code.data.strip() if form.postal_code.data else None db.session.commit() flash('地址更新成功', 'success') return redirect(url_for('address.index')) except Exception as e: db.session.rollback() flash(f'更新失败: {str(e)}', 'error') return render_template('user/address_form.html', form=form, action='edit', address=address) @address_bp.route('/delete/', methods=['POST']) @login_required def delete(address_id): """删除地址""" try: user_id = session['user_id'] address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first() if not address: return jsonify({'success': False, 'message': '地址不存在'}) # 检查是否是唯一地址 address_count = UserAddress.query.filter_by(user_id=user_id).count() if address_count <= 1: return jsonify({'success': False, 'message': '至少需要保留一个地址'}) # 如果删除的是默认地址,需要设置新的默认地址 if address.is_default: other_address = UserAddress.query.filter( UserAddress.user_id == user_id, UserAddress.id != address_id ).first() if other_address: other_address.is_default = 1 db.session.delete(address) db.session.commit() return jsonify({'success': True, 'message': '地址删除成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) @address_bp.route('/set_default/', methods=['POST']) @login_required def set_default(address_id): """设置默认地址""" try: user_id = session['user_id'] success = UserAddress.set_default_address(user_id, address_id) if success: return jsonify({'success': True, 'message': '默认地址设置成功'}) else: return jsonify({'success': False, 'message': '地址不存在'}) except Exception as e: return jsonify({'success': False, 'message': f'设置失败: {str(e)}'}) @address_bp.route('/api/list') @login_required def api_list(): """获取用户地址列表API""" user_id = session['user_id'] addresses = UserAddress.get_user_addresses(user_id) return jsonify({ 'success': True, 'addresses': [addr.to_dict() for addr in addresses] }) 🔸============================================================================== 📄 文件: app/views/admin.py 📊 大小: 7865 bytes (7.68 KB) 🕒 修改时间: 2025-07-03 05:56:57 🔸============================================================================== """ 管理员视图 """ from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify, g from werkzeug.security import generate_password_hash from app.models.admin import AdminUser from app.models.user import User from app.models.operation_log import OperationLog from app.utils.decorators import admin_required, log_operation from config.database import db from datetime import datetime, timedelta from sqlalchemy import func admin_bp = Blueprint('admin', __name__, url_prefix='/admin') @admin_bp.route('/login', methods=['GET', 'POST']) def login(): """管理员登录""" if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '').strip() if not username or not password: flash('请输入用户名和密码', 'error') return render_template('admin/login.html') # 查找管理员 admin = AdminUser.query.filter_by(username=username).first() if not admin or not admin.check_password(password): flash('用户名或密码错误', 'error') return render_template('admin/login.html') if admin.status != 1: flash('账号已被禁用,请联系系统管理员', 'error') return render_template('admin/login.html') # 登录成功 session['admin_id'] = admin.id session['admin_username'] = admin.username # 更新最后登录时间 admin.update_last_login() # 记录登录日志 try: OperationLog.create_log( user_id=admin.id, user_type=2, action='管理员登录', ip_address=request.remote_addr, user_agent=request.headers.get('User-Agent') ) except Exception as e: print(f"记录登录日志失败: {str(e)}") flash('登录成功', 'success') return redirect(url_for('admin.dashboard')) return render_template('admin/login.html') @admin_bp.route('/logout') @admin_required @log_operation('管理员登出') def logout(): """管理员登出""" session.pop('admin_id', None) session.pop('admin_username', None) flash('已安全退出', 'info') return redirect(url_for('admin.login')) @admin_bp.route('/dashboard') @admin_required def dashboard(): """管理员仪表板""" try: # 获取统计数据 stats = { 'total_users': User.query.count(), 'active_users': User.query.filter_by(status=1).count(), 'total_admins': AdminUser.query.count(), 'recent_logs_count': OperationLog.query.filter( OperationLog.created_at >= datetime.now() - timedelta(days=7) ).count() } # 获取最近的操作日志 recent_logs = OperationLog.query.order_by( OperationLog.created_at.desc() ).limit(10).all() # 用户注册趋势(最近7天) user_trend = [] for i in range(6, -1, -1): date = datetime.now() - timedelta(days=i) date_start = date.replace(hour=0, minute=0, second=0, microsecond=0) date_end = date_start + timedelta(days=1) count = User.query.filter( User.created_at >= date_start, User.created_at < date_end ).count() user_trend.append({ 'date': date.strftime('%m-%d'), 'count': count }) return render_template('admin/dashboard.html', stats=stats, recent_logs=recent_logs, user_trend=user_trend) except Exception as e: flash(f'加载仪表板数据失败: {str(e)}', 'error') return render_template('admin/dashboard.html', stats={}, recent_logs=[], user_trend=[]) @admin_bp.route('/profile') @admin_required def profile(): """管理员个人资料""" return render_template('admin/profile.html', admin=g.current_admin) @admin_bp.route('/profile/edit', methods=['POST']) @admin_required @log_operation('修改管理员资料') def edit_profile(): """编辑管理员个人资料""" try: real_name = request.form.get('real_name', '').strip() email = request.form.get('email', '').strip() phone = request.form.get('phone', '').strip() # 更新信息 if real_name: g.current_admin.real_name = real_name if email: g.current_admin.email = email if phone: g.current_admin.phone = phone db.session.commit() flash('个人资料更新成功', 'success') except Exception as e: db.session.rollback() flash(f'更新失败: {str(e)}', 'error') return redirect(url_for('admin.profile')) @admin_bp.route('/change-password', methods=['POST']) @admin_required @log_operation('修改管理员密码') def change_password(): """修改管理员密码""" try: current_password = request.form.get('current_password', '').strip() new_password = request.form.get('new_password', '').strip() confirm_password = request.form.get('confirm_password', '').strip() # 验证当前密码 if not g.current_admin.check_password(current_password): flash('当前密码错误', 'error') return redirect(url_for('admin.profile')) # 验证新密码 if len(new_password) < 6: flash('新密码长度至少6位', 'error') return redirect(url_for('admin.profile')) if new_password != confirm_password: flash('新密码和确认密码不一致', 'error') return redirect(url_for('admin.profile')) # 更新密码 g.current_admin.set_password(new_password) db.session.commit() flash('密码修改成功', 'success') except Exception as e: db.session.rollback() flash(f'密码修改失败: {str(e)}', 'error') return redirect(url_for('admin.profile')) @admin_bp.route('/users') @admin_required def users(): """用户管理""" page = request.args.get('page', 1, type=int) per_page = 20 query = User.query.order_by(User.created_at.desc()) # 搜索功能 search = request.args.get('search', '').strip() if search: query = query.filter( db.or_( User.username.like(f'%{search}%'), User.email.like(f'%{search}%'), User.phone.like(f'%{search}%'), User.nickname.like(f'%{search}%') ) ) # 状态筛选 status = request.args.get('status', '', type=str) if status: query = query.filter(User.status == int(status)) users = query.paginate(page=page, per_page=per_page, error_out=False) return render_template('admin/users.html', users=users, search=search, status=status) @admin_bp.route('/logs') @admin_required def logs(): """操作日志""" page = request.args.get('page', 1, type=int) per_page = 50 query = OperationLog.query.order_by(OperationLog.created_at.desc()) # 用户类型筛选 user_type = request.args.get('user_type', '', type=str) if user_type: query = query.filter(OperationLog.user_type == int(user_type)) # 操作类型筛选 action = request.args.get('action', '').strip() if action: query = query.filter(OperationLog.action.like(f'%{action}%')) logs = query.paginate(page=page, per_page=per_page, error_out=False) return render_template('admin/logs.html', logs=logs, user_type=user_type, action=action) 🔸============================================================================== 📄 文件: app/views/auth.py 📊 大小: 4911 bytes (4.80 KB) 🕒 修改时间: 2025-07-03 03:42:09 🔸============================================================================== from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify from app.forms import LoginForm, RegisterForm from app.models.user import User from app.models.verification import EmailVerification from app.utils.email_service import send_verification_email from config.database import db auth_bp = Blueprint('auth', __name__, url_prefix='/auth') @auth_bp.route('/login', methods=['GET', 'POST']) def login(): """用户登录""" if 'user_id' in session: return redirect(url_for('main.index')) form = LoginForm() if form.validate_on_submit(): username = form.username.data password = form.password.data # 支持用户名、手机号、邮箱登录 user = User.query.filter( (User.username == username) | (User.phone == username) | (User.email == username) ).first() if user and user.check_password(password): if user.status == 0: flash('账户已被禁用,请联系管理员', 'error') return render_template('user/login.html', form=form) # 登录成功,设置session session['user_id'] = user.id session['username'] = user.username session['nickname'] = user.nickname or user.username session.permanent = form.remember_me.data flash(f'欢迎回来,{user.nickname or user.username}!', 'success') # 获取登录前的页面 next_page = request.args.get('next') if next_page: return redirect(next_page) return redirect(url_for('main.index')) else: flash('用户名或密码错误', 'error') return render_template('user/login.html', form=form) @auth_bp.route('/send_email_code', methods=['POST']) def send_email_code(): """发送邮箱验证码""" try: data = request.get_json() email = data.get('email') code_type = data.get('type', 1) # 默认为注册类型 if not email: return jsonify({'success': False, 'message': '邮箱地址不能为空'}) # 检查邮箱格式 import re email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$' if not re.match(email_pattern, email): return jsonify({'success': False, 'message': '邮箱格式不正确'}) # 如果是注册,检查邮箱是否已被注册 if code_type == 1: existing_user = User.query.filter_by(email=email).first() if existing_user: return jsonify({'success': False, 'message': '该邮箱已被注册'}) # 检查是否频繁发送(1分钟内只能发送一次) from datetime import datetime, timedelta recent_code = EmailVerification.query.filter_by( email=email, type=code_type ).filter( EmailVerification.created_at > datetime.utcnow() - timedelta(minutes=1) ).first() if recent_code: return jsonify({'success': False, 'message': '发送过于频繁,请稍后再试'}) # 创建验证码 verification = EmailVerification.create_verification(email, code_type) # 发送邮件 send_verification_email(email, verification.code, code_type) return jsonify({'success': True, 'message': '验证码已发送'}) except Exception as e: print(f"发送邮箱验证码错误: {e}") return jsonify({'success': False, 'message': '发送失败,请重试'}) @auth_bp.route('/register', methods=['GET', 'POST']) def register(): """用户注册""" if 'user_id' in session: return redirect(url_for('main.index')) form = RegisterForm() if form.validate_on_submit(): try: # 验证邮箱验证码 if not EmailVerification.verify_code(form.email.data, form.email_code.data, 1): flash('邮箱验证码错误或已过期', 'error') return render_template('user/register.html', form=form) user = User( username=form.username.data, email=form.email.data, phone=form.phone.data, nickname=form.username.data ) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('注册成功!请登录', 'success') return redirect(url_for('auth.login')) except Exception as e: db.session.rollback() flash('注册失败,请重试', 'error') print(f"注册错误: {e}") return render_template('user/register.html', form=form) @auth_bp.route('/logout') def logout(): """用户登出""" session.clear() flash('您已成功登出', 'info') return redirect(url_for('main.index')) 🔸============================================================================== 📄 文件: app/views/cart.py 📊 大小: 7602 bytes (7.42 KB) 🕒 修改时间: 2025-07-04 02:47:10 🔸============================================================================== """ 购物车视图 """ from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash from app.models.cart import Cart from app.models.product import Product, ProductInventory from app.models.user import User from app.utils.decorators import login_required from config.database import db cart_bp = Blueprint('cart', __name__, url_prefix='/cart') @cart_bp.route('/') @login_required def index(): """购物车页面""" user_id = session['user_id'] cart_items = Cart.get_user_cart(user_id) # 计算总价和可用商品数量 total_price = 0 available_count = 0 for item in cart_items: if item.is_available(): total_price += item.get_total_price() available_count += 1 return render_template('cart/index.html', cart_items=cart_items, total_price=total_price, available_count=available_count) @cart_bp.route('/add', methods=['POST']) @login_required def add(): """添加商品到购物车""" try: user_id = session['user_id'] product_id = request.json.get('product_id') sku_code = request.json.get('sku_code') spec_combination = request.json.get('spec_combination', '') quantity = request.json.get('quantity', 1) # 验证参数 if not product_id or quantity <= 0: return jsonify({'success': False, 'message': '参数错误'}) # 检查商品是否存在且上架 product = Product.query.filter_by(id=product_id, status=1).first() if not product: return jsonify({'success': False, 'message': '商品不存在或已下架'}) # 检查库存 if sku_code: sku_info = ProductInventory.query.filter_by(sku_code=sku_code).first() if not sku_info: return jsonify({'success': False, 'message': 'SKU不存在'}) if sku_info.stock < quantity: return jsonify({'success': False, 'message': f'库存不足,仅剩{sku_info.stock}件'}) else: # 如果没有指定SKU,检查默认库存 default_sku = ProductInventory.query.filter_by( product_id=product_id, is_default=1 ).first() if default_sku and default_sku.stock < quantity: return jsonify({'success': False, 'message': f'库存不足,仅剩{default_sku.stock}件'}) # 添加到购物车 Cart.add_to_cart( user_id=user_id, product_id=product_id, sku_code=sku_code, spec_combination=spec_combination, quantity=quantity ) # 获取购物车数量 cart_count = Cart.get_cart_count(user_id) return jsonify({ 'success': True, 'message': '已添加到购物车', 'cart_count': cart_count }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'添加失败: {str(e)}'}) @cart_bp.route('/update', methods=['POST']) @login_required def update(): """更新购物车商品数量""" try: user_id = session['user_id'] cart_id = request.json.get('cart_id') quantity = request.json.get('quantity') if not cart_id or quantity is None or quantity < 0: return jsonify({'success': False, 'message': '参数错误'}) # 获取购物车项目 cart_item = Cart.query.filter_by(id=cart_id, user_id=user_id).first() if not cart_item: return jsonify({'success': False, 'message': '购物车项目不存在'}) if quantity == 0: # 删除商品 db.session.delete(cart_item) else: # 检查库存 if cart_item.get_stock() < quantity: return jsonify({ 'success': False, 'message': f'库存不足,仅剩{cart_item.get_stock()}件' }) # 更新数量 cart_item.quantity = quantity cart_item.updated_at = db.func.now() db.session.commit() # 返回更新后的信息 cart_count = Cart.get_cart_count(user_id) total_price = Cart.get_cart_total(user_id) return jsonify({ 'success': True, 'message': '更新成功', 'cart_count': cart_count, 'total_price': total_price, 'item_total': cart_item.get_total_price() if quantity > 0 else 0 }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'更新失败: {str(e)}'}) @cart_bp.route('/remove', methods=['POST']) @login_required def remove(): """删除购物车商品""" try: user_id = session['user_id'] cart_id = request.json.get('cart_id') if not cart_id: return jsonify({'success': False, 'message': '参数错误'}) # 获取购物车项目 cart_item = Cart.query.filter_by(id=cart_id, user_id=user_id).first() if not cart_item: return jsonify({'success': False, 'message': '购物车项目不存在'}) db.session.delete(cart_item) db.session.commit() # 返回更新后的信息 cart_count = Cart.get_cart_count(user_id) total_price = Cart.get_cart_total(user_id) return jsonify({ 'success': True, 'message': '删除成功', 'cart_count': cart_count, 'total_price': total_price }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) @cart_bp.route('/clear', methods=['POST']) @login_required def clear(): """清空购物车""" try: user_id = session['user_id'] Cart.query.filter_by(user_id=user_id).delete() db.session.commit() return jsonify({ 'success': True, 'message': '购物车已清空' }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'清空失败: {str(e)}'}) @cart_bp.route('/count') @login_required def count(): """获取购物车商品数量""" user_id = session['user_id'] cart_count = Cart.get_cart_count(user_id) return jsonify({'cart_count': cart_count}) @cart_bp.route('/checkout') @login_required def checkout(): """去结算""" user_id = session['user_id'] selected_items = request.args.getlist('items') if not selected_items: flash('请选择要购买的商品', 'error') return redirect(url_for('cart.index')) # 获取选中的购物车项目 cart_items = Cart.query.filter( Cart.id.in_(selected_items), Cart.user_id == user_id ).all() if not cart_items: flash('选中的商品不存在', 'error') return redirect(url_for('cart.index')) # 检查商品可用性 unavailable_items = [] for item in cart_items: if not item.is_available(): unavailable_items.append(item.product.name) if unavailable_items: flash(f'以下商品库存不足或已下架:{", ".join(unavailable_items)}', 'error') return redirect(url_for('cart.index')) # 跳转到订单结算页面 items_param = '&'.join([f'items={item_id}' for item_id in selected_items]) return redirect(url_for('order.checkout') + '?' + items_param) 🔸============================================================================== 📄 文件: app/views/main.py 📊 大小: 6561 bytes (6.41 KB) 🕒 修改时间: 2025-07-03 15:30:57 🔸============================================================================== """ 主页面视图 """ from flask import Blueprint, render_template, session, current_app, request, redirect, url_for from app.models.user import User from app.models.product import Product, Category from sqlalchemy import func main_bp = Blueprint('main', __name__) @main_bp.route('/') def index(): """首页""" user = None if 'user_id' in session: try: user = User.query.get(session['user_id']) if user and user.status != 1: # 用户被禁用,清除session session.pop('user_id', None) user = None except Exception as e: current_app.logger.error(f"获取用户信息失败: {str(e)}") session.pop('user_id', None) user = None # 获取热门商品(按销量排序,取前8个) hot_products = Product.query.filter_by(status=1)\ .order_by(Product.sales_count.desc())\ .limit(8).all() # 获取最新商品(按创建时间排序,取前8个) new_products = Product.query.filter_by(status=1)\ .order_by(Product.created_at.desc())\ .limit(8).all() # 获取活跃的顶级分类(用于导航) top_categories = Category.query.filter_by(is_active=1, parent_id=0)\ .order_by(Category.sort_order)\ .limit(6).all() return render_template('index.html', user=user, hot_products=hot_products, new_products=new_products, top_categories=top_categories) @main_bp.route('/products') def product_list(): """商品列表页面""" page = request.args.get('page', 1, type=int) per_page = 20 # 基础查询:只显示上架商品 query = Product.query.filter_by(status=1) # 分类筛选 category_id = request.args.get('category_id', type=int) if category_id: # 获取该分类及其所有子分类的商品 category = Category.query.get_or_404(category_id) if category.level == 1: # 一级分类,查找所有子分类 subcategory_ids = [c.id for c in Category.query.filter_by(parent_id=category_id).all()] subcategory_ids.append(category_id) query = query.filter(Product.category_id.in_(subcategory_ids)) else: query = query.filter_by(category_id=category_id) # 搜索功能 search = request.args.get('search', '').strip() if search: query = query.filter(Product.name.like(f'%{search}%')) # 价格筛选 min_price = request.args.get('min_price', type=float) max_price = request.args.get('max_price', type=float) if min_price is not None: query = query.filter(Product.price >= min_price) if max_price is not None: query = query.filter(Product.price <= max_price) # 排序 sort = request.args.get('sort', 'default') if sort == 'price_asc': query = query.order_by(Product.price.asc()) elif sort == 'price_desc': query = query.order_by(Product.price.desc()) elif sort == 'sales': query = query.order_by(Product.sales_count.desc()) elif sort == 'newest': query = query.order_by(Product.created_at.desc()) else: # default query = query.order_by(Product.created_at.desc()) # 分页 products = query.paginate(page=page, per_page=per_page, error_out=False) # 获取所有分类用于侧边栏 categories = Category.query.filter_by(is_active=1, parent_id=0)\ .order_by(Category.sort_order).all() # 当前分类信息 current_category = None if category_id: current_category = Category.query.get(category_id) return render_template('product/list.html', products=products, categories=categories, current_category=current_category, search=search, category_id=category_id, sort=sort, min_price=min_price, max_price=max_price) @main_bp.route('/products/') def product_detail(product_id): """商品详情页面""" product = Product.query.filter_by(id=product_id, status=1).first_or_404() # 增加浏览量 try: product.view_count += 1 from config.database import db db.session.commit() except Exception as e: current_app.logger.error(f"更新浏览量失败: {str(e)}") # 获取商品图片(按排序) images = product.images if images: images = sorted(images, key=lambda x: x.sort_order) # 获取商品库存信息并转换为字典 inventory_list = product.inventory inventory_data = [] if inventory_list: for inventory in inventory_list: inventory_data.append({ 'id': inventory.id, 'sku_code': inventory.sku_code, 'spec_combination': inventory.spec_combination, 'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0, 'stock': inventory.stock, 'warning_stock': inventory.warning_stock, 'is_default': inventory.is_default, 'status': inventory.status, 'final_price': inventory.get_final_price() }) # 获取推荐商品(同分类的其他商品) recommended_products = Product.query.filter( Product.category_id == product.category_id, Product.id != product.id, Product.status == 1 ).order_by(Product.sales_count.desc()).limit(4).all() return render_template('product/detail.html', product=product, images=images, inventory_list=inventory_list, inventory_data=inventory_data, recommended_products=recommended_products) @main_bp.route('/category/') def category_products(category_id): """分类商品页面(重定向到商品列表)""" return redirect(url_for('main.product_list', category_id=category_id)) @main_bp.route('/search') def search(): """搜索页面(重定向到商品列表)""" search_query = request.args.get('q', '').strip() return redirect(url_for('main.product_list', search=search_query)) @main_bp.route('/about') def about(): """关于我们""" return render_template('about.html') 🔸============================================================================== 📄 文件: app/views/order.py 📊 大小: 10973 bytes (10.72 KB) 🕒 修改时间: 2025-07-04 02:27:02 🔸============================================================================== """ 订单视图 """ from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash, g from app.models.order import Order, OrderItem from app.models.cart import Cart from app.models.address import UserAddress from app.models.product import ProductInventory from app.models.payment import Payment from app.forms import CheckoutForm from app.utils.decorators import login_required from config.database import db import json order_bp = Blueprint('order', __name__, url_prefix='/order') @order_bp.route('/checkout') @login_required def checkout(): """订单结算页面""" user_id = session['user_id'] selected_items = request.args.getlist('items') if not selected_items: flash('请选择要购买的商品', 'error') return redirect(url_for('cart.index')) # 获取选中的购物车项目 cart_items = Cart.query.filter( Cart.id.in_(selected_items), Cart.user_id == user_id ).all() if not cart_items: flash('选中的商品不存在', 'error') return redirect(url_for('cart.index')) # 检查商品可用性和库存 unavailable_items = [] total_amount = 0 for item in cart_items: if not item.is_available(): unavailable_items.append(item.product.name) else: total_amount += item.get_total_price() if unavailable_items: flash(f'以下商品库存不足或已下架:{", ".join(unavailable_items)}', 'error') return redirect(url_for('cart.index')) # 获取用户地址 addresses = UserAddress.get_user_addresses(user_id) if not addresses: flash('请先添加收货地址', 'warning') return redirect(url_for('address.add')) # 计算运费 shipping_fee = 0 # 默认免运费 # 创建表单并设置地址选项 form = CheckoutForm() form.address_id.choices = [(addr.id, f"{addr.receiver_name} - {addr.get_full_address()}") for addr in addresses] # 设置默认地址 default_address = UserAddress.get_default_address(user_id) if default_address: form.address_id.data = default_address.id return render_template('order/checkout.html', cart_items=cart_items, addresses=addresses, form=form, total_amount=total_amount, shipping_fee=shipping_fee, final_amount=total_amount + shipping_fee) @order_bp.route('/create', methods=['POST']) @login_required def create(): """创建订单""" try: user_id = session['user_id'] data = request.get_json() selected_items = data.get('selected_items', []) address_id = data.get('address_id') shipping_method = data.get('shipping_method', 'standard') payment_method = data.get('payment_method', 'wechat') remark = data.get('remark', '') if not selected_items or not address_id: return jsonify({'success': False, 'message': '参数错误'}) # 获取购物车商品 cart_items = Cart.query.filter( Cart.id.in_(selected_items), Cart.user_id == user_id ).all() if not cart_items: return jsonify({'success': False, 'message': '购物车商品不存在'}) # 验证地址 address = UserAddress.query.filter_by(id=address_id, user_id=user_id).first() if not address: return jsonify({'success': False, 'message': '收货地址不存在'}) # 再次检查库存和计算总价 total_amount = 0 order_items_data = [] for cart_item in cart_items: if not cart_item.is_available(): return jsonify({ 'success': False, 'message': f'商品"{cart_item.product.name}"库存不足或已下架' }) # 检查库存是否足够 current_stock = cart_item.get_stock() if current_stock < cart_item.quantity: return jsonify({ 'success': False, 'message': f'商品"{cart_item.product.name}"库存不足,仅剩{current_stock}件' }) item_total = cart_item.get_total_price() total_amount += item_total order_items_data.append({ 'product_id': cart_item.product_id, 'sku_code': cart_item.sku_code, 'product_name': cart_item.product.name, 'product_image': cart_item.product.main_image, 'spec_combination': cart_item.spec_combination, 'price': cart_item.get_price(), 'quantity': cart_item.quantity, 'total_price': item_total }) # 计算运费 shipping_fee_map = { 'standard': 0, 'express': 10, 'same_day': 20 } shipping_fee = shipping_fee_map.get(shipping_method, 0) actual_amount = total_amount + shipping_fee # 创建订单 order = Order( user_id=user_id, order_sn=Order.generate_order_sn(), total_amount=total_amount, actual_amount=actual_amount, shipping_fee=shipping_fee, payment_method=payment_method, shipping_method=shipping_method, remark=remark ) # 设置收货人信息 order.set_receiver_info({ 'receiver_name': address.receiver_name, 'receiver_phone': address.receiver_phone, 'province': address.province, 'city': address.city, 'district': address.district, 'detail_address': address.detail_address, 'postal_code': address.postal_code, 'full_address': address.get_full_address() }) db.session.add(order) db.session.flush() # 获取订单ID # 创建订单商品明细 for item_data in order_items_data: order_item = OrderItem( order_id=order.id, **item_data ) db.session.add(order_item) # 扣减库存 for cart_item in cart_items: if cart_item.sku_code: sku_info = ProductInventory.query.filter_by(sku_code=cart_item.sku_code).first() if sku_info: sku_info.stock -= cart_item.quantity # 增加销量 cart_item.product.sales_count += cart_item.quantity # 删除购物车商品 for cart_item in cart_items: db.session.delete(cart_item) # 创建支付记录 payment = Payment( order_id=order.id, payment_sn=Payment.generate_payment_sn(), payment_method=payment_method, amount=actual_amount ) db.session.add(payment) db.session.commit() return jsonify({ 'success': True, 'message': '订单创建成功', 'order_id': order.id, 'order_sn': order.order_sn, 'payment_sn': payment.payment_sn }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'创建订单失败: {str(e)}'}) @order_bp.route('/list') @login_required def list(): """订单列表""" user_id = session['user_id'] status = request.args.get('status', type=int) page = request.args.get('page', 1, type=int) per_page = 10 query = Order.query.filter_by(user_id=user_id) if status: query = query.filter_by(status=status) orders = query.order_by(Order.created_at.desc()).paginate( page=page, per_page=per_page, error_out=False ) return render_template('user/orders.html', orders=orders, current_status=status) @order_bp.route('/detail/') @login_required def detail(order_id): """订单详情""" user_id = session['user_id'] order = Order.query.filter_by(id=order_id, user_id=user_id).first_or_404() return render_template('order/detail.html', order=order) @order_bp.route('/cancel/', methods=['POST']) @login_required def cancel(order_id): """取消订单""" try: user_id = session['user_id'] order = Order.query.filter_by(id=order_id, user_id=user_id).first() if not order: return jsonify({'success': False, 'message': '订单不存在'}) if not order.can_cancel(): return jsonify({'success': False, 'message': '订单状态不允许取消'}) # 更新订单状态 order.status = Order.STATUS_CANCELLED # 恢复库存 for item in order.order_items: if item.sku_code: sku_info = ProductInventory.query.filter_by(sku_code=item.sku_code).first() if sku_info: sku_info.stock += item.quantity # 减少销量 if item.product: item.product.sales_count = max(0, item.product.sales_count - item.quantity) db.session.commit() return jsonify({'success': True, 'message': '订单已取消'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'取消失败: {str(e)}'}) @order_bp.route('/confirm_receipt/', methods=['POST']) @login_required def confirm_receipt(order_id): """确认收货""" try: user_id = session['user_id'] order = Order.query.filter_by(id=order_id, user_id=user_id).first() if not order: return jsonify({'success': False, 'message': '订单不存在'}) if not order.can_confirm_receipt(): return jsonify({'success': False, 'message': '订单状态不允许确认收货'}) # 更新订单状态 order.status = Order.STATUS_PENDING_REVIEW order.received_at = db.func.now() db.session.commit() return jsonify({'success': True, 'message': '确认收货成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'确认收货失败: {str(e)}'}) @order_bp.route('/pay/') @login_required def pay(payment_sn): """支付页面""" user_id = session['user_id'] payment = Payment.query.filter_by(payment_sn=payment_sn).first_or_404() order = payment.order # 验证订单所有权 if order.user_id != user_id: flash('订单不存在', 'error') return redirect(url_for('order.list')) # 检查是否可以支付 if not order.can_pay(): flash('订单不可支付', 'error') return redirect(url_for('order.detail', order_id=order.id)) return render_template('order/pay.html', order=order, payment=payment) 🔸============================================================================== 📄 文件: app/views/payment.py 📊 大小: 6712 bytes (6.55 KB) 🕒 修改时间: 2025-07-04 02:27:33 🔸============================================================================== """ 支付视图 """ from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for, flash from app.models.payment import Payment from app.models.order import Order from app.utils.decorators import login_required from config.database import db from datetime import datetime payment_bp = Blueprint('payment', __name__, url_prefix='/payment') @payment_bp.route('/process', methods=['POST']) @login_required def process(): """处理支付请求""" try: user_id = session['user_id'] payment_sn = request.json.get('payment_sn') payment_method = request.json.get('payment_method') if not payment_sn: return jsonify({'success': False, 'message': '支付流水号不能为空'}) # 获取支付记录 payment = Payment.query.filter_by(payment_sn=payment_sn).first() if not payment: return jsonify({'success': False, 'message': '支付记录不存在'}) order = payment.order if order.user_id != user_id: return jsonify({'success': False, 'message': '订单不存在'}) if not order.can_pay(): return jsonify({'success': False, 'message': '订单不可支付'}) # 根据支付方式处理 if payment_method == 'wechat': # 微信支付 result = process_wechat_pay(payment) elif payment_method == 'alipay': # 支付宝支付 result = process_alipay(payment) elif payment_method == 'bank': # 银行卡支付 result = process_bank_pay(payment) else: return jsonify({'success': False, 'message': '不支持的支付方式'}) return jsonify(result) except Exception as e: return jsonify({'success': False, 'message': f'支付处理失败: {str(e)}'}) def process_wechat_pay(payment): """处理微信支付""" # TODO: 接入真实的微信支付API # 目前返回模拟的支付二维码 # 模拟生成支付二维码数据 qr_code_url = f"weixin://wxpay/bizpayurl?pr={payment.payment_sn}" return { 'success': True, 'payment_type': 'qrcode', 'qr_code_url': qr_code_url, 'payment_sn': payment.payment_sn, 'amount': float(payment.amount), 'message': '请使用微信扫码支付' } def process_alipay(payment): """处理支付宝支付""" # TODO: 接入真实的支付宝API # 目前返回模拟的跳转链接 pay_url = f"https://mapi.alipay.com/gateway.do?service=create_direct_pay_by_user&payment_sn={payment.payment_sn}" return { 'success': True, 'payment_type': 'redirect', 'pay_url': pay_url, 'payment_sn': payment.payment_sn, 'amount': float(payment.amount), 'message': '正在跳转到支付宝...' } def process_bank_pay(payment): """处理银行卡支付""" # TODO: 接入银行支付网关 # 目前返回模拟的网银链接 bank_url = f"https://pay.bank.com/pay?order={payment.payment_sn}" return { 'success': True, 'payment_type': 'redirect', 'pay_url': bank_url, 'payment_sn': payment.payment_sn, 'amount': float(payment.amount), 'message': '正在跳转到网银...' } @payment_bp.route('/callback/wechat', methods=['POST']) def wechat_callback(): """微信支付回调""" try: # TODO: 验证微信支付回调签名 # 目前模拟处理 callback_data = request.get_data() # 解析回调数据,获取支付结果 # 模拟成功的回调处理 return handle_payment_success(request.form.get('payment_sn'), 'wechat_success_' + str(datetime.now().timestamp())) except Exception as e: return f"FAIL: {str(e)}" @payment_bp.route('/callback/alipay', methods=['POST']) def alipay_callback(): """支付宝支付回调""" try: # TODO: 验证支付宝回调签名 # 目前模拟处理 return handle_payment_success(request.form.get('payment_sn'), 'alipay_success_' + str(datetime.now().timestamp())) except Exception as e: return f"FAIL: {str(e)}" def handle_payment_success(payment_sn, third_party_sn): """处理支付成功""" try: payment = Payment.query.filter_by(payment_sn=payment_sn).first() if not payment: return "FAIL: Payment not found" if payment.status == Payment.STATUS_SUCCESS: return "SUCCESS" # 已经处理过的支付 # 更新支付状态 payment.status = Payment.STATUS_SUCCESS payment.third_party_sn = third_party_sn payment.paid_at = datetime.utcnow() # 更新订单状态 order = payment.order order.status = Order.STATUS_PENDING_SHIPMENT db.session.commit() return "SUCCESS" except Exception as e: db.session.rollback() return f"FAIL: {str(e)}" @payment_bp.route('/check_status/') @login_required def check_status(payment_sn): """检查支付状态""" try: user_id = session['user_id'] payment = Payment.query.filter_by(payment_sn=payment_sn).first() if not payment or payment.order.user_id != user_id: return jsonify({'success': False, 'message': '支付记录不存在'}) return jsonify({ 'success': True, 'status': payment.status, 'status_text': payment.get_status_text(), 'paid_at': payment.paid_at.isoformat() if payment.paid_at else None }) except Exception as e: return jsonify({'success': False, 'message': f'查询失败: {str(e)}'}) @payment_bp.route('/simulate_success/', methods=['POST']) @login_required def simulate_success(payment_sn): """模拟支付成功(开发测试用)""" try: user_id = session['user_id'] payment = Payment.query.filter_by(payment_sn=payment_sn).first() if not payment or payment.order.user_id != user_id: return jsonify({'success': False, 'message': '支付记录不存在'}) if payment.status == Payment.STATUS_SUCCESS: return jsonify({'success': False, 'message': '订单已支付'}) # 模拟支付成功 result = handle_payment_success(payment_sn, f'SIMULATE_{datetime.now().timestamp()}') if result == "SUCCESS": return jsonify({'success': True, 'message': '支付成功'}) else: return jsonify({'success': False, 'message': result}) except Exception as e: return jsonify({'success': False, 'message': f'模拟支付失败: {str(e)}'}) 🔸============================================================================== 📄 文件: app/views/product.py 📊 大小: 23747 bytes (23.19 KB) 🕒 修改时间: 2025-07-03 15:17:18 🔸============================================================================== """ 商品管理视图 """ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, g from werkzeug.utils import secure_filename from app.models.product import Product, Category, ProductImage, SpecName, SpecValue, ProductInventory from app.models.admin import AdminUser from app.utils.decorators import admin_required, log_operation from app.utils.cos_client import cos_client from config.database import db from sqlalchemy import func import time import uuid import json product_bp = Blueprint('product', __name__, url_prefix='/admin/products') @product_bp.route('/') @admin_required def index(): """商品列表""" page = request.args.get('page', 1, type=int) per_page = 20 query = Product.query.order_by(Product.created_at.desc()) # 搜索功能 search = request.args.get('search', '').strip() if search: query = query.filter(Product.name.like(f'%{search}%')) # 分类筛选 category_id = request.args.get('category_id', '', type=str) if category_id: query = query.filter(Product.category_id == int(category_id)) # 状态筛选 status = request.args.get('status', '', type=str) if status: query = query.filter(Product.status == int(status)) products = query.paginate(page=page, per_page=per_page, error_out=False) # 获取所有分类用于筛选 categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all() return render_template('admin/products.html', products=products, categories=categories, search=search, category_id=category_id, status=status) @product_bp.route('/add') @admin_required def add(): """添加商品页面""" categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all() spec_names = SpecName.query.order_by(SpecName.sort_order).all() return render_template('admin/product_form.html', product=None, categories=categories, spec_names=spec_names) @product_bp.route('/edit/') @admin_required def edit(product_id): """编辑商品页面""" product = Product.query.get_or_404(product_id) categories = Category.query.filter_by(is_active=1).order_by(Category.sort_order).all() spec_names = SpecName.query.order_by(SpecName.sort_order).all() return render_template('admin/product_form.html', product=product, categories=categories, spec_names=spec_names) @product_bp.route('/save', methods=['POST']) @admin_required @log_operation('保存商品信息', 'product') def save(): """保存商品信息""" try: product_id = request.form.get('product_id', type=int) # 基本信息 name = request.form.get('name', '').strip() category_id = request.form.get('category_id', type=int) brand = request.form.get('brand', '').strip() price = request.form.get('price', type=float) original_price = request.form.get('original_price', type=float) description = request.form.get('description', '').strip() weight = request.form.get('weight', type=float) status = request.form.get('status', 1, type=int) # 验证必填字段 if not name or not category_id or not price: flash('请填写完整的商品基本信息', 'error') return redirect(request.referrer) # 创建或更新商品 if product_id: product = Product.query.get_or_404(product_id) else: product = Product() product.name = name product.category_id = category_id product.brand = brand product.price = price product.original_price = original_price product.description = description product.weight = weight product.status = status # 处理库存类型 inventory_type = request.form.get('inventory_type', 'single') if inventory_type == 'single': product.has_specs = 0 else: product.has_specs = 1 if not product_id: db.session.add(product) db.session.flush() # 获取product.id # 处理库存信息 if inventory_type == 'single': # 单规格处理 single_stock = request.form.get('single_stock', 0, type=int) warning_stock = request.form.get('warning_stock', 10, type=int) # 删除现有库存记录(如果是编辑模式) if product_id: ProductInventory.query.filter_by(product_id=product.id).delete() # 创建单个SKU sku_code = f"{product.name[:3].upper()}-DEFAULT-{product.id}" inventory = ProductInventory( product_id=product.id, sku_code=sku_code, spec_combination=None, price_adjustment=0, stock=single_stock, warning_stock=warning_stock, is_default=1, status=1 ) db.session.add(inventory) else: # 多规格处理 sku_codes = request.form.getlist('sku_codes[]') spec_combinations = request.form.getlist('spec_combinations[]') price_adjustments = request.form.getlist('price_adjustments[]') stocks = request.form.getlist('stocks[]') warning_stocks = request.form.getlist('warning_stocks[]') default_sku_index = request.form.get('default_sku', 0, type=int) if not sku_codes: flash('请至少添加一个SKU', 'error') return redirect(request.referrer) # 删除现有库存记录(如果是编辑模式) if product_id: ProductInventory.query.filter_by(product_id=product.id).delete() # 创建多个SKU for i, sku_code in enumerate(sku_codes): try: spec_combination = json.loads(spec_combinations[i]) if spec_combinations[i] else None except: spec_combination = None inventory = ProductInventory( product_id=product.id, sku_code=sku_code, spec_combination=spec_combination, price_adjustment=float(price_adjustments[i]) if price_adjustments[i] else 0, stock=int(stocks[i]) if stocks[i] else 0, warning_stock=int(warning_stocks[i]) if warning_stocks[i] else 10, is_default=1 if i == default_sku_index else 0, status=1 ) db.session.add(inventory) db.session.commit() flash('商品信息保存成功', 'success') return redirect(url_for('product.edit', product_id=product.id)) except Exception as e: db.session.rollback() flash(f'保存失败: {str(e)}', 'error') return redirect(request.referrer) @product_bp.route('/upload-images/', methods=['POST']) @admin_required @log_operation('上传商品图片', 'productdef index():') def upload_images(product_id): """上传商品图片""" try: product = Product.query.get_or_404(product_id) if 'files' not in request.files: return jsonify({'success': False, 'message': '没有选择文件'}) files = request.files.getlist('files') uploaded_images = [] for file in files: if file.filename == '': continue # 检查文件类型 allowed_extensions = {'jpg', 'jpeg', 'png', 'gif', 'webp'} file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else '' if file_ext not in allowed_extensions: continue # 上传到COS result = cos_client.upload_file(file, 'product', file.filename) if result['success']: # 检查是否是第一张图片 existing_images_count = ProductImage.query.filter_by(product_id=product_id).count() is_first_image = (existing_images_count == 0) # 保存图片记录 image = ProductImage( product_id=product_id, image_url=result['url'], sort_order=existing_images_count, is_main=1 if is_first_image else 0 # 第一张图片自动设为主图 ) db.session.add(image) # 如果是第一张图片,同时更新商品主图 if is_first_image: product.main_image = result['url'] uploaded_images.append({ 'id': None, # 临时ID,提交后会更新 'url': result['url'], 'sort_order': image.sort_order, 'is_main': is_first_image }) db.session.commit() # 更新图片ID for i, uploaded_image in enumerate(uploaded_images): image = ProductImage.query.filter_by( product_id=product_id, image_url=uploaded_image['url'] ).first() if image: uploaded_images[i]['id'] = image.id return jsonify({ 'success': True, 'message': f'成功上传 {len(uploaded_images)} 张图片', 'images': uploaded_images }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'上传失败: {str(e)}'}) @product_bp.route('/delete-image/', methods=['DELETE']) @admin_required @log_operation('删除商品图片', 'product_image') def delete_image(image_id): """删除商品图片""" try: image = ProductImage.query.get_or_404(image_id) # 从COS删除文件 if image.image_url: file_key = image.image_url.split('/')[-4:] # 提取文件路径 file_key = '/'.join(file_key) cos_client.delete_file(file_key) db.session.delete(image) db.session.commit() return jsonify({'success': True, 'message': '图片删除成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) @product_bp.route('/set-main-image/', methods=['POST']) @admin_required @log_operation('设置主图', 'product') def set_main_image(image_id): """设置主图""" try: image = ProductImage.query.get_or_404(image_id) product = image.product # 清除当前主图 ProductImage.query.filter_by(product_id=product.id, is_main=1).update({'is_main': 0}) # 设置新主图 image.is_main = 1 product.main_image = image.image_url db.session.commit() return jsonify({'success': True, 'message': '主图设置成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'设置失败: {str(e)}'}) @product_bp.route('/sort-images/', methods=['POST']) @admin_required @log_operation('排序商品图片', 'product') def sort_images(product_id): """图片排序""" try: image_ids = request.json.get('image_ids', []) for index, image_id in enumerate(image_ids): ProductImage.query.filter_by(id=image_id).update({'sort_order': index}) db.session.commit() return jsonify({'success': True, 'message': '排序保存成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'排序失败: {str(e)}'}) @product_bp.route('/delete/', methods=['DELETE']) @admin_required @log_operation('删除商品', 'product') def delete(product_id): """删除商品""" try: product = Product.query.get_or_404(product_id) # 删除商品图片 for image in product.images: if image.image_url: file_key = image.image_url.split('/')[-4:] file_key = '/'.join(file_key) cos_client.delete_file(file_key) # 删除商品记录 db.session.delete(product) db.session.commit() return jsonify({'success': True, 'message': '商品删除成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) # 分类管理相关路由 @product_bp.route('/categories') @admin_required def categories(): """分类管理""" categories = Category.query.order_by(Category.sort_order).all() return render_template('admin/categories.html', categories=categories) @product_bp.route('/categories/save', methods=['POST']) @admin_required @log_operation('保存商品分类', 'category') def save_category(): """保存分类""" try: category_id = request.form.get('category_id', type=int) name = request.form.get('name', '').strip() parent_id = request.form.get('parent_id', 0, type=int) sort_order = request.form.get('sort_order', 0, type=int) is_active = request.form.get('is_active', 1, type=int) if not name: flash('分类名称不能为空', 'error') return redirect(url_for('product.categories')) # 检查分类名称是否重复 existing = Category.query.filter_by(name=name, parent_id=parent_id).first() if existing and (not category_id or existing.id != category_id): flash('同一层级下分类名称不能重复', 'error') return redirect(url_for('product.categories')) if category_id: category = Category.query.get_or_404(category_id) # 防止将分类设为自己的子分类 if parent_id == category_id: flash('不能将分类设为自己的子分类', 'error') return redirect(url_for('product.categories')) # 防止循环引用 if parent_id != 0: parent = Category.query.get(parent_id) temp_parent = parent while temp_parent and temp_parent.parent_id != 0: if temp_parent.parent_id == category_id: flash('不能创建循环引用的分类层级', 'error') return redirect(url_for('product.categories')) temp_parent = Category.query.get(temp_parent.parent_id) else: category = Category() category.name = name category.parent_id = parent_id category.sort_order = sort_order category.is_active = is_active # 设置层级 if parent_id == 0: category.level = 1 else: parent = Category.query.get(parent_id) if parent: category.level = parent.level + 1 if category.level > 3: flash('分类层级不能超过3级', 'error') return redirect(url_for('product.categories')) else: category.level = 1 # 处理图标上传 if 'icon' in request.files: icon_file = request.files['icon'] if icon_file and icon_file.filename: # 删除旧图标 if category.icon_url: old_file_key = category.icon_url.split('/')[-4:] old_file_key = '/'.join(old_file_key) cos_client.delete_file(old_file_key) # 上传新图标 result = cos_client.upload_file(icon_file, 'category', icon_file.filename) if result['success']: category.icon_url = result['url'] else: flash(f'图标上传失败: {result["error"]}', 'error') return redirect(url_for('product.categories')) if not category_id: db.session.add(category) db.session.commit() flash('分类保存成功', 'success') except Exception as e: db.session.rollback() flash(f'保存失败: {str(e)}', 'error') return redirect(url_for('product.categories')) @product_bp.route('/categories/', methods=['GET']) @admin_required def get_category(category_id): """获取分类详情""" try: category = Category.query.get_or_404(category_id) return jsonify({ 'success': True, 'category': category.to_dict() }) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @product_bp.route('/categories/', methods=['DELETE']) @admin_required @log_operation('删除商品分类', 'category') def delete_category(category_id): """删除分类""" try: category = Category.query.get_or_404(category_id) # 检查是否有子分类 children = Category.query.filter_by(parent_id=category_id).count() if children > 0: return jsonify({'success': False, 'message': '该分类下还有子分类,无法删除'}) # 检查是否有商品使用此分类 products = Product.query.filter_by(category_id=category_id).count() if products > 0: return jsonify({'success': False, 'message': f'该分类下还有 {products} 个商品,无法删除'}) # 删除分类图标 if category.icon_url: file_key = category.icon_url.split('/')[-4:] file_key = '/'.join(file_key) cos_client.delete_file(file_key) db.session.delete(category) db.session.commit() return jsonify({'success': True, 'message': '分类删除成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) @product_bp.route('/categories/sort', methods=['POST']) @admin_required @log_operation('分类排序', 'category') def sort_categories(): """分类排序""" try: category_orders = request.json.get('orders', []) for item in category_orders: category_id = item.get('id') sort_order = item.get('sort_order') Category.query.filter_by(id=category_id).update({'sort_order': sort_order}) db.session.commit() return jsonify({'success': True, 'message': '排序保存成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'排序失败: {str(e)}'}) # 库存管理相关路由 @product_bp.route('/inventory/') @admin_required def get_inventory(product_id): """获取商品库存详情""" try: product = Product.query.get_or_404(product_id) inventory_list = ProductInventory.query.filter_by(product_id=product_id) \ .order_by(ProductInventory.is_default.desc(), ProductInventory.id).all() inventory_data = [] for inventory in inventory_list: inventory_data.append({ 'id': inventory.id, 'sku_code': inventory.sku_code, 'spec_combination': inventory.spec_combination, 'stock': inventory.stock, 'warning_stock': inventory.warning_stock, 'price_adjustment': float(inventory.price_adjustment) if inventory.price_adjustment else 0, 'is_default': inventory.is_default, 'status': inventory.status, 'final_price': inventory.get_final_price() }) return jsonify({ 'success': True, 'inventory': inventory_data, 'product_name': product.name }) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @product_bp.route('/inventory/update', methods=['POST']) @admin_required @log_operation('更新库存', 'inventory') def update_inventory(): """批量更新库存""" try: inventory_data = request.json.get('inventory_list', []) for item in inventory_data: inventory_id = item.get('id') new_stock = item.get('stock') if inventory_id and new_stock is not None: inventory = ProductInventory.query.get(inventory_id) if inventory: old_stock = inventory.stock inventory.stock = new_stock # 记录库存变更日志 from app.models.product import InventoryLog InventoryLog.create_log( product_id=inventory.product_id, sku_code=inventory.sku_code, change_type=3, # 调整 change_quantity=new_stock - old_stock, before_stock=old_stock, after_stock=new_stock, remark='管理员手动调整' ) db.session.commit() return jsonify({'success': True, 'message': '库存更新成功'}) except Exception as e: db.session.rollback() return jsonify({'success': False, 'message': f'更新失败: {str(e)}'}) @product_bp.route('/inventory/log/') @admin_required def inventory_log(product_id): """查看库存变更日志""" product = Product.query.get_or_404(product_id) page = request.args.get('page', 1, type=int) per_page = 20 from app.models.product import InventoryLog logs = InventoryLog.query.filter_by(product_id=product_id) \ .order_by(InventoryLog.created_at.desc()) \ .paginate(page=page, per_page=per_page, error_out=False) return render_template('admin/inventory_log.html', product=product, logs=logs) @product_bp.route('/generate-sku-code', methods=['POST']) @admin_required def generate_sku_code(): """生成SKU编码""" try: product_name = request.json.get('product_name', '') spec_combination = request.json.get('spec_combination', {}) # 生成SKU编码逻辑 short_name = product_name[:3].upper() if product_name else 'PRD' spec_code = ''.join([v[:2].upper() for v in spec_combination.values()]) if spec_combination else 'DEFAULT' timestamp = str(int(time.time()))[-4:] sku_code = f"{short_name}-{spec_code}-{timestamp}" return jsonify({'success': True, 'sku_code': sku_code}) except Exception as e: return jsonify({'success': False, 'message': str(e)}) @product_bp.route('/check-sku-code', methods=['POST']) @admin_required def check_sku_code(): """检查SKU编码是否重复""" try: sku_code = request.json.get('sku_code', '') product_id = request.json.get('product_id', None) query = ProductInventory.query.filter_by(sku_code=sku_code) if product_id: query = query.filter(ProductInventory.product_id != product_id) exists = query.first() is not None return jsonify({ 'success': True, 'exists': exists, 'message': 'SKU编码已存在' if exists else 'SKU编码可用' }) except Exception as e: return jsonify({'success': False, 'message': str(e)}) 🔸============================================================================== 📄 文件: app/views/upload.py 📊 大小: 5464 bytes (5.34 KB) 🕒 修改时间: 2025-07-03 04:48:37 🔸============================================================================== """ 文件上传视图 """ from flask import Blueprint, request, jsonify, session, current_app from werkzeug.utils import secure_filename from app.utils.decorators import login_required from app.models.user import User from app.utils.cos_client import cos_client from config.database import db from config.cos_config import COSConfig import os upload_bp = Blueprint('upload', __name__) @upload_bp.route('/avatar', methods=['POST']) @login_required def upload_avatar(): """ 上传用户头像 """ try: # 检查是否有文件 if 'avatar' not in request.files: return jsonify({ 'success': False, 'message': '没有选择文件' }), 400 file = request.files['avatar'] # 检查文件名 if file.filename == '': return jsonify({ 'success': False, 'message': '没有选择文件' }), 400 # 验证文件类型 if not allowed_file(file.filename, COSConfig.ALLOWED_IMAGE_EXTENSIONS): return jsonify({ 'success': False, 'message': f'不支持的文件格式,只支持: {", ".join(COSConfig.ALLOWED_IMAGE_EXTENSIONS)}' }), 400 # 验证文件大小 file.seek(0, 2) # 移动到文件末尾 file_size = file.tell() file.seek(0) # 重置文件指针 if file_size > COSConfig.MAX_IMAGE_SIZE: size_mb = COSConfig.MAX_IMAGE_SIZE / 1024 / 1024 return jsonify({ 'success': False, 'message': f'文件大小超过限制,最大允许 {size_mb:.1f}MB' }), 400 # 获取当前用户 user = User.query.get(session['user_id']) if not user: return jsonify({ 'success': False, 'message': '用户不存在' }), 404 # 上传到COS upload_result = cos_client.upload_file( file_obj=file, folder_type='avatar', original_filename=file.filename ) if not upload_result['success']: current_app.logger.error(f"COS上传失败: {upload_result['error']}") return jsonify({ 'success': False, 'message': '文件上传失败,请重试' }), 500 # 删除旧头像(如果存在) if user.avatar_url: try: # 从URL中提取文件路径 old_file_key = extract_file_key_from_url(user.avatar_url) if old_file_key: cos_client.delete_file(old_file_key) current_app.logger.info(f"删除旧头像: {old_file_key}") except Exception as e: current_app.logger.warning(f"删除旧头像失败: {str(e)}") # 更新用户头像URL user.avatar_url = upload_result['url'] db.session.commit() current_app.logger.info(f"用户 {user.username} 头像上传成功: {upload_result['file_key']}") return jsonify({ 'success': True, 'message': '头像上传成功', 'avatar_url': upload_result['url'], 'file_key': upload_result['file_key'] }) except Exception as e: current_app.logger.error(f"头像上传异常: {str(e)}") db.session.rollback() return jsonify({ 'success': False, 'message': '服务器内部错误' }), 500 def allowed_file(filename, allowed_extensions): """ 检查文件扩展名是否允许 """ return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in allowed_extensions def extract_file_key_from_url(url): """ 从COS URL中提取文件路径 """ try: if not url: return None # 移除域名部分,只保留文件路径 if COSConfig.BUCKET_DOMAIN in url: return url.split(COSConfig.BUCKET_DOMAIN + '/')[-1] return None except Exception: return None @upload_bp.route('/test', methods=['GET', 'POST']) @login_required def test_upload(): """ 测试上传功能 """ if request.method == 'GET': return ''' 测试上传

测试文件上传

''' # POST 请求处理 if 'test_file' not in request.files: return '没有文件' file = request.files['test_file'] if file.filename == '': return '没有选择文件' # 上传到COS result = cos_client.upload_file( file_obj=file, folder_type='temp', original_filename=file.filename ) if result['success']: return f'''

上传成功!

文件路径: {result['file_key']}

访问URL: {result['url']}

''' else: return f'上传失败: {result["error"]}' 🔸============================================================================== 📄 文件: app/views/user.py 📊 大小: 1046 bytes (1.02 KB) 🕒 修改时间: 2025-07-03 03:00:41 🔸============================================================================== from flask import Blueprint, render_template, session, redirect, url_for, flash from app.models.user import User user_bp = Blueprint('user', __name__, url_prefix='/user') def login_required(f): """登录验证装饰器""" def decorated_function(*args, **kwargs): if 'user_id' not in session: flash('请先登录', 'warning') return redirect(url_for('auth.login')) return f(*args, **kwargs) decorated_function.__name__ = f.__name__ return decorated_function @user_bp.route('/profile') @login_required def profile(): """用户个人中心""" user = User.query.get(session['user_id']) if not user: session.clear() flash('用户不存在,请重新登录', 'error') return redirect(url_for('auth.login')) return render_template('user/profile.html', user=user) @user_bp.route('/orders') @login_required def orders(): """用户订单""" user = User.query.get(session['user_id']) return render_template('user/orders.html', user=user) 🔸============================================================================== 📄 文件: check_avatar.py 📊 大小: 616 bytes (0.60 KB) 🕒 修改时间: 2025-07-03 05:21:18 🔸============================================================================== """ 检查用户头像URL """ import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) from app import create_app from app.models.user import User app = create_app() with app.app_context(): # 查看所有用户的头像信息 users = User.query.all() print("=" * 60) print("用户头像信息检查") print("=" * 60) for user in users: print(f"用户: {user.username}") print(f"头像URL: {user.avatar_url}") print(f"完整URL: {user.avatar_url if user.avatar_url else '无头像'}") print("-" * 40) """ 检查用户头像URL """ 🔸============================================================================== 📄 文件: config/__init__.py 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: config/config.py 📊 大小: 1541 bytes (1.50 KB) 🕒 修改时间: 2025-07-03 04:02:34 🔸============================================================================== import os from datetime import timedelta class Config: # 基础配置 SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here-change-in-production' # 数据库配置 MYSQL_HOST = '27.124.22.104' MYSQL_USER = 'taibai' MYSQL_PASSWORD = 'taibaishopping' MYSQL_DB = 'online_shopping' MYSQL_PORT = 3306 # SQLAlchemy配置 SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}' SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = { 'pool_size': 10, 'pool_timeout': 20, 'pool_recycle': -1, 'pool_pre_ping': True } # Session配置 SESSION_TYPE = 'filesystem' SESSION_PERMANENT = False PERMANENT_SESSION_LIFETIME = timedelta(hours=24) # 文件上传配置 MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB UPLOAD_FOLDER = 'app/static/uploads' # 分页配置 POSTS_PER_PAGE = 20 # 邮件配置 MAIL_SERVER = 'mail.sq0715.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USE_SSL = False MAIL_USERNAME = 'vip@sq0715.com' MAIL_PASSWORD = 'Aalsq12350501.' MAIL_DEFAULT_SENDER = 'vip@sq0715.com' # 验证码配置 EMAIL_CODE_EXPIRE_MINUTES = 10 # 邮箱验证码有效期(分钟) class DevelopmentConfig(Config): DEBUG = True class ProductionConfig(Config): DEBUG = False config = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'default': DevelopmentConfig } 🔸============================================================================== 📄 文件: config/cos_config.py 📊 大小: 2805 bytes (2.74 KB) 🕒 修改时间: 2025-07-03 07:11:25 🔸============================================================================== """ 腾讯云COS配置 """ import os class COSConfig: """COS配置类""" # 腾讯云密钥信息 SECRET_ID = 'AKIDWu3xbz7zbw1qpeDWZLs99tMYUAZiaBVZ' SECRET_KEY = 'qQjlX2GEvMWQ3PUIq77qIUP3RZQ0KBtL' # 存储桶信息 BUCKET_NAME = 'taibai-1328510989' REGION = 'ap-guangzhou' # 存储桶域名 BUCKET_DOMAIN = f'{BUCKET_NAME}.cos.{REGION}.myqcloud.com' # 文件存储路径配置 UPLOAD_FOLDERS = { 'avatar': 'uploads/avatars/', # 用户头像 'product': 'uploads/products/', # 商品图片 'review': 'uploads/reviews/', # 评价图片 'temp': 'uploads/temp/', # 临时文件 } # 允许上传的文件类型 ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} ALLOWED_FILE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'doc', 'docx'} # 文件大小限制 (字节) MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2MB # 图片处理配置 IMAGE_QUALITY = 85 # 压缩质量 THUMBNAIL_SIZE = (200, 200) # 缩略图尺寸 @classmethod def get_full_url(cls, file_path): """获取文件完整访问URL""" if not file_path: return None if file_path.startswith('http'): return file_path return f'https://{cls.BUCKET_DOMAIN}/{file_path}' """ 腾讯云COS配置 """ import os class COSConfig: """COS配置类""" # 腾讯云密钥信息 SECRET_ID = 'AKIDWu3xbz7zbw1qpeDWZLs99tMYUAZiaBVZ' SECRET_KEY = 'qQjlX2GEvMWQ3PUIq77qIUP3RZQ0KBtL' # 存储桶信息 BUCKET_NAME = 'taibai-1328510989' REGION = 'ap-guangzhou' # 存储桶域名 BUCKET_DOMAIN = f'{BUCKET_NAME}.cos.{REGION}.myqcloud.com' # 文件存储路径配置 UPLOAD_FOLDERS = { 'avatar': 'uploads/avatars/', # 用户头像 'product': 'uploads/products/', # 商品图片 'category': 'uploads/categories/', # 分类图标 'review': 'uploads/reviews/', # 评价图片 'temp': 'uploads/temp/', # 临时文件 } # 允许上传的文件类型 ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} ALLOWED_FILE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'pdf', 'doc', 'docx'} # 文件大小限制 (字节) MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2MB # 图片处理配置 IMAGE_QUALITY = 85 # 压缩质量 THUMBNAIL_SIZE = (200, 200) # 缩略图尺寸 @classmethod def get_full_url(cls, file_path): """获取文件完整访问URL""" if not file_path: return None if file_path.startswith('http'): return file_path return f'https://{cls.BUCKET_DOMAIN}/{file_path}' 🔸============================================================================== 📄 文件: config/database.py 📊 大小: 770 bytes (0.75 KB) 🕒 修改时间: 2025-07-03 03:08:46 🔸============================================================================== from flask_sqlalchemy import SQLAlchemy import sys db = SQLAlchemy() def init_db(app): """初始化数据库""" db.init_app(app) try: with app.app_context(): # 测试数据库连接 result = db.session.execute(db.text('SELECT 1')) print("✅ 数据库连接成功") # 由于表已存在,我们只需要确保模型与数据库同步 # 不需要重新创建表 print("✅ 数据库初始化完成") except Exception as e: print(f"❌ 数据库初始化失败: {e}") print("请检查数据库配置和网络连接") # 在开发环境中不退出,允许继续运行 print("⚠️ 继续运行,但可能会有数据库相关问题") 🔸============================================================================== 📄 文件: create_admin.py 📊 大小: 5307 bytes (5.18 KB) 🕒 修改时间: 2025-07-03 05:51:05 🔸============================================================================== #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 管理员账号创建工具 """ import sys import os import getpass import re from datetime import datetime # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from app import create_app from app.models.admin import AdminUser from config.database import db def validate_email(email): """验证邮箱格式""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None def validate_phone(phone): """验证手机号格式""" pattern = r'^1[3-9]\d{9}$' return re.match(pattern, phone) is not None def validate_password(password): """验证密码强度""" if len(password) < 6: return False, "密码长度至少6位" if not re.search(r'[a-zA-Z]', password): return False, "密码必须包含字母" if not re.search(r'\d', password): return False, "密码必须包含数字" return True, "密码符合要求" def create_admin(): """创建管理员账号""" app = create_app() with app.app_context(): print("=" * 50) print("🛠️ 太白购物商城 - 管理员账号创建工具") print("=" * 50) print() # 检查是否已有管理员 existing_count = AdminUser.query.count() if existing_count > 0: print(f"⚠️ 当前已有 {existing_count} 个管理员账号") confirm = input("是否继续创建新的管理员账号?(y/N): ").strip().lower() if confirm != 'y': print("❌ 取消创建") return print() # 输入用户名 while True: username = input("请输入管理员用户名: ").strip() if not username: print("❌ 用户名不能为空") continue if len(username) < 3: print("❌ 用户名长度至少3位") continue # 检查用户名是否已存在 if AdminUser.query.filter_by(username=username).first(): print("❌ 用户名已存在") continue break # 输入真实姓名 real_name = input("请输入真实姓名: ").strip() # 输入邮箱 while True: email = input("请输入邮箱地址: ").strip() if not email: break if not validate_email(email): print("❌ 邮箱格式不正确") continue # 检查邮箱是否已存在 if AdminUser.query.filter_by(email=email).first(): print("❌ 邮箱已被使用") continue break # 输入手机号 while True: phone = input("请输入手机号: ").strip() if not phone: break if not validate_phone(phone): print("❌ 手机号格式不正确") continue # 检查手机号是否已存在 if AdminUser.query.filter_by(phone=phone).first(): print("❌ 手机号已被使用") continue break # 输入密码 while True: password = getpass.getpass("请输入密码: ") is_valid, message = validate_password(password) if not is_valid: print(f"❌ {message}") continue confirm_password = getpass.getpass("请确认密码: ") if password != confirm_password: print("❌ 密码不一致,请重新输入") continue break print() print("=" * 30) print("📋 管理员信息确认") print("=" * 30) print(f"用户名: {username}") print(f"真实姓名: {real_name if real_name else '未填写'}") print(f"邮箱: {email if email else '未填写'}") print(f"手机号: {phone if phone else '未填写'}") print(f"创建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print() confirm = input("确认创建?(y/N): ").strip().lower() if confirm != 'y': print("❌ 取消创建") return try: # 创建管理员 admin = AdminUser( username=username, real_name=real_name if real_name else None, email=email if email else None, phone=phone if phone else None, status=1 ) admin.set_password(password) db.session.add(admin) db.session.commit() print() print("✅ 管理员账号创建成功!") print("=" * 30) print("📌 登录信息") print("=" * 30) print(f"登录地址: http://localhost:5000/admin/login") print(f"用户名: {username}") print(f"密码: [已设置]") print() print("🔐 请妥善保管登录信息") except Exception as e: print(f"❌ 创建失败: {str(e)}") db.session.rollback() if __name__ == '__main__': create_admin() 🔸============================================================================== 📄 文件: create_sample_categories.py 📊 大小: 4784 bytes (4.67 KB) 🕒 修改时间: 2025-07-03 07:12:00 🔸============================================================================== #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 创建示例分类数据 """ import sys import os # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from app import create_app from app.models.product import Category from config.database import db def create_sample_categories(): """创建示例分类数据""" app = create_app() with app.app_context(): print("=== 创建示例分类数据 ===\n") # 检查是否已有分类 if Category.query.count() > 0: print("⚠️ 数据库中已有分类数据,是否继续添加?(y/N): ", end="") if input().strip().lower() != 'y': return # 创建示例分类数据 categories_data = [ # 一级分类 {'name': '数码电子', 'parent_id': 0, 'level': 1, 'sort_order': 1}, {'name': '服装鞋帽', 'parent_id': 0, 'level': 1, 'sort_order': 2}, {'name': '食品饮料', 'parent_id': 0, 'level': 1, 'sort_order': 3}, {'name': '家居生活', 'parent_id': 0, 'level': 1, 'sort_order': 4}, {'name': '图书文具', 'parent_id': 0, 'level': 1, 'sort_order': 5}, ] # 创建一级分类 level1_categories = {} for cat_data in categories_data: category = Category(**cat_data) db.session.add(category) db.session.flush() # 获取ID level1_categories[cat_data['name']] = category.id print(f"✅ 创建一级分类: {cat_data['name']}") # 二级分类数据 level2_data = [ # 数码电子子分类 {'name': '手机通讯', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 1}, {'name': '电脑办公', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 2}, {'name': '相机摄像', 'parent_id': level1_categories['数码电子'], 'level': 2, 'sort_order': 3}, # 服装鞋帽子分类 {'name': '男装', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 1}, {'name': '女装', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 2}, {'name': '运动鞋', 'parent_id': level1_categories['服装鞋帽'], 'level': 2, 'sort_order': 3}, # 食品饮料子分类 {'name': '零食小食', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 1}, {'name': '饮料冲调', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 2}, {'name': '生鲜食品', 'parent_id': level1_categories['食品饮料'], 'level': 2, 'sort_order': 3}, ] # 创建二级分类 level2_categories = {} for cat_data in level2_data: category = Category(**cat_data) db.session.add(category) db.session.flush() level2_categories[cat_data['name']] = category.id print(f" ├─ 创建二级分类: {cat_data['name']}") # 三级分类数据 level3_data = [ # 手机通讯子分类 {'name': '智能手机', 'parent_id': level2_categories['手机通讯'], 'level': 3, 'sort_order': 1}, {'name': '手机配件', 'parent_id': level2_categories['手机通讯'], 'level': 3, 'sort_order': 2}, # 男装子分类 {'name': 'T恤', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 1}, {'name': '衬衫', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 2}, {'name': '牛仔裤', 'parent_id': level2_categories['男装'], 'level': 3, 'sort_order': 3}, # 零食小食子分类 {'name': '饼干糕点', 'parent_id': level2_categories['零食小食'], 'level': 3, 'sort_order': 1}, {'name': '坚果炒货', 'parent_id': level2_categories['零食小食'], 'level': 3, 'sort_order': 2}, ] # 创建三级分类 for cat_data in level3_data: category = Category(**cat_data) db.session.add(category) print(f" └─ 创建三级分类: {cat_data['name']}") # 提交数据 db.session.commit() print(f"\n✅ 示例分类数据创建完成!") print(f"一级分类: {len(categories_data)} 个") print(f"二级分类: {len(level2_data)} 个") print(f"三级分类: {len(level3_data)} 个") print(f"总计: {len(categories_data) + len(level2_data) + len(level3_data)} 个分类") print(f"\n访问地址: http://localhost:5000/admin/products/categories") if __name__ == '__main__': create_sample_categories() 🔸============================================================================== 📄 文件: create_sample_specs.py 📊 大小: 2010 bytes (1.96 KB) 🕒 修改时间: 2025-07-03 15:07:45 🔸============================================================================== """ 创建示例规格数据 """ from app import create_app from config.database import db from app.models.product import SpecName, SpecValue def create_sample_specs(): """创建示例规格数据""" app = create_app() with app.app_context(): # 检查是否已有数据 if SpecName.query.count() > 0: print("规格数据已存在,跳过创建") return print("开始创建规格数据...") # 创建规格名称 specs_data = [ { 'name': '颜色', 'values': ['红色', '蓝色', '黑色', '白色', '金色', '银色'] }, { 'name': '尺寸', 'values': ['S', 'M', 'L', 'XL', 'XXL'] }, { 'name': '内存', 'values': ['4GB', '8GB', '16GB', '32GB'] }, { 'name': '存储容量', 'values': ['64GB', '128GB', '256GB', '512GB', '1TB'] }, { 'name': '型号', 'values': ['标准版', '升级版', '专业版', '旗舰版'] } ] for i, spec_data in enumerate(specs_data): # 创建规格名称 spec_name = SpecName( name=spec_data['name'], sort_order=i + 1 ) db.session.add(spec_name) db.session.flush() # 获取ID # 创建规格值 for j, value in enumerate(spec_data['values']): spec_value = SpecValue( spec_name_id=spec_name.id, value=value, sort_order=j + 1 ) db.session.add(spec_value) print(f"✅ 创建规格:{spec_data['name']} - {len(spec_data['values'])} 个值") db.session.commit() print("✅ 规格数据创建完成!") if __name__ == '__main__': create_sample_specs() 🔸============================================================================== 📄 文件: create_test_order.py 📊 大小: 2823 bytes (2.76 KB) 🕒 修改时间: 2025-07-04 02:48:05 🔸============================================================================== #!/usr/bin/env python """ 创建测试订单数据 """ from app import create_app from config.database import db from app.models.user import User from app.models.address import UserAddress from app.models.product import Product, ProductInventory from app.models.cart import Cart def create_test_data(): app = create_app() with app.app_context(): try: # 检查是否有测试用户 test_user = User.query.filter_by(username='testuser').first() if not test_user: print("请先运行 create_test_user.py 创建测试用户") return print(f"测试用户: {test_user.username} (ID: {test_user.id})") # 创建测试地址 if not UserAddress.query.filter_by(user_id=test_user.id).first(): address = UserAddress( user_id=test_user.id, receiver_name='张三', receiver_phone='13800138000', province='广东省', city='广州市', district='天河区', detail_address='天河路123号', postal_code='510000', is_default=1 ) db.session.add(address) print("创建测试地址") # 添加商品到购物车 products = Product.query.filter_by(status=1).limit(3).all() for product in products: # 检查是否已在购物车 existing_cart = Cart.query.filter_by( user_id=test_user.id, product_id=product.id ).first() if not existing_cart: # 获取默认SKU default_sku = ProductInventory.query.filter_by( product_id=product.id, is_default=1 ).first() cart_item = Cart( user_id=test_user.id, product_id=product.id, sku_code=default_sku.sku_code if default_sku else None, quantity=1 ) db.session.add(cart_item) print(f"添加商品到购物车: {product.name}") db.session.commit() print("测试数据创建完成!") print("\n测试步骤:") print("1. 使用 testuser / 123456 登录") print("2. 访问购物车页面") print("3. 选择商品进行结算") print("4. 测试订单流程") except Exception as e: db.session.rollback() print(f"创建测试数据失败: {e}") if __name__ == '__main__': create_test_data() 🔸============================================================================== 📄 文件: create_test_user.py 📊 大小: 2050 bytes (2.00 KB) 🕒 修改时间: 2025-07-03 03:09:13 🔸============================================================================== import pymysql import sys from werkzeug.security import generate_password_hash from datetime import datetime # 数据库配置 config = { 'host': '27.124.22.104', 'user': 'taibai', 'password': 'taibaishopping', 'database': 'online_shopping', 'port': 3306, 'charset': 'utf8mb4' } def create_test_user(): try: # 连接数据库 connection = pymysql.connect(**config) print("✅ 数据库连接成功") with connection.cursor() as cursor: # 检查是否已存在测试用户 cursor.execute("SELECT id FROM users WHERE username = %s", ('testuser',)) existing_user = cursor.fetchone() if existing_user: print("✅ 测试用户已存在!") print("用户名: testuser") print("密码: 123456") print("邮箱: test@example.com") print("手机: 13800138000") return # 创建测试用户 password_hash = generate_password_hash('123456') now = datetime.now() sql = """ INSERT INTO users (username, phone, email, password_hash, nickname, status, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ cursor.execute(sql, ( 'testuser', '13800138000', 'test@example.com', password_hash, '测试用户', 1, now, now )) connection.commit() print("✅ 测试用户创建成功!") print("用户名: testuser") print("密码: 123456") print("邮箱: test@example.com") print("手机: 13800138000") except Exception as e: print(f"❌ 创建测试用户失败: {e}") sys.exit(1) finally: if 'connection' in locals(): connection.close() if __name__ == '__main__': create_test_user() 🔸============================================================================== 📄 文件: docker/.dockerignore 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: docker/Dockerfile 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: docker/docker-compose.yml 📊 大小: 0 bytes (0.00 KB) 🕒 修改时间: 2025-07-03 02:46:14 🔸============================================================================== 🔸============================================================================== 📄 文件: export_code.py 📊 大小: 10115 bytes (9.88 KB) 🕒 修改时间: 2025-07-04 03:35:25 🔸============================================================================== #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 项目代码导出工具 用于将整个电商项目的代码导出到文本文件中 """ import os import datetime from pathlib import Path class CodeExporter: def __init__(self, project_root=None): """ 初始化代码导出器 :param project_root: 项目根目录,默认为当前目录 """ self.project_root = Path(project_root) if project_root else Path('.') self.output_file = None # 需要导出的文件扩展名 self.include_extensions = { '.py', '.html', '.css', '.js', '.sql', '.txt', '.md', '.yml', '.yaml', '.json', '.xml', '.ini', '.cfg' } # 需要排除的目录 self.exclude_dirs = { 'venv', '.venv', 'env', '.env', '__pycache__', '.git', '.idea', '.vscode', 'node_modules', 'logs', 'temp', 'tmp', '.pytest_cache', '.coverage', 'htmlcov', 'dist', 'build' } # 需要排除的文件 self.exclude_files = { '.DS_Store', 'Thumbs.db', '.gitignore', '*.pyc', '*.pyo', '*.log', '*.tmp', '*.bak', '*.swp', '*.swo' } # 特殊处理的文件(即使没有扩展名也要包含) self.special_files = { 'Dockerfile', 'requirements.txt', 'README', 'LICENSE', 'Makefile', 'Procfile', '.dockerignore' } def should_include_file(self, file_path): """ 判断文件是否应该被包含在导出中 :param file_path: 文件路径 :return: bool """ file_name = file_path.name file_suffix = file_path.suffix.lower() # 检查特殊文件 if file_name in self.special_files: return True # 检查扩展名 if file_suffix in self.include_extensions: return True return False def should_exclude_dir(self, dir_path): """ 判断目录是否应该被排除 :param dir_path: 目录路径 :return: bool """ dir_name = dir_path.name return dir_name in self.exclude_dirs or dir_name.startswith('.') def get_file_info(self, file_path): """ 获取文件信息 :param file_path: 文件路径 :return: dict """ try: stat = file_path.stat() return { 'size': stat.st_size, 'modified': datetime.datetime.fromtimestamp(stat.st_mtime), 'relative_path': file_path.relative_to(self.project_root) } except Exception as e: return { 'size': 0, 'modified': datetime.datetime.now(), 'relative_path': file_path.relative_to(self.project_root), 'error': str(e) } def read_file_content(self, file_path): """ 读取文件内容 :param file_path: 文件路径 :return: str """ try: # 尝试用UTF-8编码读取 with open(file_path, 'r', encoding='utf-8') as f: return f.read() except UnicodeDecodeError: try: # 如果UTF-8失败,尝试GBK编码 with open(file_path, 'r', encoding='gbk') as f: return f.read() except UnicodeDecodeError: try: # 如果还是失败,尝试latin-1编码 with open(file_path, 'r', encoding='latin-1') as f: return f.read() except Exception as e: return f"[无法读取文件内容: {str(e)}]" except Exception as e: return f"[读取文件时发生错误: {str(e)}]" def scan_project(self): """ 扫描项目目录,获取所有需要导出的文件 :return: list """ files_to_export = [] for root, dirs, files in os.walk(self.project_root): root_path = Path(root) # 过滤掉需要排除的目录 dirs[:] = [d for d in dirs if not self.should_exclude_dir(root_path / d)] for file in files: file_path = root_path / file if self.should_include_file(file_path): file_info = self.get_file_info(file_path) files_to_export.append({ 'path': file_path, 'info': file_info }) # 按相对路径排序 files_to_export.sort(key=lambda x: str(x['info']['relative_path'])) return files_to_export def export_to_file(self, output_filename=None): """ 导出代码到文件 :param output_filename: 输出文件名 """ if not output_filename: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") output_filename = f"project_code_export_{timestamp}.txt" self.output_file = output_filename files_to_export = self.scan_project() print(f"开始导出项目代码...") print(f"项目根目录: {self.project_root.absolute()}") print(f"找到 {len(files_to_export)} 个文件需要导出") print(f"输出文件: {output_filename}") with open(output_filename, 'w', encoding='utf-8') as output: # 写入文件头 self.write_header(output, files_to_export) # 写入每个文件的内容 for i, file_data in enumerate(files_to_export, 1): file_path = file_data['path'] file_info = file_data['info'] print(f"正在处理 ({i}/{len(files_to_export)}): {file_info['relative_path']}") self.write_file_section(output, file_path, file_info) # 写入文件尾 self.write_footer(output) print(f"\n✅ 导出完成!") print(f"输出文件: {output_filename}") print(f"文件大小: {os.path.getsize(output_filename) / 1024:.2f} KB") def write_header(self, output, files_to_export): """ 写入文件头部信息 """ output.write("=" * 80 + "\n") output.write("项目代码导出文件\n") output.write("=" * 80 + "\n") output.write(f"项目名称: 基于Python的线上电商系统\n") output.write(f"导出时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") output.write(f"项目路径: {self.project_root.absolute()}\n") output.write(f"文件总数: {len(files_to_export)}\n") output.write("=" * 80 + "\n\n") # 写入文件目录 output.write("📁 文件目录:\n") output.write("-" * 50 + "\n") for file_data in files_to_export: file_info = file_data['info'] size_kb = file_info['size'] / 1024 if file_info['size'] > 0 else 0 output.write(f"{file_info['relative_path']} ({size_kb:.1f} KB)\n") output.write("\n" + "=" * 80 + "\n\n") def write_file_section(self, output, file_path, file_info): """ 写入单个文件的内容 """ relative_path = file_info['relative_path'] # 文件分隔符 output.write("🔸" + "=" * 78 + "\n") output.write(f"📄 文件: {relative_path}\n") output.write(f"📊 大小: {file_info['size']} bytes ({file_info['size'] / 1024:.2f} KB)\n") output.write(f"🕒 修改时间: {file_info['modified'].strftime('%Y-%m-%d %H:%M:%S')}\n") if 'error' in file_info: output.write(f"⚠️ 错误: {file_info['error']}\n") output.write("🔸" + "=" * 78 + "\n\n") # 文件内容 content = self.read_file_content(file_path) output.write(content) # 确保文件结尾有换行 if not content.endswith('\n'): output.write('\n') output.write("\n\n") def write_footer(self, output): """ 写入文件尾部信息 """ output.write("=" * 80 + "\n") output.write("导出完成\n") output.write(f"导出时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") output.write("=" * 80 + "\n") def export_summary(self): """ 导出项目摘要信息 """ files_to_export = self.scan_project() # 按文件类型统计 type_stats = {} total_size = 0 for file_data in files_to_export: file_path = file_data['path'] file_info = file_data['info'] ext = file_path.suffix.lower() or '无扩展名' if ext not in type_stats: type_stats[ext] = {'count': 0, 'size': 0} type_stats[ext]['count'] += 1 type_stats[ext]['size'] += file_info['size'] total_size += file_info['size'] print("\n📊 项目统计信息:") print("-" * 50) print(f"总文件数: {len(files_to_export)}") print(f"总大小: {total_size / 1024:.2f} KB") print("\n📋 文件类型统计:") for ext, stats in sorted(type_stats.items(), key=lambda x: x[1]['count'], reverse=True): print(f"{ext:>10}: {stats['count']:>3} 个文件, {stats['size'] / 1024:>6.1f} KB") def main(): """ 主函数 """ print("🚀 项目代码导出工具") print("=" * 50) # 创建导出器 exporter = CodeExporter() # 显示项目摘要 exporter.export_summary() # 询问是否继续导出 print("\n" + "=" * 50) choice = input("是否继续导出完整代码到文件? (y/n): ").lower().strip() if choice in ['y', 'yes', '是']: # 询问输出文件名 output_name = input("请输入输出文件名 (直接回车使用默认名称): ").strip() if not output_name: output_name = None # 开始导出 exporter.export_to_file(output_name) else: print("取消导出。") if __name__ == "__main__": main() 🔸============================================================================== 📄 文件: requirements.txt 📊 大小: 245 bytes (0.24 KB) 🕒 修改时间: 2025-07-03 04:23:09 🔸============================================================================== Flask==2.3.3 Flask-SQLAlchemy==3.0.5 Flask-WTF==1.1.1 WTForms==3.0.1 PyMySQL==1.1.0 Werkzeug==2.3.7 python-dotenv==1.0.0 Flask-Session==0.5.0 email-validator==2.0.0 Flask-Mail==0.9.1 cos-python-sdk-v5==1.9.24 Pillow==10.0.1 python-magic==0.4.27 🔸============================================================================== 📄 文件: run.py 📊 大小: 175 bytes (0.17 KB) 🕒 修改时间: 2025-07-03 03:10:14 🔸============================================================================== from app import create_app import os app = create_app(os.getenv('FLASK_CONFIG') or 'default') if __name__ == '__main__': app.run(host='0.0.0.0', port=50400, debug=True) 🔸============================================================================== 📄 文件: test_cos_connection.py 📊 大小: 6619 bytes (6.46 KB) 🕒 修改时间: 2025-07-03 04:26:42 🔸============================================================================== """ 测试腾讯云COS连接 - 独立测试脚本 """ import sys import os from datetime import datetime # 添加项目根目录到Python路径 sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 直接导入COS相关模块,避免导入Flask应用 from qcloud_cos import CosConfig, CosS3Client from qcloud_cos.cos_exception import CosClientError, CosServiceError from config.cos_config import COSConfig class COSTestClient: """COS测试客户端""" def __init__(self): """初始化COS客户端""" try: # 配置COS config = CosConfig( Region=COSConfig.REGION, SecretId=COSConfig.SECRET_ID, SecretKey=COSConfig.SECRET_KEY, Token=None, Scheme='https' ) # 创建客户端 self.client = CosS3Client(config) self.bucket = COSConfig.BUCKET_NAME print("✅ COS客户端初始化成功") except Exception as e: print(f"❌ COS客户端初始化失败: {str(e)}") raise def test_connection(self): """测试COS连接""" try: # 尝试列出存储桶 response = self.client.list_objects( Bucket=self.bucket, MaxKeys=1 ) return { 'success': True, 'message': 'COS连接测试成功', 'bucket': self.bucket, 'region': COSConfig.REGION } except Exception as e: return { 'success': False, 'message': f'COS连接测试失败: {str(e)}', 'bucket': self.bucket, 'region': COSConfig.REGION } def list_files(self, prefix='', max_keys=10): """列出文件""" try: response = self.client.list_objects( Bucket=self.bucket, Prefix=prefix, MaxKeys=max_keys ) files = [] if 'Contents' in response: for obj in response['Contents']: files.append({ 'key': obj['Key'], 'size': obj['Size'], 'last_modified': obj['LastModified'], 'url': COSConfig.get_full_url(obj['Key']) }) return files except Exception as e: print(f"❌ 列出文件失败: {str(e)}") return [] def upload_test_file(self): """上传测试文件""" test_content = f"COS上传测试文件\n创建时间: {datetime.now()}\n测试内容: Hello COS!" test_file_key = f"test/test_upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" try: # 上传文件 response = self.client.put_object( Bucket=self.bucket, Body=test_content.encode('utf-8'), Key=test_file_key, StorageClass='STANDARD' ) file_url = COSConfig.get_full_url(test_file_key) return { 'success': True, 'file_key': test_file_key, 'url': file_url, 'etag': response['ETag'] } except Exception as e: return { 'success': False, 'error': str(e) } def delete_file(self, file_key): """删除文件""" try: response = self.client.delete_object( Bucket=self.bucket, Key=file_key ) return {'success': True} except Exception as e: return { 'success': False, 'error': str(e) } def main(): """主测试函数""" print("=" * 60) print("🚀 腾讯云COS连接测试") print("=" * 60) # 显示配置信息 print(f"📦 存储桶名称: {COSConfig.BUCKET_NAME}") print(f"🌍 所属地域: {COSConfig.REGION}") print(f"🔗 访问域名: {COSConfig.BUCKET_DOMAIN}") print(f"🔑 SecretId: {COSConfig.SECRET_ID[:8]}***") print("-" * 60) try: # 初始化测试客户端 cos_test = COSTestClient() # 1. 测试连接 print("1️⃣ 测试COS连接...") result = cos_test.test_connection() if result['success']: print("✅ COS连接测试成功!") print(f" 存储桶: {result['bucket']}") print(f" 地域: {result['region']}") else: print("❌ COS连接测试失败!") print(f" 错误信息: {result['message']}") return False print("-" * 60) # 2. 测试文件列表 print("2️⃣ 测试文件列表功能...") files = cos_test.list_files(max_keys=5) print(f"✅ 文件列表获取成功,共找到 {len(files)} 个文件") if files: print("📁 最近的文件:") for i, file_info in enumerate(files[:3], 1): size_mb = file_info['size'] / 1024 / 1024 print(f" {i}. {file_info['key']}") print(f" 大小: {size_mb:.2f}MB") print(f" 修改时间: {file_info['last_modified']}") else: print("📭 存储桶为空") print("-" * 60) # 3. 测试文件上传 print("3️⃣ 测试文件上传功能...") upload_result = cos_test.upload_test_file() if upload_result['success']: print("✅ 文件上传测试成功!") print(f" 文件路径: {upload_result['file_key']}") print(f" 访问URL: {upload_result['url']}") print(f" ETag: {upload_result['etag']}") # 4. 测试文件删除 print("-" * 60) print("4️⃣ 测试文件删除功能...") delete_result = cos_test.delete_file(upload_result['file_key']) if delete_result['success']: print("✅ 文件删除测试成功!") else: print(f"❌ 文件删除测试失败: {delete_result['error']}") else: print(f"❌ 文件上传测试失败: {upload_result['error']}") print("=" * 60) print("🎉 COS功能测试完成!") print("=" * 60) return True except Exception as e: print(f"❌ 测试过程中发生异常: {str(e)}") return False if __name__ == '__main__': main() 🔸============================================================================== 📄 文件: test_db_connection.py 📊 大小: 3366 bytes (3.29 KB) 🕒 修改时间: 2025-07-03 03:06:50 🔸============================================================================== import pymysql import sys # 数据库配置 config = { 'host': '27.124.22.104', 'user': 'taibai', 'password': 'taibaishopping', 'database': 'online_shopping', 'port': 3306, 'charset': 'utf8mb4', 'connect_timeout': 10, # 设置连接超时时间 'read_timeout': 10, 'write_timeout': 10 } def test_connection(): try: print("正在测试数据库连接...") print(f"主机: {config['host']}") print(f"端口: {config['port']}") print(f"用户: {config['user']}") print(f"数据库: {config['database']}") # 尝试连接数据库 connection = pymysql.connect(**config) print("✅ 数据库连接成功!") # 测试查询 with connection.cursor() as cursor: cursor.execute("SELECT VERSION()") version = cursor.fetchone() print(f"MySQL版本: {version[0]}") cursor.execute("SHOW TABLES") tables = cursor.fetchall() print(f"当前数据库中的表数量: {len(tables)}") if tables: print("现有表:") for table in tables: print(f" - {table[0]}") connection.close() return True except pymysql.Error as e: print(f"❌ 数据库连接失败: {e}") return False except Exception as e: print(f"❌ 连接过程中发生错误: {e}") return False if __name__ == "__main__": if test_connection(): print("\n数据库连接测试通过,可以继续运行应用。") else: print("\n请检查数据库配置或网络连接。") sys.exit(1) import pymysql import sys # 数据库配置 config = { 'host': '27.124.22.104', 'user': 'taibai', 'password': 'taibaishopping', 'database': 'online_shopping', 'port': 3306, 'charset': 'utf8mb4', 'connect_timeout': 10, # 设置连接超时时间 'read_timeout': 10, 'write_timeout': 10 } def test_connection(): try: print("正在测试数据库连接...") print(f"主机: {config['host']}") print(f"端口: {config['port']}") print(f"用户: {config['user']}") print(f"数据库: {config['database']}") # 尝试连接数据库 connection = pymysql.connect(**config) print("✅ 数据库连接成功!") # 测试查询 with connection.cursor() as cursor: cursor.execute("SELECT VERSION()") version = cursor.fetchone() print(f"MySQL版本: {version[0]}") cursor.execute("SHOW TABLES") tables = cursor.fetchall() print(f"当前数据库中的表数量: {len(tables)}") if tables: print("现有表:") for table in tables: print(f" - {table[0]}") connection.close() return True except pymysql.Error as e: print(f"❌ 数据库连接失败: {e}") return False except Exception as e: print(f"❌ 连接过程中发生错误: {e}") return False if __name__ == "__main__": if test_connection(): print("\n数据库连接测试通过,可以继续运行应用。") else: print("\n请检查数据库配置或网络连接。") sys.exit(1) 🔸============================================================================== 📄 文件: test_email_detailed.py 📊 大小: 8512 bytes (8.31 KB) 🕒 修改时间: 2025-07-03 04:02:12 🔸============================================================================== import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart def test_smtp_detailed(): """详细测试mail.sq0715.com的不同配置""" server = 'mail.sq0715.com' username = 'vip@sq0715.com' password = 'Aalsq12350501.' configs = [ { 'name': '587端口 + STARTTLS', 'port': 587, 'use_tls': True, 'use_ssl': False }, { 'name': '465端口 + SSL', 'port': 465, 'use_tls': False, 'use_ssl': True }, { 'name': '25端口 + STARTTLS', 'port': 25, 'use_tls': True, 'use_ssl': False }, { 'name': '25端口 无加密', 'port': 25, 'use_tls': False, 'use_ssl': False }, { 'name': '993端口 + SSL', 'port': 993, 'use_tls': False, 'use_ssl': True } ] for config in configs: print(f"\n{'=' * 50}") print(f"测试配置: {config['name']}") print(f"服务器: {server}:{config['port']}") print(f"TLS: {config['use_tls']}, SSL: {config['use_ssl']}") print('=' * 50) try: # 创建SMTP连接 if config['use_ssl']: print("使用SSL连接...") smtp_server = smtplib.SMTP_SSL(server, config['port'], timeout=30) else: print("使用普通连接...") smtp_server = smtplib.SMTP(server, config['port'], timeout=30) # 开启调试模式 smtp_server.set_debuglevel(1) print("连接建立成功,发送EHLO...") smtp_server.ehlo() # 如果需要STARTTLS if config['use_tls']: print("启动TLS加密...") smtp_server.starttls() smtp_server.ehlo() # 重新发送EHLO print("尝试登录...") smtp_server.login(username, password) print("✅ 登录成功!") # 发送测试邮件 print("发送测试邮件...") msg = MIMEMultipart() msg['From'] = username msg['To'] = username # 发送给自己 msg['Subject'] = f'测试邮件 - {config["name"]}' body = f"这是使用 {config['name']} 配置发送的测试邮件" msg.attach(MIMEText(body, 'plain', 'utf-8')) smtp_server.send_message(msg) print("✅ 邮件发送成功!") smtp_server.quit() print(f"🎉 配置 '{config['name']}' 完全成功!") return config # 返回成功的配置 except smtplib.SMTPAuthenticationError as e: print(f"❌ 认证失败: {e}") except smtplib.SMTPConnectError as e: print(f"❌ 连接失败: {e}") except smtplib.SMTPServerDisconnected as e: print(f"❌ 服务器断开连接: {e}") except smtplib.SMTPRecipientsRefused as e: print(f"❌ 收件人被拒绝: {e}") except Exception as e: print(f"❌ 其他错误: {type(e).__name__}: {e}") return None if __name__ == '__main__': print("开始测试 mail.sq0715.com 的SMTP配置...") successful_config = test_smtp_detailed() if successful_config: print(f"\n🎉 找到可用配置!") print("请在config.py中使用以下配置:") print("-" * 40) print(f"MAIL_SERVER = 'mail.sq0715.com'") print(f"MAIL_PORT = {successful_config['port']}") print(f"MAIL_USE_TLS = {successful_config['use_tls']}") print(f"MAIL_USE_SSL = {successful_config['use_ssl']}") print(f"MAIL_USERNAME = 'vip@sq0715.com'") print(f"MAIL_PASSWORD = 'Aalsq12350501.'") print(f"MAIL_DEFAULT_SENDER = 'vip@sq0715.com'") else: print("\n❌ 所有配置都失败了") print("可能的原因:") print("1. 邮箱密码不正确") print("2. 邮箱服务器不支持SMTP") print("3. 需要在邮箱设置中开启SMTP服务") print("4. 服务器防火墙阻止了连接") import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart def test_smtp_detailed(): """详细测试mail.sq0715.com的不同配置""" server = 'mail.sq0715.com' username = 'vip@sq0715.com' password = 'Aalsq12350501.' configs = [ { 'name': '587端口 + STARTTLS', 'port': 587, 'use_tls': True, 'use_ssl': False }, { 'name': '465端口 + SSL', 'port': 465, 'use_tls': False, 'use_ssl': True }, { 'name': '25端口 + STARTTLS', 'port': 25, 'use_tls': True, 'use_ssl': False }, { 'name': '25端口 无加密', 'port': 25, 'use_tls': False, 'use_ssl': False }, { 'name': '993端口 + SSL', 'port': 993, 'use_tls': False, 'use_ssl': True } ] for config in configs: print(f"\n{'=' * 50}") print(f"测试配置: {config['name']}") print(f"服务器: {server}:{config['port']}") print(f"TLS: {config['use_tls']}, SSL: {config['use_ssl']}") print('=' * 50) try: # 创建SMTP连接 if config['use_ssl']: print("使用SSL连接...") smtp_server = smtplib.SMTP_SSL(server, config['port'], timeout=30) else: print("使用普通连接...") smtp_server = smtplib.SMTP(server, config['port'], timeout=30) # 开启调试模式 smtp_server.set_debuglevel(1) print("连接建立成功,发送EHLO...") smtp_server.ehlo() # 如果需要STARTTLS if config['use_tls']: print("启动TLS加密...") smtp_server.starttls() smtp_server.ehlo() # 重新发送EHLO print("尝试登录...") smtp_server.login(username, password) print("✅ 登录成功!") # 发送测试邮件 print("发送测试邮件...") msg = MIMEMultipart() msg['From'] = username msg['To'] = username # 发送给自己 msg['Subject'] = f'测试邮件 - {config["name"]}' body = f"这是使用 {config['name']} 配置发送的测试邮件" msg.attach(MIMEText(body, 'plain', 'utf-8')) smtp_server.send_message(msg) print("✅ 邮件发送成功!") smtp_server.quit() print(f"🎉 配置 '{config['name']}' 完全成功!") return config # 返回成功的配置 except smtplib.SMTPAuthenticationError as e: print(f"❌ 认证失败: {e}") except smtplib.SMTPConnectError as e: print(f"❌ 连接失败: {e}") except smtplib.SMTPServerDisconnected as e: print(f"❌ 服务器断开连接: {e}") except smtplib.SMTPRecipientsRefused as e: print(f"❌ 收件人被拒绝: {e}") except Exception as e: print(f"❌ 其他错误: {type(e).__name__}: {e}") return None if __name__ == '__main__': print("开始测试 mail.sq0715.com 的SMTP配置...") successful_config = test_smtp_detailed() if successful_config: print(f"\n🎉 找到可用配置!") print("请在config.py中使用以下配置:") print("-" * 40) print(f"MAIL_SERVER = 'mail.sq0715.com'") print(f"MAIL_PORT = {successful_config['port']}") print(f"MAIL_USE_TLS = {successful_config['use_tls']}") print(f"MAIL_USE_SSL = {successful_config['use_ssl']}") print(f"MAIL_USERNAME = 'vip@sq0715.com'") print(f"MAIL_PASSWORD = 'Aalsq12350501.'") print(f"MAIL_DEFAULT_SENDER = 'vip@sq0715.com'") else: print("\n❌ 所有配置都失败了") print("可能的原因:") print("1. 邮箱密码不正确") print("2. 邮箱服务器不支持SMTP") print("3. 需要在邮箱设置中开启SMTP服务") print("4. 服务器防火墙阻止了连接") ================================================================================ 导出完成 导出时间: 2025-07-04 03:35:45 ================================================================================