commit 5fcd6b701730b68c7497b62aa549143b8a17536b Author: superlishunqin <852326703@qq.com> Date: Fri Jul 4 19:07:35 2025 +0800 online_shopping_07_04 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a265cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# Environments (保留配置文件,只排除本地环境变量) +.env.local +.env.development +.env.test + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# 操作系统 +.DS_Store +.AppleDouble +.LSOverride +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# 虚拟环境 +venv/ +.venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# 日志文件 +logs/*.log +*.log + +# 上传文件(用户上传的内容不需要版本控制) +app/static/uploads/* +!app/static/uploads/.gitkeep + +# 数据库文件(如果使用SQLite) +*.db +*.sqlite +*.sqlite3 + +# 临时文件 +temp/ +tmp/ +*.tmp +*.bak +*.backup + +# 测试输出 +test_output/ +test_results/ + +# 缓存文件 +.cache/ + +# 压缩文件 +*.zip +*.tar.gz +*.rar + +# 编译文件 +*.pyc + +# 性能分析 +*.prof + +# 本地开发临时文件 +scratch/ +notes.txt +todo.txt + +# Docker临时文件(保留主要Docker文件) +.dockerignore.bak + +# 部署相关临时文件 +deploy_temp/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7965521 --- /dev/null +++ b/README.md @@ -0,0 +1,211 @@ + +
+ +# 🛍️ Python Flask 电商系统 🛍️ + +**一个功能完善、技术栈现代、可直接部署的线上电商平台毕业设计项目。** + +![Python](https://img.shields.io/badge/Python-3.8%2B-blue?style=for-the-badge&logo=python)![Flask](https://img.shields.io/badge/Flask-2.x-black?style=for-the-badge&logo=flask)![MySQL](https://img.shields.io/badge/MySQL-5.7%2B-orange?style=for-the-badge&logo=mysql)![Docker](https://img.shields.io/badge/Docker-Ready-blue?style=for-the-badge&logo=docker)![Tencent Cloud](https://img.shields.io/badge/Tencent_Cloud-COS_%26_CDN-red?style=for-the-badge&logo=tencent-cloud) + +
+ +--- + +## ✨ 项目简介 + +本项目是一个基于 **Python + Flask** 构建的全功能线上电商系统。它涵盖了从用户注册登录、商品浏览、购物车、下单支付到后台管理的全流程。项目架构清晰,代码结构规范,并集成了**腾讯云COS**、**CDN**等云服务,最终可通过 **Docker** 实现快速、标准化的部署。 + +## 🚀 技术栈概览 + +| 分类 | 技术 | 描述 | +| :--- | :--- | :--- | +| 🖥️ **后端** | `Python`, `Flask`, `SQLAlchemy` | 核心开发语言、Web框架与ORM | +| 🗃️ **数据库** | `MySQL` | 持久化数据存储 | +| 🎨 **前端** | `HTML`, `CSS`, `JavaScript`, `Bootstrap` | 页面构建、样式与交互逻辑 | +| ☁️ **云服务** | `腾讯云COS`, `腾讯云CDN`, `微信支付` | 对象存储、内容分发网络与支付服务 | +| 🐳 **部署** | `Docker`, `Nginx` | 容器化部署与反向代理 | +| ⚙️ **工具** | `Flask-Login`, `Flask-WTF`, `Flask-Mail` | 用户认证、表单处理、邮件服务 | + +## 🌟 系统核心功能 + +
+🛍️ 用户端功能 (点击展开) + +- **👤 用户中心** + - 手机/邮箱注册登录,支持短信/邮箱验证码 + - 微信授权登录(可选) + - 个人信息编辑(昵称、头像、性别等) + - 头像上传至腾讯云COS + - 收货地址管理(增删改查、设为默认) + - 我的收藏夹 & 浏览历史 + +- **🛒 购物流程** + - 多级商品分类导航 + - 商品列表(分页、排序、筛选、搜索) + - 商品详情页(轮播图、规格选择、用户评价) + - 购物车(添加、修改数量、删除、结算) + - 未登录用户购物车(`localStorage` 支持) + +- **💳 订单与支付** + - 创建订单,填写备注 + - **微信支付**(PC端扫码、移动端JSAPI) + - 订单状态跟踪(待支付、待发货、待收货、待评价...) + - 查看订单详情与物流信息 + - 取消订单、申请退款、确认收货 + +- **✍️ 评价系统** + - 对已完成订单的商品进行评价(评分、文字、图片) + - 匿名评价选项 + +
+ +
+🔧 管理后台功能 (点击展开) + +- **📊 数据看板 (Dashboard)** + - 销售额、订单量、用户增长等核心指标可视化 + +- **📦 商品管理** + - 商品分类的增删改查 + - 商品信息管理(上架/下架、编辑、库存、价格) + - 商品规格与属性管理 + +- **📋 订单管理** + - 按状态筛选和搜索订单 + - 查看订单详情 + - 执行发货操作(填写物流信息) + - 处理用户退款申请 + +- **👥 用户管理** + - 查看用户列表 + - 禁用/启用用户账户 + +- **⚙️ 系统设置** + - 网站基本信息配置 + - 支付接口与云存储配置 + +
+ +## 📂 项目结构 + +``` +Online_shopping_platform/ +├── app/ # 核心应用目录 +│ ├── models/ # 📦 数据模型 (ORM) +│ ├── views/ # 👁️ 视图函数 (Controllers) +│ ├── templates/ # 📄 HTML模板 +│ ├── static/ # 🎨 静态资源 (CSS, JS, Images) +│ ├── utils/ # 🛠️ 工具模块 (支付, COS, 邮件等) +│ ├── forms.py # 📝 表单定义 +│ └── __init__.py # 🚀 应用工厂函数 +├── config/ # ⚙️ 配置文件 +├── docker/ # 🐳 Docker 相关配置 +├── logs/ # 📜 日志文件 +├── tests/ # 🧪 测试用例 +├── requirements.txt # 📦 Python 依赖 +├── run.py # ▶️ 应用启动脚本 +└── README.md # 📖 你正在阅读的文件 +``` + +## 🗄️ 数据库设计 + +项目数据库设计遵循电商业务逻辑,结构清晰,关系明确。 + +
+查看核心数据表 (点击展开) + +| 表名 | 用途 | +| :--- | :--- | +| `users` | 存储用户信息 | +| `user_addresses` | 用户收货地址 | +| `products` | 商品基本信息 (SPU) | +| `product_inventory` | 商品库存单元 (SKU) | +| `categories` | 商品分类 | +| `cart` | 购物车 | +| `orders` | 订单主表 | +| `order_items` | 订单详情表 | +| `payments` | 支付记录 | +| `reviews` | 商品评价 | +| `admin_users` | 后台管理员 | + +
+ +> 完整的 `CREATE TABLE` SQL语句已在项目文件中提供,包含了详细的字段、索引和外键设计。 + +## 🛠️ 本地运行与部署 + +### 1. 环境准备 + +- 克隆项目到本地 + ```bash + git clone + cd Online_shopping_platform + ``` +- 创建并激活Python虚拟环境 + ```bash + python -m venv venv + source venv/bin/activate # Linux/Mac + # venv\Scripts\activate # Windows + ``` +- 安装项目依赖 + ```bash + pip install -r requirements.txt + ``` + +### 2. 配置 + +- 复制 `.env.example` 文件为 `.env` (如果提供)。 +- 根据提示修改 `config/` 目录下的配置文件,或设置环境变量,填入你的: + - **数据库连接信息** + - **Flask `SECRET_KEY`** + - **腾讯云 `COS` 和 `CDN` 配置** + - **微信支付商户信息** + - **邮件服务器配置** + +### 3. 初始化 + +- 初始化数据库表结构 + ```bash + # 在Flask应用上下文中执行 + flask db init # 如果使用Flask-Migrate + flask db migrate + flask db upgrade + # 或者使用自定义脚本 + python -c "from app import create_app, db; app = create_app(); app.app_context().push(); db.create_all()" + ``` +- 创建第一个管理员账户 + ```bash + python create_admin.py + ``` + +### 4. 启动! + +- 运行开发服务器 + ```bash + python run.py + ``` +- 🎉 恭喜!现在可以访问 `http://127.0.0.1:5000` 查看你的电商网站了。 + +### 5. Docker 部署 (推荐) + +- 确保已安装 Docker 和 Docker Compose。 +- 在项目根目录下执行: + ```bash + docker-compose up --build -d + ``` +- 服务将在后台启动,并通过 Nginx 代理对外提供服务。 + +## 💡 项目亮点 + +- **🛡️ 安全性**: 全面考虑了常见的Web安全问题,如 `SQL注入`、`XSS`、`CSRF`,并进行了有效防护。 +- **⚡ 性能优化**: 集成了 `CDN加速`、`图片懒加载`、`数据库查询优化` 等多种性能优化手段。 +- **📱 响应式设计**: 基于 Bootstrap 框架,完美适配PC和移动设备,提供一致的用户体验。 +- **🧩 模块化设计**: 代码结构清晰,功能高度解耦,易于维护和二次开发。 +- **☁️ 云原生支持**: 无缝集成云存储和CDN,为高并发和大数据量场景打下基础。 + +--- + +
+

本项目为毕业设计作品,旨在展示一个完整的Web应用开发流程。

+

作者:林金兴 | 指导老师:[指导老师姓名]

+
\ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..1a8e226 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,95 @@ +""" +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("✅ 购物车蓝图注册成功") diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..fb74c9a --- /dev/null +++ b/app/forms.py @@ -0,0 +1,128 @@ +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('提交订单') \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..c26a7ad --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,18 @@ +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' +] diff --git a/app/models/address.py b/app/models/address.py new file mode 100644 index 0000000..9c53c67 --- /dev/null +++ b/app/models/address.py @@ -0,0 +1,79 @@ +""" +用户地址模型 +""" +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'' diff --git a/app/models/admin.py b/app/models/admin.py new file mode 100644 index 0000000..b2fe1b5 --- /dev/null +++ b/app/models/admin.py @@ -0,0 +1,52 @@ +""" +管理员模型 +""" +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'' diff --git a/app/models/cart.py b/app/models/cart.py new file mode 100644 index 0000000..caa2f23 --- /dev/null +++ b/app/models/cart.py @@ -0,0 +1,136 @@ +""" +购物车模型 +""" +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'' diff --git a/app/models/operation_log.py b/app/models/operation_log.py new file mode 100644 index 0000000..e5ce4c7 --- /dev/null +++ b/app/models/operation_log.py @@ -0,0 +1,57 @@ +""" +操作日志模型 +""" +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'' diff --git a/app/models/order.py b/app/models/order.py new file mode 100644 index 0000000..f4e9775 --- /dev/null +++ b/app/models/order.py @@ -0,0 +1,182 @@ +""" +订单模型 +""" +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'' diff --git a/app/models/payment.py b/app/models/payment.py new file mode 100644 index 0000000..1941e7a --- /dev/null +++ b/app/models/payment.py @@ -0,0 +1,68 @@ +""" +支付模型 +""" +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'' diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..0964899 --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,265 @@ +""" +商品相关模型 +""" +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'' diff --git a/app/models/review.py b/app/models/review.py new file mode 100644 index 0000000..1267f90 --- /dev/null +++ b/app/models/review.py @@ -0,0 +1,64 @@ +""" +评价模型 +""" +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'' diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..95cdb62 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,50 @@ +""" +用户模型 +""" +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'' diff --git a/app/models/verification.py b/app/models/verification.py new file mode 100644 index 0000000..b998a4e --- /dev/null +++ b/app/models/verification.py @@ -0,0 +1,61 @@ +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 diff --git a/app/static/css/address_form.css b/app/static/css/address_form.css new file mode 100644 index 0000000..ebc14ec --- /dev/null +++ b/app/static/css/address_form.css @@ -0,0 +1,96 @@ +/* 地址表单页面样式 */ +.form-label { + font-weight: 500; + margin-bottom: 0.5rem; +} + +.form-label .text-danger { + font-size: 0.9em; +} + +.form-select:focus, +.form-control:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.text-danger { + font-size: 0.875em; + margin-top: 0.25rem; +} + +/* 调试信息样式 */ +.alert-info { + border-left: 4px solid #0dcaf0; + background-color: #cff4fc; + border-color: #b8daff; +} + +#debugInfo { + font-family: 'Courier New', monospace; + font-size: 0.9em; + margin-top: 0.5rem; +} + +/* 表单布局优化 */ +.row .col-md-4, +.row .col-md-6, +.row .col-md-8 { + margin-bottom: 0; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +/* 按钮组样式 */ +.d-flex.gap-2 { + gap: 0.5rem !important; +} + +.btn { + padding: 0.5rem 1rem; + font-weight: 500; +} + +/* 复选框样式 */ +.form-check { + padding-left: 1.5em; +} + +.form-check-input { + margin-top: 0.25em; +} + +.form-check-label { + font-weight: 500; + cursor: pointer; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .col-md-4, + .col-md-6, + .col-md-8 { + margin-bottom: 1rem; + } + + .d-flex.gap-2 { + flex-direction: column; + } + + .d-flex.gap-2 .btn { + width: 100%; + } +} + +/* 加载状态样式 */ +.form-select:disabled { + background-color: #e9ecef; + opacity: 0.65; +} + +.loading-text { + color: #6c757d; + font-style: italic; +} diff --git a/app/static/css/addresses.css b/app/static/css/addresses.css new file mode 100644 index 0000000..8648a70 --- /dev/null +++ b/app/static/css/addresses.css @@ -0,0 +1,79 @@ +/* 地址管理页面样式 */ +.address-card { + transition: all 0.3s ease; + cursor: pointer; +} + +.address-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.address-card.border-primary { + border-width: 2px !important; +} + +.address-card .card-body { + position: relative; +} + +.dropdown-toggle::after { + display: none; +} + +/* 空状态样式 */ +.empty-state { + padding: 3rem 0; +} + +.empty-state i { + opacity: 0.5; +} + +/* 地址卡片内容样式 */ +.address-card .card-title { + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.address-card .badge { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; +} + +.address-card .text-muted { + font-size: 0.9rem; +} + +/* 下拉菜单样式 */ +.dropdown-menu { + min-width: 120px; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + border: 1px solid rgba(0,0,0,.125); +} + +.dropdown-item { + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.dropdown-item:hover { + background-color: #f8f9fa; +} + +.dropdown-item.text-danger:hover { + background-color: #f8d7da; + color: #721c24 !important; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .address-card { + margin-bottom: 1rem; + } + + .col-md-6 { + padding-left: 0.75rem; + padding-right: 0.75rem; + } +} diff --git a/app/static/css/admin_base.css b/app/static/css/admin_base.css new file mode 100644 index 0000000..fcb35d8 --- /dev/null +++ b/app/static/css/admin_base.css @@ -0,0 +1,117 @@ +:root { + --admin-primary: #0d6efd; + --admin-sidebar: #212529; + --admin-sidebar-hover: #495057; + --admin-bg: #f8f9fa; +} + +body { + background-color: var(--admin-bg); +} + +.admin-sidebar { + min-height: 100vh; + background-color: var(--admin-sidebar); + width: 250px; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + padding-top: 20px; +} + +.admin-sidebar .nav-link { + color: #fff; + padding: 12px 20px; + border-radius: 0; + margin-bottom: 2px; +} + +.admin-sidebar .nav-link:hover, +.admin-sidebar .nav-link.active { + background-color: var(--admin-sidebar-hover); + color: #fff; +} + +.admin-sidebar .nav-link i { + margin-right: 10px; + width: 20px; +} + +.admin-main { + margin-left: 250px; + padding: 0; +} + +.admin-header { + background-color: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + padding: 15px 30px; + margin-bottom: 30px; +} + +.admin-content { + padding: 0 30px 30px; +} + +.stats-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 15px; + padding: 25px; + margin-bottom: 20px; + border: none; +} + +.stats-card.success { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); +} + +.stats-card.warning { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.stats-card.info { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); +} + +.admin-table { + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.admin-table th { + background-color: #f8f9fa; + border: none; + font-weight: 600; + color: #495057; +} + +.admin-table td { + border: none; + vertical-align: middle; +} + +.admin-table tbody tr { + border-bottom: 1px solid #f8f9fa; +} + +.admin-table tbody tr:hover { + background-color: #f8f9fa; +} + +.sidebar-brand { + color: #fff; + font-size: 1.2rem; + font-weight: bold; + padding: 0 20px 30px; + border-bottom: 1px solid #495057; + margin-bottom: 20px; +} + +.sidebar-brand i { + margin-right: 10px; + color: var(--admin-primary); +} diff --git a/app/static/css/admin_categories.css b/app/static/css/admin_categories.css new file mode 100644 index 0000000..5a8d047 --- /dev/null +++ b/app/static/css/admin_categories.css @@ -0,0 +1,155 @@ +/* 分类管理页面样式 */ +.category-tree { + background: white; + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.category-item { + border: 1px solid #e9ecef; + border-radius: 8px; + margin-bottom: 10px; + transition: all 0.3s ease; +} + +.category-item:hover { + border-color: #0d6efd; + box-shadow: 0 2px 8px rgba(13, 110, 253, 0.15); +} + +.category-header { + padding: 15px 20px; + background: #f8f9fa; + border-radius: 8px 8px 0 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.category-level-1 .category-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.category-level-2 .category-header { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; +} + +.category-level-3 .category-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; +} + +.category-info { + display: flex; + align-items: center; + gap: 15px; +} + +.category-icon { + width: 40px; + height: 40px; + border-radius: 8px; + object-fit: cover; + border: 2px solid rgba(255,255,255,0.3); +} + +.default-icon { + width: 40px; + height: 40px; + border-radius: 8px; + background: rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.category-details h6 { + margin: 0; + font-weight: 600; +} + +.category-meta { + font-size: 12px; + opacity: 0.8; + margin-top: 2px; +} + +.category-actions { + display: flex; + gap: 8px; +} + +.children-categories { + padding: 0 20px 20px; + margin-left: 40px; + border-left: 2px dashed #dee2e6; +} + +.btn-icon { + width: 32px; + height: 32px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; +} + +.add-category-form { + background: #f8f9fa; + border-radius: 10px; + padding: 25px; + margin-bottom: 30px; + border: 2px dashed #dee2e6; +} + +.icon-upload-area { + width: 80px; + height: 80px; + border: 2px dashed #dee2e6; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.icon-upload-area:hover { + border-color: #0d6efd; + background: #e3f2fd; +} + +.icon-upload-area img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.sort-handle { + cursor: move; + color: #6c757d; + margin-right: 10px; +} + +.sort-handle:hover { + color: #0d6efd; +} + +.empty-state { + text-align: center; + padding: 60px 20px; + color: #6c757d; +} + +.empty-state i { + font-size: 4rem; + margin-bottom: 20px; + opacity: 0.5; +} diff --git a/app/static/css/admin_dashboard.css b/app/static/css/admin_dashboard.css new file mode 100644 index 0000000..c00a5ed --- /dev/null +++ b/app/static/css/admin_dashboard.css @@ -0,0 +1,39 @@ +/* Dashboard specific styles */ +.dashboard-stats { + margin-bottom: 30px; +} + +.chart-container { + position: relative; + height: 300px; +} + +.system-status-item { + display: flex; + justify-content: between; + align-items: center; + margin-bottom: 15px; + padding: 10px 0; + border-bottom: 1px solid #f0f0f0; +} + +.system-status-item:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.log-table-container { + margin-top: 30px; +} + +.empty-state { + text-align: center; + padding: 40px 20px; + color: #6c757d; +} + +.empty-state i { + font-size: 3rem; + margin-bottom: 15px; + opacity: 0.5; +} diff --git a/app/static/css/admin_login.css b/app/static/css/admin_login.css new file mode 100644 index 0000000..97c1940 --- /dev/null +++ b/app/static/css/admin_login.css @@ -0,0 +1,82 @@ +body { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.login-card { + background: white; + border-radius: 15px; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); + padding: 40px; + min-width: 400px; +} + +.login-header { + text-align: center; + margin-bottom: 30px; +} + +.login-header h2 { + color: #333; + font-weight: 600; + margin-bottom: 10px; +} + +.login-header p { + color: #666; + margin-bottom: 0; +} + +.form-control { + padding: 12px 15px; + border-radius: 8px; + border: 1px solid #ddd; + font-size: 16px; +} + +.form-control:focus { + border-color: #667eea; + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); +} + +.btn-login { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + padding: 12px; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-login:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.input-group-text { + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 8px 0 0 8px; +} + +.form-control { + border-radius: 0 8px 8px 0; +} + +.form-control:first-child { + border-radius: 8px 0 0 8px; +} + +.back-link { + text-decoration: none; + color: #667eea; + font-size: 14px; +} + +.back-link:hover { + color: #764ba2; +} diff --git a/app/static/css/admin_profile.css b/app/static/css/admin_profile.css new file mode 100644 index 0000000..fe2161e --- /dev/null +++ b/app/static/css/admin_profile.css @@ -0,0 +1,231 @@ +/* 管理员个人资料页面样式 */ +.profile-container { + padding: 20px 0; +} + +.profile-card { + border: none; + border-radius: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + margin-bottom: 20px; +} + +.profile-card .card-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px 12px 0 0 !important; + padding: 15px 20px; +} + +.profile-card .card-header h5 { + margin: 0; + font-weight: 600; +} + +.profile-card .card-header i { + margin-right: 8px; +} + +.profile-card .card-body { + padding: 25px; +} + +.form-label { + font-weight: 600; + color: #495057; + margin-bottom: 8px; +} + +.form-control { + border-radius: 8px; + border: 1px solid #e0e6ed; + padding: 12px 15px; + transition: all 0.3s ease; +} + +.form-control:focus { + border-color: #667eea; + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); +} + +.form-control[readonly] { + background-color: #f8f9fa; + color: #6c757d; +} + +.form-text { + font-size: 12px; + color: #6c757d; + margin-top: 5px; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + padding: 12px 24px; + font-weight: 600; + transition: all 0.3s ease; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.btn-warning { + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + border: none; + border-radius: 8px; + padding: 12px 24px; + font-weight: 600; + color: #8b4513; + transition: all 0.3s ease; +} + +.btn-warning:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(252, 182, 159, 0.4); + color: #8b4513; +} + +.btn i { + margin-right: 6px; +} + +/* 账号信息卡片 */ +.info-card { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + border: none; + border-radius: 12px; + margin-bottom: 20px; +} + +.info-card .card-header { + background: rgba(255, 255, 255, 0.2); + border: none; + border-radius: 12px 12px 0 0 !important; +} + +.info-card .card-body { + background: rgba(255, 255, 255, 0.1); + border-radius: 0 0 12px 12px; +} + +.info-item { + margin-bottom: 15px; + padding: 10px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.info-item:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.info-item strong { + color: #2c3e50; + font-weight: 600; +} + +.badge { + font-size: 12px; + padding: 6px 12px; + border-radius: 20px; + font-weight: 500; +} + +.badge.bg-success { + background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%) !important; + color: #2c3e50; +} + +.badge.bg-danger { + background: linear-gradient(135deg, #fc466b 0%, #3f5efb 100%) !important; + color: white; +} + +/* 密码修改卡片 */ +.password-card { + background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); + border: none; + border-radius: 12px; +} + +.password-card .card-header { + background: rgba(255, 255, 255, 0.2); + border: none; + border-radius: 12px 12px 0 0 !important; +} + +.password-card .card-body { + background: rgba(255, 255, 255, 0.1); + border-radius: 0 0 12px 12px; +} + +.password-card .form-control { + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.password-card .form-control:focus { + background: white; + border-color: #fcb69f; + box-shadow: 0 0 0 0.2rem rgba(252, 182, 159, 0.25); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .profile-container { + padding: 10px 0; + } + + .profile-card .card-body { + padding: 20px 15px; + } + + .row .col-md-6 { + margin-bottom: 15px; + } +} + +/* 动画效果 */ +.profile-card { + animation: fadeInUp 0.6s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 表单验证样式 */ +.form-control.is-invalid { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.form-control.is-valid { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: block; + color: #dc3545; + font-size: 12px; + margin-top: 5px; +} + +.valid-feedback { + display: block; + color: #28a745; + font-size: 12px; + margin-top: 5px; +} diff --git a/app/static/css/auth.css b/app/static/css/auth.css new file mode 100644 index 0000000..7061a8e --- /dev/null +++ b/app/static/css/auth.css @@ -0,0 +1,65 @@ +/* 认证页面样式 */ +.auth-container { + min-height: 60vh; + display: flex; + align-items: center; +} + +.auth-card { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border: none; + border-radius: 10px; +} + +.auth-card .card-header { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + color: white; + border-radius: 10px 10px 0 0; + border: none; +} + +.auth-card .card-header h4 { + margin: 0; + font-weight: 500; +} + +.auth-card .card-body { + padding: 2rem; +} + +/* 表单样式 */ +.form-control:focus { + border-color: #007bff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn-primary { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + border: none; + padding: 0.75rem; + font-weight: 500; +} + +.btn-primary:hover { + background: linear-gradient(135deg, #0056b3 0%, #004085 100%); + transform: translateY(-1px); +} + +/* 链接样式 */ +.auth-link { + color: #007bff; + text-decoration: none; + font-weight: 500; +} + +.auth-link:hover { + color: #0056b3; + text-decoration: underline; +} + +/* 响应式设计 */ +@media (max-width: 576px) { + .auth-card .card-body { + padding: 1.5rem; + } +} diff --git a/app/static/css/base.css b/app/static/css/base.css new file mode 100644 index 0000000..e9634e9 --- /dev/null +++ b/app/static/css/base.css @@ -0,0 +1,47 @@ +/* 基础样式 */ +.navbar-brand { + font-weight: bold; + color: #007bff !important; +} + +.footer { + background-color: #f8f9fa; + padding: 2rem 0; + margin-top: 3rem; +} + +.alert { + margin-bottom: 0; +} + +.search-form { + max-width: 300px; +} + +.cart-badge { + position: relative; + top: -2px; + font-size: 0.7rem; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .search-form { + max-width: 100%; + margin: 10px 0; + } +} + +/* 返回顶部按钮 */ +#backToTop { + display: none; + z-index: 1000; +} + +/* 成功提示框样式 */ +.success-toast { + top: 20px; + right: 20px; + z-index: 9999; + min-width: 300px; +} diff --git a/app/static/css/cart.css b/app/static/css/cart.css new file mode 100644 index 0000000..665396c --- /dev/null +++ b/app/static/css/cart.css @@ -0,0 +1,26 @@ +.cart-item { + transition: background-color 0.2s; +} + +.cart-item:hover { + background-color: #f8f9fa; +} + +.quantity-input { + width: 60px; +} + +.item-checkbox { + transform: scale(1.2); +} + +.position-sticky { + top: 20px !important; +} + +@media (max-width: 768px) { + .col-md-4 .position-sticky { + position: relative !important; + top: auto !important; + } +} diff --git a/app/static/css/checkout.css b/app/static/css/checkout.css new file mode 100644 index 0000000..2179dd1 --- /dev/null +++ b/app/static/css/checkout.css @@ -0,0 +1,46 @@ +/* 订单结算页面样式 */ +.checkout-section { + margin-bottom: 20px; +} + +.address-card { + cursor: pointer; + transition: all 0.3s ease; +} + +.address-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.address-card.selected { + border-color: #007bff; + background-color: #f8f9ff; +} + +.product-item { + border-bottom: 1px solid #eee; + padding: 15px 0; +} + +.product-item:last-child { + border-bottom: none; +} + +.order-summary { + background-color: #f8f9fa; + border-radius: 8px; + padding: 20px; +} + +.price-row { + display: flex; + justify-content: space-between; + margin-bottom: 10px; +} + +.total-price { + font-size: 1.2em; + font-weight: bold; + color: #e74c3c; +} diff --git a/app/static/css/index.css b/app/static/css/index.css new file mode 100644 index 0000000..78a4ea9 --- /dev/null +++ b/app/static/css/index.css @@ -0,0 +1,68 @@ +/* 首页样式 */ +.product-card { + transition: transform 0.2s; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.category-card { + transition: all 0.2s; +} + +.category-card:hover { + transform: translateY(-3px); + box-shadow: 0 4px 10px rgba(0,0,0,0.1); +} + +/* 欢迎横幅样式 */ +.jumbotron { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); +} + +/* 商品图片样式 */ +.product-image { + height: 200px; + object-fit: cover; +} + +.product-image-placeholder { + height: 200px; + background-color: #f8f9fa; + display: flex; + align-items: center; + justify-content: center; +} + +/* 价格样式 */ +.price-current { + color: #dc3545; + font-weight: bold; +} + +.price-original { + color: #6c757d; + text-decoration: line-through; + font-size: 0.875rem; +} + +/* 服务特色卡片 */ +.feature-card { + transition: transform 0.2s; +} + +.feature-card:hover { + transform: translateY(-3px); +} + +/* 用户专区卡片 */ +.user-zone-card { + transition: all 0.2s; +} + +.user-zone-card:hover { + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} diff --git a/app/static/css/order_detail.css b/app/static/css/order_detail.css new file mode 100644 index 0000000..04c8642 --- /dev/null +++ b/app/static/css/order_detail.css @@ -0,0 +1,80 @@ +/* 订单详情页面样式 */ +.order-status-timeline { + position: relative; + padding-left: 30px; +} + +.timeline-item { + position: relative; + padding-bottom: 20px; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: -30px; + top: 0; + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #dee2e6; +} + +.timeline-item.completed::before { + background-color: #28a745; +} + +.timeline-item.current::before { + background-color: #007bff; + box-shadow: 0 0 0 4px rgba(0,123,255,0.2); +} + +.timeline-item::after { + content: ''; + position: absolute; + left: -24px; + top: 12px; + width: 2px; + height: calc(100% - 12px); + background-color: #dee2e6; +} + +.timeline-item:last-child::after { + display: none; +} + +.timeline-item.completed::after { + background-color: #28a745; +} + +.order-detail-card { + margin-bottom: 20px; +} + +.product-item { + border-bottom: 1px solid #f0f0f0; + padding: 15px 0; +} + +.product-item:last-child { + border-bottom: none; +} + +.product-image { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; +} + +.info-row { + display: flex; + justify-content: space-between; + margin-bottom: 8px; +} + +.total-amount { + color: #e74c3c; + font-weight: bold; + font-size: 1.2em; +} diff --git a/app/static/css/orders.css b/app/static/css/orders.css new file mode 100644 index 0000000..4726603 --- /dev/null +++ b/app/static/css/orders.css @@ -0,0 +1,160 @@ +/* 订单页面样式 */ +.order-card { + margin-bottom: 20px; + border: 1px solid #dee2e6; + border-radius: 8px; + transition: box-shadow 0.3s ease; +} + +.order-card:hover { + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.order-header { + background-color: #f8f9fa; + padding: 15px; + border-bottom: 1px solid #dee2e6; + border-radius: 8px 8px 0 0; +} + +.order-item { + padding: 15px; + border-bottom: 1px solid #f0f0f0; +} + +.order-item:last-child { + border-bottom: none; +} + +.order-footer { + background-color: #f8f9fa; + padding: 15px; + border-top: 1px solid #dee2e6; + border-radius: 0 0 8px 8px; +} + +.status-badge { + font-size: 0.85em; + padding: 4px 8px; +} + +.order-amount { + color: #e74c3c; + font-weight: bold; + font-size: 1.1em; +} + +/* 强制限制商品图片尺寸 - 多重选择器确保优先级 */ +.product-image, +img.product-image, +.order-item .product-image, +.order-item img.product-image { + width: 80px !important; + height: 80px !important; + object-fit: cover !important; + border-radius: 4px !important; + display: block !important; + max-width: 80px !important; + max-height: 80px !important; + min-width: 80px !important; + min-height: 80px !important; +} + +/* 防止任何外部样式影响 */ +.col-md-2 .product-image, +.order-item .col-md-2 img { + width: 80px !important; + height: 80px !important; + object-fit: cover !important; + border-radius: 4px !important; +} + +/* 导航标签样式 */ +.nav-pills .nav-link { + border-radius: 20px; + padding: 0.5rem 1rem; + margin-right: 0.5rem; + transition: all 0.3s ease; +} + +.nav-pills .nav-link:hover { + background-color: #e9ecef; + color: #495057; +} + +.nav-pills .nav-link.active { + background-color: #007bff; + color: white; +} + +/* 空状态样式 */ +.empty-state { + padding: 3rem 0; +} + +.empty-state i { + opacity: 0.5; +} + +/* 分页样式 */ +.pagination { + margin-top: 2rem; +} + +.page-link { + color: #007bff; + border-color: #dee2e6; +} + +.page-link:hover { + color: #0056b3; + background-color: #e9ecef; + border-color: #dee2e6; +} + +.page-item.active .page-link { + background-color: #007bff; + border-color: #007bff; +} + +/* 按钮组样式 */ +.btn-group .btn { + margin-right: 0.5rem; +} + +.btn-group .btn:last-child { + margin-right: 0; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .order-header .row, + .order-footer .row { + flex-direction: column; + } + + .order-header .col-md-3, + .order-footer .col-md-6 { + margin-bottom: 0.5rem; + text-align: left !important; + } + + .order-item .row { + flex-direction: column; + text-align: center; + } + + .order-item .col-md-2, + .order-item .col-md-6 { + margin-bottom: 1rem; + } + + .nav-pills { + flex-wrap: wrap; + } + + .nav-pills .nav-link { + margin-bottom: 0.5rem; + font-size: 0.9rem; + } +} diff --git a/app/static/css/pay.css b/app/static/css/pay.css new file mode 100644 index 0000000..654bbef --- /dev/null +++ b/app/static/css/pay.css @@ -0,0 +1,49 @@ +/* 订单支付页面样式 */ +.pay-container { + max-width: 600px; + margin: 50px auto; +} + +.order-info { + background-color: #f8f9fa; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.payment-method { + border: 2px solid #dee2e6; + border-radius: 8px; + padding: 20px; + margin-bottom: 15px; + cursor: pointer; + transition: all 0.3s ease; +} + +.payment-method:hover { + border-color: #007bff; + background-color: #f8f9ff; +} + +.payment-method.selected { + border-color: #007bff; + background-color: #f8f9ff; +} + +.qr-code { + text-align: center; + padding: 30px; + background-color: #f8f9fa; + border-radius: 8px; +} + +.countdown { + font-size: 1.2em; + color: #e74c3c; + font-weight: bold; +} + +.payment-status { + text-align: center; + padding: 20px; +} diff --git a/app/static/css/product_detail.css b/app/static/css/product_detail.css new file mode 100644 index 0000000..39dee84 --- /dev/null +++ b/app/static/css/product_detail.css @@ -0,0 +1,71 @@ +.product-card { + transition: transform 0.2s; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.spec-option { + border-radius: 4px; + transition: all 0.2s; +} + +.spec-option:hover { + transform: translateY(-1px); +} + +.thumbnail-image { + transition: all 0.2s; +} + +.thumbnail-image:hover { + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.price-section { + background: linear-gradient(135deg, #fff5f5 0%, #ffeee8 100%); + padding: 20px; + border-radius: 8px; + border: 1px solid #ffe6e6; +} + +.product-description { + line-height: 1.8; + white-space: pre-line; +} + +.service-promises li { + padding: 5px 0; +} + +/* 规格选择动效 */ +.spec-option.btn-primary { + background-color: #007bff; + border-color: #007bff; + color: white; + transform: scale(1.05); +} + +/* 按钮禁用状态样式 */ +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* 响应式优化 */ +@media (max-width: 768px) { + .price-section { + text-align: center; + } + + .action-buttons .d-md-flex { + flex-direction: column; + } + + .action-buttons .btn { + margin-bottom: 10px; + } +} diff --git a/app/static/css/product_list.css b/app/static/css/product_list.css new file mode 100644 index 0000000..55aa14c --- /dev/null +++ b/app/static/css/product_list.css @@ -0,0 +1,14 @@ +.product-card { + transition: transform 0.2s; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.btn-check:checked + .btn { + background-color: #007bff; + border-color: #007bff; + color: white; +} diff --git a/app/static/css/profile.css b/app/static/css/profile.css new file mode 100644 index 0000000..52b5584 --- /dev/null +++ b/app/static/css/profile.css @@ -0,0 +1,231 @@ +/* 个人中心页面样式 */ + +/* 头像上传相关CSS */ +.avatar-upload { + position: relative !important; + display: inline-block !important; + width: 120px !important; + height: 120px !important; + overflow: hidden !important; +} + +/* 强制限制头像尺寸 - 多重选择器确保优先级 */ +.avatar-preview, +#avatarPreview, +.avatar-upload .avatar-preview, +.avatar-upload #avatarPreview { + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + border: 3px solid #ddd !important; + object-fit: cover !important; + cursor: pointer !important; + transition: all 0.3s ease !important; + display: block !important; + max-width: 120px !important; + max-height: 120px !important; + min-width: 120px !important; + min-height: 120px !important; +} + +.avatar-preview:hover, +#avatarPreview:hover { + border-color: #007bff !important; + transform: scale(1.05) !important; +} + +.avatar-placeholder { + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + border: 3px dashed #ddd !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + transition: all 0.3s ease !important; + background-color: #f8f9fa !important; +} + +.avatar-placeholder:hover { + border-color: #007bff !important; + background-color: #e3f2fd !important; +} + +.upload-overlay { + position: absolute !important; + top: 0 !important; + left: 0 !important; + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + background: rgba(0, 0, 0, 0.5) !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + opacity: 0 !important; + transition: opacity 0.3s ease !important; + cursor: pointer !important; +} + +.avatar-upload:hover .upload-overlay { + opacity: 1 !important; +} + +.upload-progress { + display: none !important; + margin-top: 10px !important; +} + +.upload-progress.show { + display: block !important; +} + +/* 图片预览模态框样式 */ +.image-preview-modal .modal-dialog { + max-width: 500px !important; + margin: 1.75rem auto !important; + display: flex !important; + align-items: center !important; + min-height: calc(100% - 3.5rem) !important; +} + +.image-preview-modal .modal-content { + max-height: 90vh !important; + display: flex !important; + flex-direction: column !important; +} + +.image-preview-modal .modal-body { + overflow-y: auto !important; +} + +.preview-container { + background: #f8f9fa !important; + border-radius: 12px !important; + padding: 30px !important; + text-align: center !important; +} + +.preview-image-wrapper { + position: relative !important; + display: block !important; + margin-bottom: 20px !important; + max-width: 100% !important; +} + +/* 强制限制预览图片大小 */ +.preview-image, +#previewImage, +.preview-image-wrapper .preview-image, +.preview-image-wrapper #previewImage { + max-width: 280px !important; + max-height: 280px !important; + width: auto !important; + height: auto !important; + object-fit: contain !important; + border-radius: 12px !important; + box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important; + border: 3px solid #fff !important; +} + +.preview-info { + background: #fff !important; + border-radius: 8px !important; + padding: 15px !important; + margin-top: 15px !important; + box-shadow: 0 2px 10px rgba(0,0,0,0.1) !important; +} + +.preview-stats { + display: flex !important; + justify-content: space-around !important; + margin-top: 10px !important; +} + +.stat-item { + text-align: center !important; +} + +.stat-value { + font-weight: bold !important; + color: #007bff !important; + font-size: 1.1em !important; +} + +.stat-label { + font-size: 0.85em !important; + color: #6c757d !important; + margin-top: 2px !important; +} + +/* 进度条样式 */ +.upload-progress .progress { + height: 8px !important; + margin-bottom: 5px !important; + border-radius: 4px !important; +} + +.upload-progress .progress-bar { + transition: width 0.3s ease !important; + border-radius: 4px !important; +} + +/* 模态框动画 */ +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out !important; + transform: translate(0, -50px) !important; +} + +.modal.show .modal-dialog { + transform: none !important; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .image-preview-modal .modal-dialog { + max-width: 95% !important; + margin: 10px auto !important; + } + + .preview-image, + #previewImage { + max-width: 250px !important; + max-height: 250px !important; + } + + .preview-container { + padding: 20px !important; + } +} + +/* 大屏幕优化 */ +@media (min-width: 1200px) { + .preview-image, + #previewImage { + max-width: 300px !important; + max-height: 300px !important; + } +} + +/* 终极覆盖规则 - 确保所有情况下样式都生效 */ +img.avatar-preview, +img#avatarPreview { + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + object-fit: cover !important; + max-width: 120px !important; + max-height: 120px !important; + min-width: 120px !important; + min-height: 120px !important; +} + +/* 防止任何外部样式影响 */ +.col-md-4 .avatar-upload img, +.text-center .avatar-upload img { + width: 120px !important; + height: 120px !important; + border-radius: 50% !important; + object-fit: cover !important; +} diff --git a/app/static/css/register.css b/app/static/css/register.css new file mode 100644 index 0000000..cfbfee0 --- /dev/null +++ b/app/static/css/register.css @@ -0,0 +1,36 @@ +/* 注册页面样式 */ +.is-valid { + border-color: #28a745 !important; +} + +.is-invalid { + border-color: #dc3545 !important; +} + +.text-success { + color: #28a745 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +/* 验证码按钮样式 */ +.btn.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* 表单增强样式 */ +.form-text { + font-size: 0.875em; + margin-top: 0.25rem; +} + +.input-group .btn { + border-left: 0; +} + +.input-group .form-control:focus + .btn { + border-color: #86b7fe; +} diff --git a/app/static/js/address_form.js b/app/static/js/address_form.js new file mode 100644 index 0000000..db1087b --- /dev/null +++ b/app/static/js/address_form.js @@ -0,0 +1,229 @@ +// 地址表单页面JavaScript功能 + +// 全局变量,避免重复初始化 +let addressFormInitialized = false; + +// 页面完全加载后初始化 +window.addEventListener('load', function() { + console.log('=== 地址表单初始化开始 ==='); + + // 显示调试信息 + document.getElementById('debugAlert').style.display = 'block'; + + // 检查数据是否加载 + if (typeof cityData === 'undefined') { + console.error('❌ cityData 未加载'); + document.getElementById('debugInfo').innerHTML = '❌ 地址数据加载失败'; + showAlert('地址数据加载失败,请刷新页面重试', 'error'); + return; + } + + console.log('✅ cityData 已加载,省份数量:', Object.keys(cityData).length); + document.getElementById('debugInfo').innerHTML = '✅ 地址数据已加载,省份数量: ' + Object.keys(cityData).length + ''; + + // 避免重复初始化 + if (addressFormInitialized) { + console.log('地址表单已初始化,跳过'); + return; + } + + addressFormInitialized = true; + + // 初始化省份列表 + initializeProvinces(); + + // 设置事件监听器 + setupEventListeners(); + + // 如果是编辑模式,设置初始值 + const savedProvince = document.getElementById('provinceValue').value; + const savedCity = document.getElementById('cityValue').value; + const savedDistrict = document.getElementById('districtValue').value; + + console.log('初始值:', {savedProvince, savedCity, savedDistrict}); + + if (savedProvince) { + setTimeout(() => { + setInitialValues(savedProvince, savedCity, savedDistrict); + }, 500); // 增加延迟确保DOM完全准备好 + } + + console.log('=== 地址表单初始化完成 ==='); +}); + +// 初始化省份列表 +function initializeProvinces() { + const provinceSelect = document.getElementById('province'); + + if (!provinceSelect) { + console.error('省份选择框未找到'); + return; + } + + console.log('开始初始化省份列表...'); + + // 清空并添加默认选项 + provinceSelect.innerHTML = ''; + + // 添加所有省份 + const provinces = Object.keys(cityData); + console.log('可用省份:', provinces); + + provinces.forEach(province => { + const option = document.createElement('option'); + option.value = province; + option.textContent = province; + provinceSelect.appendChild(option); + console.log('添加省份:', province); + }); + + console.log('省份列表初始化完成,总计:', provinces.length, '个'); +} + +// 设置事件监听器 +function setupEventListeners() { + console.log('设置事件监听器...'); + + // 省份变化事件 + const provinceSelect = document.getElementById('province'); + if (provinceSelect) { + provinceSelect.addEventListener('change', function() { + const province = this.value; + console.log('选择省份:', province); + updateCities(province); + clearDistricts(); + }); + } + + // 城市变化事件 + const citySelect = document.getElementById('city'); + if (citySelect) { + citySelect.addEventListener('change', function() { + const province = document.getElementById('province').value; + const city = this.value; + console.log('选择城市:', city, '省份:', province); + updateDistricts(province, city); + }); + } + + // 表单提交验证 + document.getElementById('addressForm').addEventListener('submit', function(e) { + const province = document.getElementById('province').value; + const city = document.getElementById('city').value; + const district = document.getElementById('district').value; + + console.log('表单验证:', {province, city, district}); + + if (!province) { + e.preventDefault(); + showAlert('请选择省份', 'warning'); + return false; + } + + if (!city) { + e.preventDefault(); + showAlert('请选择城市', 'warning'); + return false; + } + + if (!district) { + e.preventDefault(); + showAlert('请选择区县', 'warning'); + return false; + } + + return true; + }); + + console.log('事件监听器设置完成'); +} + +// 更新城市列表 +function updateCities(province) { + const citySelect = document.getElementById('city'); + citySelect.innerHTML = ''; + + console.log('更新城市列表,省份:', province); + + if (!province || !cityData[province]) { + console.log('省份为空或数据不存在'); + return; + } + + const cities = Object.keys(cityData[province]); + console.log('可用城市:', cities); + + cities.forEach(city => { + const option = document.createElement('option'); + option.value = city; + option.textContent = city; + citySelect.appendChild(option); + }); + + console.log('城市列表更新完成,总计:', cities.length, '个'); +} + +// 更新区县列表 +function updateDistricts(province, city) { + const districtSelect = document.getElementById('district'); + districtSelect.innerHTML = ''; + + console.log('更新区县列表,省份:', province, '城市:', city); + + if (!province || !city || !cityData[province] || !cityData[province][city]) { + console.log('省份或城市为空或数据不存在'); + return; + } + + const districts = cityData[province][city]; + console.log('可用区县:', districts); + + districts.forEach(district => { + const option = document.createElement('option'); + option.value = district; + option.textContent = district; + districtSelect.appendChild(option); + }); + + console.log('区县列表更新完成,总计:', districts.length, '个'); +} + +// 清空区县列表 +function clearDistricts() { + const districtSelect = document.getElementById('district'); + districtSelect.innerHTML = ''; + console.log('区县列表已清空'); +} + +// 设置初始值(编辑模式) +function setInitialValues(province, city, district) { + console.log('设置初始值:', {province, city, district}); + + const provinceSelect = document.getElementById('province'); + const citySelect = document.getElementById('city'); + const districtSelect = document.getElementById('district'); + + // 设置省份 + if (province && provinceSelect) { + provinceSelect.value = province; + console.log('省份设置为:', province); + updateCities(province); + + // 延迟设置城市 + setTimeout(() => { + if (city && citySelect) { + citySelect.value = city; + console.log('城市设置为:', city); + updateDistricts(province, city); + + // 延迟设置区县 + setTimeout(() => { + if (district && districtSelect) { + districtSelect.value = district; + console.log('区县设置为:', district); + } + }, 200); + } + }, 200); + } +} diff --git a/app/static/js/addresses.js b/app/static/js/addresses.js new file mode 100644 index 0000000..2ac8217 --- /dev/null +++ b/app/static/js/addresses.js @@ -0,0 +1,64 @@ +// 地址管理页面JavaScript功能 + +function setDefaultAddress(addressId) { + if (confirm('确定要设置为默认地址吗?')) { + fetch(`/address/set_default/${addressId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('操作失败,请重试', 'error'); + }); + } +} + +function deleteAddress(addressId) { + if (confirm('确定要删除这个地址吗?删除后无法恢复。')) { + fetch(`/address/delete/${addressId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('删除失败,请重试', 'error'); + }); + } +} + +// 页面加载完成后的处理 +document.addEventListener('DOMContentLoaded', function() { + // 为地址卡片添加点击效果 + const addressCards = document.querySelectorAll('.address-card'); + addressCards.forEach(card => { + card.addEventListener('mouseenter', function() { + this.style.transform = 'translateY(-2px)'; + this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.1)'; + }); + + card.addEventListener('mouseleave', function() { + this.style.transform = 'translateY(0)'; + this.style.boxShadow = ''; + }); + }); +}); diff --git a/app/static/js/admin_categories.js b/app/static/js/admin_categories.js new file mode 100644 index 0000000..964c568 --- /dev/null +++ b/app/static/js/admin_categories.js @@ -0,0 +1,276 @@ +// 分类管理页面JavaScript + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', function() { + initIconUpload(); + initEditIconUpload(); + initFormSubmission(); +}); + +// 初始化图标上传 +function initIconUpload() { + const uploadArea = document.getElementById('iconUploadArea'); + const iconInput = document.getElementById('iconInput'); + const iconPreview = document.getElementById('iconPreview'); + + if (uploadArea && iconInput && iconPreview) { + uploadArea.addEventListener('click', () => iconInput.click()); + + iconInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + iconPreview.src = e.target.result; + iconPreview.style.display = 'block'; + uploadArea.querySelector('i').style.display = 'none'; + }; + reader.readAsDataURL(file); + } + }); + } +} + +// 初始化编辑图标上传 +function initEditIconUpload() { + const uploadArea = document.getElementById('editIconUploadArea'); + const iconInput = document.getElementById('editIconInput'); + const iconPreview = document.getElementById('editIconPreview'); + + if (uploadArea && iconInput && iconPreview) { + uploadArea.addEventListener('click', () => iconInput.click()); + + iconInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + iconPreview.src = e.target.result; + iconPreview.style.display = 'block'; + uploadArea.querySelector('i').style.display = 'none'; + }; + reader.readAsDataURL(file); + } + }); + } +} + +// 编辑分类 +function editCategory(categoryId) { + fetch(`/admin/products/categories/${categoryId}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const category = data.category; + document.getElementById('edit_category_id').value = category.id; + document.getElementById('edit_name').value = category.name; + document.getElementById('edit_parent_id').value = category.parent_id; + document.getElementById('edit_sort_order').value = category.sort_order; + document.getElementById('edit_is_active').value = category.is_active; + + // 设置图标预览 + const iconPreview = document.getElementById('editIconPreview'); + const uploadIcon = document.getElementById('editIconUploadArea').querySelector('i'); + + if (category.icon_url) { + iconPreview.src = category.icon_url; + iconPreview.style.display = 'block'; + uploadIcon.style.display = 'none'; + } else { + iconPreview.style.display = 'none'; + uploadIcon.style.display = 'block'; + } + + // 禁用当前分类及其子分类作为父分类选项 + const parentSelect = document.getElementById('edit_parent_id'); + Array.from(parentSelect.options).forEach(option => { + option.disabled = false; + if (option.value == categoryId) { + option.disabled = true; + } + }); + + new bootstrap.Modal(document.getElementById('editCategoryModal')).show(); + } else { + alert('获取分类信息失败'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('获取分类信息失败: ' + error); + }); +} + +// 添加子分类 +function addSubCategory(parentId) { + const parentSelect = document.getElementById('parent_id'); + const nameInput = document.getElementById('name'); + + if (parentSelect) { + parentSelect.value = parentId; + } + + if (nameInput) { + nameInput.focus(); + } + + // 滚动到添加表单 + const addForm = document.querySelector('.add-category-form'); + if (addForm) { + addForm.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } +} + +// 删除分类 +function deleteCategory(categoryId) { + if (confirm('确定要删除这个分类吗?删除后无法恢复!')) { + fetch(`/admin/products/categories/${categoryId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('分类删除成功'); + location.reload(); + } else { + alert('删除失败: ' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('删除失败: ' + error); + }); + } +} + +// 切换分类展开/收起 +function toggleCategory(categoryId) { + const categoryItem = document.querySelector(`[data-id="${categoryId}"]`); + if (!categoryItem) return; + + const childrenDiv = categoryItem.querySelector('.children-categories'); + const toggleBtn = categoryItem.querySelector('.category-header .bi-chevron-down, .category-header .bi-chevron-up'); + + if (childrenDiv && toggleBtn) { + if (childrenDiv.style.display === 'none') { + childrenDiv.style.display = 'block'; + toggleBtn.className = 'bi bi-chevron-up'; + } else { + childrenDiv.style.display = 'none'; + toggleBtn.className = 'bi bi-chevron-down'; + } + } +} + +// 展开全部 +function expandAll() { + document.querySelectorAll('.children-categories').forEach(div => { + div.style.display = 'block'; + }); + document.querySelectorAll('.bi-chevron-down').forEach(icon => { + icon.className = 'bi bi-chevron-up'; + }); +} + +// 收起全部 +function collapseAll() { + document.querySelectorAll('.children-categories').forEach(div => { + div.style.display = 'none'; + }); + document.querySelectorAll('.bi-chevron-up').forEach(icon => { + icon.className = 'bi bi-chevron-down'; + }); +} + +// 初始化表单提交 +function initFormSubmission() { + const addForm = document.getElementById('addCategoryForm'); + if (addForm) { + addForm.addEventListener('submit', function(e) { + setTimeout(() => { + if (!document.querySelector('.alert-danger')) { + // 重置表单 + this.reset(); + const iconPreview = document.getElementById('iconPreview'); + const uploadIcon = document.getElementById('iconUploadArea').querySelector('i'); + + if (iconPreview) iconPreview.style.display = 'none'; + if (uploadIcon) uploadIcon.style.display = 'block'; + } + }, 100); + }); + } +} + +// 表单验证 +function validateCategoryForm(formId) { + const form = document.getElementById(formId); + if (!form) return false; + + const nameInput = form.querySelector('input[name="name"]'); + if (!nameInput || !nameInput.value.trim()) { + alert('请输入分类名称'); + if (nameInput) nameInput.focus(); + return false; + } + + return true; +} + +// 工具函数:显示加载状态 +function showLoading(element) { + if (element) { + element.disabled = true; + const originalText = element.innerHTML; + element.innerHTML = ' 处理中...'; + + setTimeout(() => { + element.disabled = false; + element.innerHTML = originalText; + }, 2000); + } +} + +// 工具函数:显示成功消息 +function showSuccess(message) { + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-success alert-dismissible fade show'; + alertDiv.innerHTML = ` + ${message} + + `; + + const container = document.querySelector('.container-fluid'); + if (container) { + container.insertBefore(alertDiv, container.firstChild); + + setTimeout(() => { + alertDiv.remove(); + }, 3000); + } +} + +// 工具函数:显示错误消息 +function showError(message) { + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-danger alert-dismissible fade show'; + alertDiv.innerHTML = ` + ${message} + + `; + + const container = document.querySelector('.container-fluid'); + if (container) { + container.insertBefore(alertDiv, container.firstChild); + + setTimeout(() => { + alertDiv.remove(); + }, 5000); + } +} diff --git a/app/static/js/admin_dashboard.js b/app/static/js/admin_dashboard.js new file mode 100644 index 0000000..d196844 --- /dev/null +++ b/app/static/js/admin_dashboard.js @@ -0,0 +1,80 @@ +// Dashboard JavaScript functionality +document.addEventListener('DOMContentLoaded', function() { + // Initialize user trend chart if canvas exists + const chartCanvas = document.getElementById('userTrendChart'); + if (chartCanvas) { + initUserTrendChart(); + } + + // Auto refresh dashboard data every 5 minutes + setInterval(function() { + refreshDashboardStats(); + }, 300000); // 5 minutes +}); + +function initUserTrendChart() { + const ctx = document.getElementById('userTrendChart').getContext('2d'); + + // Get data from template variables (these will be rendered by Jinja2) + const labels = window.userTrendLabels || []; + const data = window.userTrendData || []; + + const userTrendChart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: '注册用户数', + data: data, + borderColor: 'rgb(102, 126, 234)', + backgroundColor: 'rgba(102, 126, 234, 0.1)', + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1 + } + } + } + } + }); +} + +function refreshDashboardStats() { + // This function could be used to refresh dashboard statistics via AJAX + // For now, it's a placeholder for future implementation + console.log('Refreshing dashboard stats...'); +} + +// Utility function to format numbers +function formatNumber(num) { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); +} + +// Function to update stats cards (for future AJAX updates) +function updateStatsCard(cardSelector, value) { + const card = document.querySelector(cardSelector); + if (card) { + const valueElement = card.querySelector('h3'); + if (valueElement) { + valueElement.textContent = formatNumber(value); + } + } +} diff --git a/app/static/js/base.js b/app/static/js/base.js new file mode 100644 index 0000000..09e1372 --- /dev/null +++ b/app/static/js/base.js @@ -0,0 +1,76 @@ +// 基础JavaScript功能 + +// 返回顶部功能 +window.addEventListener('scroll', function() { + const backToTop = document.getElementById('backToTop'); + if (window.pageYOffset > 300) { + backToTop.style.display = 'block'; + } else { + backToTop.style.display = 'none'; + } +}); + +function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); +} + +// 购物车数量更新 +function updateCartBadge(count) { + const badge = document.getElementById('cartBadge'); + if (count > 0) { + badge.textContent = count; + badge.style.display = 'inline-block'; + } else { + badge.style.display = 'none'; + } +} + +// 页面加载完成后的初始化 +document.addEventListener('DOMContentLoaded', function() { + // 当前页面高亮 + const currentPath = window.location.pathname; + const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); + + navLinks.forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } + }); + + // 初始化购物车数量 + // TODO: 实现购物车数量获取 +}); + +// 通用AJAX错误处理 +function handleAjaxError(xhr) { + if (xhr.status === 401) { + alert('请先登录'); + window.location.href = '/auth/login'; + } else if (xhr.status === 403) { + alert('没有权限执行此操作'); + } else { + alert('操作失败,请稍后再试'); + } +} + +// 通用成功提示 +function showSuccessMessage(message) { + // 创建临时提示框 + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed success-toast'; + alertDiv.innerHTML = ` + ${message} + + `; + document.body.appendChild(alertDiv); + + // 3秒后自动消失 + setTimeout(() => { + if (alertDiv.parentNode) { + alertDiv.parentNode.removeChild(alertDiv); + } + }, 3000); +} diff --git a/app/static/js/cart.js b/app/static/js/cart.js new file mode 100644 index 0000000..a2bc0fd --- /dev/null +++ b/app/static/js/cart.js @@ -0,0 +1,219 @@ +let selectedItems = new Set(); + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', function() { + updateSelectAllState(); + updateTotalPrice(); +}); + +// 全选/取消全选 +document.getElementById('selectAll').addEventListener('change', function() { + const checkboxes = document.querySelectorAll('.item-checkbox:not(:disabled)'); + checkboxes.forEach(checkbox => { + checkbox.checked = this.checked; + if (this.checked) { + selectedItems.add(parseInt(checkbox.value)); + } else { + selectedItems.delete(parseInt(checkbox.value)); + } + }); + updateTotalPrice(); +}); + +// 单个商品选择 +document.querySelectorAll('.item-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', function() { + const cartId = parseInt(this.value); + if (this.checked) { + selectedItems.add(cartId); + } else { + selectedItems.delete(cartId); + } + updateSelectAllState(); + updateTotalPrice(); + }); +}); + +// 更新全选状态 +function updateSelectAllState() { + const selectAllCheckbox = document.getElementById('selectAll'); + const availableCheckboxes = document.querySelectorAll('.item-checkbox:not(:disabled)'); + const checkedCheckboxes = document.querySelectorAll('.item-checkbox:not(:disabled):checked'); + + if (availableCheckboxes.length === 0) { + selectAllCheckbox.disabled = true; + selectAllCheckbox.checked = false; + } else { + selectAllCheckbox.disabled = false; + selectAllCheckbox.checked = availableCheckboxes.length === checkedCheckboxes.length; + } +} + +// 更新总价 +function updateTotalPrice() { + let totalPrice = 0; + let selectedCount = 0; + + selectedItems.forEach(cartId => { + const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); + if (cartItem) { + const itemTotal = parseFloat(cartItem.querySelector('.item-total').textContent); + const quantity = parseInt(cartItem.querySelector('.quantity-input').value); + totalPrice += itemTotal; + selectedCount += quantity; + } + }); + + document.getElementById('selectedCount').textContent = selectedCount; + document.getElementById('selectedTotal').textContent = totalPrice.toFixed(2); + document.getElementById('finalTotal').textContent = totalPrice.toFixed(2); + + // 更新结算按钮状态 + const checkoutBtn = document.getElementById('checkoutBtn'); + checkoutBtn.disabled = selectedItems.size === 0; +} + +// 修改数量 +function changeQuantity(cartId, delta) { + const input = document.querySelector(`[data-cart-id="${cartId}"]`); + const currentValue = parseInt(input.value); + const newValue = currentValue + delta; + + if (newValue >= 1 && newValue <= parseInt(input.max)) { + updateQuantity(cartId, newValue); + } +} + +// 更新数量 +function updateQuantity(cartId, quantity) { + if (quantity < 1) return; + + fetch('/cart/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cart_id: cartId, + quantity: parseInt(quantity) + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 更新页面显示 + const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); + cartItem.querySelector('.quantity-input').value = quantity; + cartItem.querySelector('.item-total').textContent = data.item_total.toFixed(2); + + // 更新总价 + updateTotalPrice(); + + // 更新全局购物车数量 + updateCartBadge(data.cart_count); + + showSuccessMessage('数量更新成功'); + } else { + alert(data.message); + // 恢复原始值 + location.reload(); + } + }) + .catch(error => { + console.error('Error:', error); + alert('更新失败'); + location.reload(); + }); +} + +// 删除商品 +function removeItem(cartId) { + if (!confirm('确定要删除这件商品吗?')) { + return; + } + + fetch('/cart/remove', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cart_id: cartId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 从页面中移除商品 + const cartItem = document.querySelector(`[data-cart-id="${cartId}"]`); + cartItem.remove(); + + // 从选中列表中移除 + selectedItems.delete(cartId); + + // 更新显示 + updateSelectAllState(); + updateTotalPrice(); + updateCartBadge(data.cart_count); + + showSuccessMessage('商品已删除'); + + // 如果购物车为空,刷新页面 + if (data.cart_count === 0) { + setTimeout(() => { + location.reload(); + }, 1000); + } + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('删除失败'); + }); +} + +// 清空购物车 +function clearCart() { + if (!confirm('确定要清空购物车吗?')) { + return; + } + + fetch('/cart/clear', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showSuccessMessage('购物车已清空'); + setTimeout(() => { + location.reload(); + }, 1000); + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('清空失败'); + }); +} + +// 去结算 +function checkout() { + if (selectedItems.size === 0) { + alert('请选择要购买的商品'); + return; + } + + const params = new URLSearchParams(); + selectedItems.forEach(cartId => { + params.append('items', cartId); + }); + + window.location.href = `/cart/checkout?${params.toString()}`; +} diff --git a/app/static/js/checkout.js b/app/static/js/checkout.js new file mode 100644 index 0000000..e91d97e --- /dev/null +++ b/app/static/js/checkout.js @@ -0,0 +1,114 @@ +// 订单结算页面脚本 +let selectedAddressId = 0; +let subtotal = 0; + +// 初始化页面 +document.addEventListener('DOMContentLoaded', function() { + // 从页面获取初始数据 + const defaultAddress = document.querySelector('input[name="address_id"]:checked'); + if (defaultAddress) { + selectedAddressId = parseInt(defaultAddress.value); + } + + // 获取商品总价 + const subtotalElement = document.getElementById('subtotal'); + if (subtotalElement) { + subtotal = parseFloat(subtotalElement.textContent.replace('¥', '')); + } +}); + +// 选择地址 +function selectAddress(addressId) { + selectedAddressId = addressId; + + // 更新UI + document.querySelectorAll('.address-card').forEach(card => { + card.classList.remove('selected'); + }); + + document.querySelector(`[data-address-id="${addressId}"]`).classList.add('selected'); + + // 更新单选按钮 + document.querySelector(`input[value="${addressId}"]`).checked = true; +} + +// 更新运费 +function updateShippingFee() { + const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value; + let fee = 0; + + switch(shippingMethod) { + case 'express': + fee = 10; + break; + case 'same_day': + fee = 20; + break; + default: + fee = 0; + } + + document.getElementById('shippingFee').textContent = `¥${fee.toFixed(2)}`; + document.getElementById('totalAmount').textContent = `¥${(subtotal + fee).toFixed(2)}`; +} + +// 提交订单 +function submitOrder() { + if (!selectedAddressId) { + showAlert('请选择收货地址', 'warning'); + return; + } + + const shippingMethod = document.querySelector('input[name="shipping_method"]:checked').value; + const paymentMethod = document.querySelector('input[name="payment_method"]:checked').value; + const remark = document.getElementById('orderRemark').value; + + // 获取选中的购物车商品ID + const urlParams = new URLSearchParams(window.location.search); + const selectedItems = urlParams.getAll('items'); + + if (selectedItems.length === 0) { + showAlert('没有选中的商品', 'error'); + return; + } + + const orderData = { + selected_items: selectedItems, + address_id: selectedAddressId, + shipping_method: shippingMethod, + payment_method: paymentMethod, + remark: remark + }; + + // 显示加载状态 + const submitBtn = document.querySelector('.btn-danger'); + const originalText = submitBtn.innerHTML; + submitBtn.innerHTML = ' 提交中...'; + submitBtn.disabled = true; + + fetch('/order/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(orderData) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert('订单创建成功!正在跳转到支付页面...', 'success'); + setTimeout(() => { + window.location.href = `/order/pay/${data.payment_sn}`; + }, 1500); + } else { + showAlert(data.message, 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + }) + .catch(error => { + showAlert('提交订单失败,请重试', 'error'); + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + }); +} diff --git a/app/static/js/city_data.js b/app/static/js/city_data.js new file mode 100644 index 0000000..fb0aee4 --- /dev/null +++ b/app/static/js/city_data.js @@ -0,0 +1,466 @@ +// 中国省市区数据 +const cityData = { + '北京市': { + '北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区', '门头沟区', '房山区', '通州区', '顺义区', '昌平区', '大兴区', '怀柔区', '平谷区', '密云区', '延庆区'] + }, + '上海市': { + '上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区', '杨浦区', '闵行区', '宝山区', '嘉定区', '浦东新区', '金山区', '松江区', '青浦区', '奉贤区', '崇明区'] + }, + '天津市': { + '天津市': ['和平区', '河东区', '河西区', '南开区', '河北区', '红桥区', '东丽区', '西青区', '津南区', '北辰区', '武清区', '宝坻区', '滨海新区', '宁河区', '静海区', '蓟州区'] + }, + '重庆市': { + '重庆市': ['万州区', '涪陵区', '渝中区', '大渡口区', '江北区', '沙坪坝区', '九龙坡区', '南岸区', '北碚区', '綦江区', '大足区', '渝北区', '巴南区', '黔江区', '长寿区', '江津区', '合川区', '永川区', '南川区', '璧山区', '铜梁区', '潼南区', '荣昌区', '开州区', '梁平区', '武隆区', '城口县', '丰都县', '垫江县', '忠县', '云阳县', '奉节县', '巫山县', '巫溪县', '石柱土家族自治县', '秀山土家族苗族自治县', '酉阳土家族苗族自治县', '彭水苗族土家族自治县'] + }, + '河北省': { + '石家庄市': ['长安区', '桥西区', '新华区', '井陉矿区', '裕华区', '藁城区', '鹿泉区', '栾城区', '井陉县', '正定县', '行唐县', '灵寿县', '高邑县', '深泽县', '赞皇县', '无极县', '平山县', '元氏县', '赵县', '辛集市', '晋州市', '新乐市'], + '唐山市': ['路南区', '路北区', '古冶区', '开平区', '丰南区', '丰润区', '曹妃甸区', '滦州市', '滦南县', '乐亭县', '迁西县', '玉田县', '遵化市', '迁安市'], + '秦皇岛市': ['海港区', '山海关区', '北戴河区', '抚宁区', '青龙满族自治县', '昌黎县', '卢龙县'], + '邯郸市': ['邯山区', '丛台区', '复兴区', '峰峰矿区', '肥乡区', '永年区', '临漳县', '成安县', '大名县', '涉县', '磁县', '邱县', '鸡泽县', '广平县', '馆陶县', '魏县', '曲周县', '武安市'], + '邢台市': ['桥东区', '桥西区', '邢台县', '临城县', '内丘县', '柏乡县', '隆尧县', '任县', '南和县', '宁晋县', '巨鹿县', '新河县', '广宗县', '平乡县', '威县', '清河县', '临西县', '南宫市', '沙河市'], + '保定市': ['竞秀区', '莲池区', '满城区', '清苑区', '徐水区', '涞水县', '阜平县', '定兴县', '唐县', '高阳县', '容城县', '涞源县', '望都县', '安新县', '易县', '曲阳县', '蠡县', '顺平县', '博野县', '雄县', '涿州市', '定州市', '安国市', '高碑店市'], + '张家口市': ['桥东区', '桥西区', '宣化区', '下花园区', '万全区', '崇礼区', '张北县', '康保县', '沽源县', '尚义县', '蔚县', '阳原县', '怀安县', '怀来县', '涿鹿县', '赤城县'], + '承德市': ['双桥区', '双滦区', '鹰手营子矿区', '承德县', '兴隆县', '平泉市', '滦平县', '隆化县', '丰宁满族自治县', '宽城满族自治县', '围场满族蒙古族自治县'], + '沧州市': ['新华区', '运河区', '沧县', '青县', '东光县', '海兴县', '盐山县', '肃宁县', '南皮县', '吴桥县', '献县', '孟村回族自治县', '泊头市', '任丘市', '黄骅市', '河间市'], + '廊坊市': ['安次区', '广阳区', '固安县', '永清县', '香河县', '大城县', '文安县', '大厂回族自治县', '霸州市', '三河市'], + '衡水市': ['桃城区', '冀州区', '枣强县', '武邑县', '武强县', '饶阳县', '安平县', '故城县', '景县', '阜城县', '深州市'] + }, + '山西省': { + '太原市': ['小店区', '迎泽区', '杏花岭区', '尖草坪区', '万柏林区', '晋源区', '清徐县', '阳曲县', '娄烦县', '古交市'], + '大同市': ['平城区', '云冈区', '新荣区', '左云县', '阳高县', '天镇县', '广灵县', '灵丘县', '浑源县', '云州区'], + '阳泉市': ['城区', '矿区', '郊区', '平定县', '盂县'], + '长治市': ['潞州区', '上党区', '屯留区', '潞城区', '襄垣县', '平顺县', '黎城县', '壶关县', '长子县', '武乡县', '沁县', '沁源县'], + '晋城市': ['城区', '沁水县', '阳城县', '陵川县', '泽州县', '高平市'], + '朔州市': ['朔城区', '平鲁区', '山阴县', '应县', '右玉县', '怀仁市'], + '晋中市': ['榆次区', '榆社县', '左权县', '和顺县', '昔阳县', '寿阳县', '太谷县', '祁县', '平遥县', '灵石县', '介休市'], + '运城市': ['盐湖区', '临猗县', '万荣县', '闻喜县', '稷山县', '新绛县', '绛县', '垣曲县', '夏县', '平陆县', '芮城县', '永济市', '河津市'], + '忻州市': ['忻府区', '定襄县', '五台县', '代县', '繁峙县', '宁武县', '静乐县', '神池县', '五寨县', '岢岚县', '河曲县', '保德县', '偏关县', '原平市'], + '临汾市': ['尧都区', '曲沃县', '翼城县', '襄汾县', '洪洞县', '古县', '安泽县', '浮山县', '吉县', '乡宁县', '大宁县', '隰县', '永和县', '蒲县', '汾西县', '侯马市', '霍州市'], + '吕梁市': ['离石区', '文水县', '交城县', '兴县', '临县', '柳林县', '石楼县', '岚县', '方山县', '中阳县', '交口县', '孝义市', '汾阳市'] + }, + '内蒙古自治区': { + '呼和浩特市': ['新城区', '回民区', '玉泉区', '赛罕区', '土默特左旗', '托克托县', '和林格尔县', '清水河县', '武川县'], + '包头市': ['东河区', '昆都仑区', '青山区', '石拐区', '白云鄂博矿区', '九原区', '土默特右旗', '固阳县', '达尔罕茂明安联合旗'], + '乌海市': ['海勃湾区', '海南区', '乌达区'], + '赤峰市': ['红山区', '元宝山区', '松山区', '阿鲁科尔沁旗', '巴林左旗', '巴林右旗', '林西县', '克什克腾旗', '翁牛特旗', '喀喇沁旗', '宁城县', '敖汉旗'], + '通辽市': ['科尔沁区', '科尔沁左翼中旗', '科尔沁左翼后旗', '开鲁县', '库伦旗', '奈曼旗', '扎鲁特旗', '霍林郭勒市'], + '鄂尔多斯市': ['东胜区', '康巴什区', '达拉特旗', '准格尔旗', '鄂托克前旗', '鄂托克旗', '杭锦旗', '乌审旗', '伊金霍洛旗'], + '呼伦贝尔市': ['海拉尔区', '扎赉诺尔区', '阿荣旗', '莫力达瓦达斡尔族自治旗', '鄂伦春自治旗', '鄂温克族自治旗', '陈巴尔虎旗', '新巴尔虎左旗', '新巴尔虎右旗', '满洲里市', '牙克石市', '扎兰屯市', '额尔古纳市', '根河市'], + '巴彦淖尔市': ['临河区', '五原县', '磴口县', '乌拉特前旗', '乌拉特中旗', '乌拉特后旗', '杭锦后旗'], + '乌兰察布市': ['集宁区', '卓资县', '化德县', '商都县', '兴和县', '凉城县', '察哈尔右翼前旗', '察哈尔右翼中旗', '察哈尔右翼后旗', '四子王旗', '丰镇市'], + '兴安盟': ['乌兰浩特市', '阿尔山市', '科尔沁右翼前旗', '科尔沁右翼中旗', '扎赉特旗', '突泉县'], + '锡林郭勒盟': ['锡林浩特市', '阿巴嘎旗', '苏尼特左旗', '苏尼特右旗', '东乌珠穆沁旗', '西乌珠穆沁旗', '太仆寺旗', '镶黄旗', '正镶白旗', '正蓝旗', '多伦县', '二连浩特市'], + '阿拉善盟': ['阿拉善左旗', '阿拉善右旗', '额济纳旗'] + }, + '辽宁省': { + '沈阳市': ['和平区', '沈河区', '大东区', '皇姑区', '铁西区', '苏家屯区', '浑南区', '沈北新区', '于洪区', '辽中区', '康平县', '法库县', '新民市'], + '大连市': ['中山区', '西岗区', '沙河口区', '甘井子区', '旅顺口区', '金州区', '普兰店区', '长海县', '瓦房店市', '庄河市'], + '鞍山市': ['铁东区', '铁西区', '立山区', '千山区', '台安县', '岫岩满族自治县', '海城市'], + '抚顺市': ['新抚区', '东洲区', '望花区', '顺城区', '抚顺县', '新宾满族自治县', '清原满族自治县'], + '本溪市': ['平山区', '溪湖区', '明山区', '南芬区', '本溪满族自治县', '桓仁满族自治县'], + '丹东市': ['元宝区', '振兴区', '振安区', '宽甸满族自治县', '东港市', '凤城市'], + '锦州市': ['古塔区', '凌河区', '太和区', '黑山县', '义县', '凌海市', '北镇市'], + '营口市': ['站前区', '西市区', '鲅鱼圈区', '老边区', '盖州市', '大石桥市'], + '阜新市': ['海州区', '新邱区', '太平区', '清河门区', '细河区', '阜新蒙古族自治县', '彰武县'], + '辽阳市': ['白塔区', '文圣区', '宏伟区', '弓长岭区', '太子河区', '辽阳县', '灯塔市'], + '盘锦市': ['双台子区', '兴隆台区', '大洼区', '盘山县'], + '铁岭市': ['银州区', '清河区', '铁岭县', '西丰县', '昌图县', '调兵山市', '开原市'], + '朝阳市': ['双塔区', '龙城区', '朝阳县', '建平县', '喀喇沁左翼蒙古族自治县', '北票市', '凌源市'], + '葫芦岛市': ['连山区', '龙港区', '南票区', '绥中县', '建昌县', '兴城市'] + }, + '吉林省': { + '长春市': ['南关区', '宽城区', '朝阳区', '二道区', '绿园区', '双阳区', '九台区', '农安县', '榆树市', '德惠市'], + '吉林市': ['昌邑区', '龙潭区', '船营区', '丰满区', '永吉县', '蛟河市', '桦甸市', '舒兰市', '磐石市'], + '四平市': ['铁西区', '铁东区', '梨树县', '伊通满族自治县', '公主岭市', '双辽市'], + '辽源市': ['龙山区', '西安区', '东丰县', '东辽县'], + '通化市': ['东昌区', '二道江区', '通化县', '辉南县', '柳河县', '梅河口市', '集安市'], + '白山市': ['浑江区', '江源区', '抚松县', '靖宇县', '长白朝鲜族自治县', '临江市'], + '松原市': ['宁江区', '前郭尔罗斯蒙古族自治县', '长岭县', '乾安县', '扶余市'], + '白城市': ['洮北区', '镇赖县', '通榆县', '洮南市', '大安市'], + '延边朝鲜族自治州': ['延吉市', '图们市', '敦化市', '珲春市', '龙井市', '和龙市', '汪清县', '安图县'] + }, + '黑龙江省': { + '哈尔滨市': ['道里区', '南岗区', '道外区', '平房区', '松北区', '香坊区', '呼兰区', '阿城区', '双城区', '依兰县', '方正县', '宾县', '巴彦县', '木兰县', '通河县', '延寿县', '尚志市', '五常市'], + '齐齐哈尔市': ['龙沙区', '建华区', '铁锋区', '昂昂溪区', '富拉尔基区', '碾子山区', '梅里斯达斡尔族区', '龙江县', '依安县', '泰来县', '甘南县', '富裕县', '克山县', '克东县', '拜泉县', '讷河市'], + '鸡西市': ['鸡冠区', '恒山区', '滴道区', '梨树区', '城子河区', '麻山区', '鸡东县', '虎林市', '密山市'], + '鹤岗市': ['向阳区', '工农区', '南山区', '兴安区', '东山区', '兴山区', '萝北县', '绥滨县'], + '双鸭山市': ['尖山区', '岭东区', '四方台区', '宝山区', '集贤县', '友谊县', '宝清县', '饶河县'], + '大庆市': ['萨尔图区', '龙凤区', '让胡路区', '红岗区', '大同区', '肇州县', '肇源县', '林甸县', '杜尔伯特蒙古族自治县'], + '伊春市': ['伊春区', '南岔区', '友好区', '西林区', '翠峦区', '新青区', '美溪区', '金山屯区', '五营区', '乌马河区', '汤旺河区', '带岭区', '乌伊岭区', '红星区', '上甘岭区', '嘉荫县', '铁力市'], + '佳木斯市': ['向阳区', '前进区', '东风区', '郊区', '桦南县', '桦川县', '汤原县', '抚远市', '同江市', '富锦市'], + '七台河市': ['新兴区', '桃山区', '茄子河区', '勃利县'], + '牡丹江市': ['东安区', '阳明区', '爱民区', '西安区', '林口县', '绥芬河市', '海林市', '宁安市', '穆棱市', '东宁市'], + '黑河市': ['爱辉区', '嫩江县', '逊克县', '孙吴县', '北安市', '五大连池市'], + '绥化市': ['北林区', '望奎县', '兰西县', '青冈县', '庆安县', '明水县', '绥棱县', '安达市', '肇东市', '海伦市'], + '大兴安岭地区': ['呼玛县', '塔河县', '漠河市'] + }, + '江苏省': { + '南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区', '雨花台区', '江宁区', '六合区', '溧水区', '高淳区'], + '无锡市': ['锡山区', '惠山区', '滨湖区', '梁溪区', '新吴区', '江阴市', '宜兴市'], + '徐州市': ['鼓楼区', '云龙区', '贾汪区', '泉山区', '铜山区', '丰县', '沛县', '睢宁县', '新沂市', '邳州市'], + '常州市': ['天宁区', '钟楼区', '新北区', '武进区', '金坛区', '溧阳市'], + '苏州市': ['虎丘区', '吴中区', '相城区', '姑苏区', '吴江区', '常熟市', '张家港市', '昆山市', '太仓市'], + '南通市': ['崇川区', '港闸区', '通州区', '海安市', '如东县', '启东市', '如皋市', '海门市'], + '连云港市': ['连云区', '海州区', '赣榆区', '东海县', '灌云县', '灌南县'], + '淮安市': ['淮安区', '淮阴区', '清江浦区', '洪泽区', '涟水县', '盱眙县', '金湖县'], + '盐城市': ['亭湖区', '盐都区', '大丰区', '响水县', '滨海县', '阜宁县', '射阳县', '建湖县', '东台市'], + '扬州市': ['广陵区', '邗江区', '江都区', '宝应县', '仪征市', '高邮市'], + '镇江市': ['京口区', '润州区', '丹徒区', '丹阳市', '扬中市', '句容市'], + '泰州市': ['海陵区', '高港区', '姜堰区', '兴化市', '靖江市', '泰兴市'], + '宿迁市': ['宿城区', '宿豫区', '沭阳县', '泗阳县', '泗洪县'] + }, + '浙江省': { + '杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区', '萧山区', '余杭区', '富阳区', '临安区', '桐庐县', '淳安县', '建德市'], + '宁波市': ['海曙区', '江北区', '北仑区', '镇海区', '鄞州区', '奉化区', '象山县', '宁海县', '余姚市', '慈溪市'], + '温州市': ['鹿城区', '龙湾区', '瓯海区', '洞头区', '永嘉县', '平阳县', '苍南县', '文成县', '泰顺县', '瑞安市', '乐清市'], + '嘉兴市': ['南湖区', '秀洲区', '嘉善县', '海盐县', '海宁市', '平湖市', '桐乡市'], + '湖州市': ['吴兴区', '南浔区', '德清县', '长兴县', '安吉县'], + '绍兴市': ['越城区', '柯桥区', '上虞区', '新昌县', '诸暨市', '嵊州市'], + '金华市': ['婺城区', '金东区', '武义县', '浦江县', '磐安县', '兰溪市', '义乌市', '东阳市', '永康市'], + '衢州市': ['柯城区', '衢江区', '常山县', '开化县', '龙游县', '江山市'], + '舟山市': ['定海区', '普陀区', '岱山县', '嵊泗县'], + '台州市': ['椒江区', '黄岩区', '路桥区', '三门县', '天台县', '仙居县', '温岭市', '临海市', '玉环市'], + '丽水市': ['莲都区', '青田县', '缙云县', '遂昌县', '松阳县', '云和县', '庆元县', '景宁畲族自治县', '龙泉市'] + }, + '安徽省': { + '合肥市': ['瑶海区', '庐阳区', '蜀山区', '包河区', '长丰县', '肥东县', '肥西县', '庐江县', '巢湖市'], + '芜湖市': ['镜湖区', '弋江区', '鸠江区', '三山区', '芜湖县', '繁昌县', '南陵县', '无为市'], + '蚌埠市': ['龙子湖区', '蚌山区', '禹会区', '淮上区', '怀远县', '五河县', '固镇县'], + '淮南市': ['大通区', '田家庵区', '谢家集区', '八公山区', '潘集区', '凤台县', '寿县'], + '马鞍山市': ['花山区', '雨山区', '博望区', '当涂县', '含山县', '和县'], + '淮北市': ['杜集区', '相山区', '烈山区', '濉溪县'], + '铜陵市': ['铜官区', '义安区', '郊区', '枞阳县'], + '安庆市': ['迎江区', '大观区', '宜秀区', '怀宁县', '潜山市', '太湖县', '宿松县', '望江县', '岳西县', '桐城市'], + '黄山市': ['屯溪区', '黄山区', '徽州区', '歙县', '休宁县', '黟县', '祁门县'], + '滁州市': ['琅琊区', '南谯区', '来安县', '全椒县', '定远县', '凤阳县', '天长市', '明光市'], + '阜阳市': ['颍州区', '颍东区', '颍泉区', '临泉县', '太和县', '阜南县', '颍上县', '界首市'], + '宿州市': ['埇桥区', '砀山县', '萧县', '灵璧县', '泗县'], + '六安市': ['金安区', '裕安区', '叶集区', '霍邱县', '舒城县', '金寨县', '霍山县'], + '亳州市': ['谯城区', '涡阳县', '蒙城县', '利辛县'], + '池州市': ['贵池区', '东至县', '石台县', '青阳县'], + '宣城市': ['宣州区', '郎溪县', '广德市', '泾县', '绩溪县', '旌德县', '宁国市'] + }, + '福建省': { + '福州市': ['鼓楼区', '台江区', '仓山区', '马尾区', '晋安区', '长乐区', '闽侯县', '连江县', '罗源县', '闽清县', '永泰县', '平潭县', '福清市'], + '厦门市': ['思明区', '海沧区', '湖里区', '集美区', '同安区', '翔安区'], + '莆田市': ['城厢区', '涵江区', '荔城区', '秀屿区', '仙游县'], + '三明市': ['梅列区', '三元区', '明溪县', '清流县', '宁化县', '大田县', '尤溪县', '沙县', '将乐县', '泰宁县', '建宁县', '永安市'], + '泉州市': ['鲤城区', '丰泽区', '洛江区', '泉港区', '惠安县', '安溪县', '永春县', '德化县', '金门县', '石狮市', '晋江市', '南安市'], + '漳州市': ['芗城区', '龙文区', '云霄县', '漳浦县', '诏安县', '长泰县', '东山县', '南靖县', '平和县', '华安县', '龙海市'], + '南平市': ['延平区', '建阳区', '顺昌县', '浦城县', '光泽县', '松溪县', '政和县', '邵武市', '武夷山市', '建瓯市'], + '龙岩市': ['新罗区', '永定区', '长汀县', '上杭县', '武平县', '连城县', '漳平市'], + '宁德市': ['蕉城区', '霞浦县', '古田县', '屏南县', '寿宁县', '周宁县', '柘荣县', '福安市', '福鼎市'] + }, + '江西省': { + '南昌市': ['东湖区', '西湖区', '青云谱区', '湾里区', '青山湖区', '新建区', '南昌县', '安义县', '进贤县'], + '景德镇市': ['昌江区', '珠山区', '浮梁县', '乐平市'], + '萍乡市': ['安源区', '湘东区', '莲花县', '上栗县', '芦溪县'], + '九江市': ['濂溪区', '浔阳区', '柴桑区', '武宁县', '修水县', '永修县', '德安县', '都昌县', '湖口县', '彭泽县', '瑞昌市', '共青城市', '庐山市'], + '新余市': ['渝水区', '分宜县'], + '鹰潭市': ['月湖区', '余江区', '贵溪市'], + '赣州市': ['章贡区', '南康区', '赣县区', '信丰县', '大余县', '上犹县', '崇义县', '安远县', '龙南县', '定南县', '全南县', '宁都县', '于都县', '兴国县', '会昌县', '寻乌县', '石城县', '瑞金市'], + '吉安市': ['吉州区', '青原区', '吉安县', '吉水县', '峡江县', '新干县', '永丰县', '泰和县', '遂川县', '万安县', '安福县', '永新县', '井冈山市'], + '宜春市': ['袁州区', '奉新县', '万载县', '上高县', '宜丰县', '靖安县', '铜鼓县', '丰城市', '樟树市', '高安市'], + '抚州市': ['临川区', '东乡区', '南城县', '黎川县', '南丰县', '崇仁县', '乐安县', '宜黄县', '金溪县', '资溪县', '广昌县'], + '上饶市': ['信州区', '广丰区', '广信区', '玉山县', '铅山县', '横峰县', '弋阳县', '余干县', '鄱阳县', '万年县', '婺源县', '德兴市'] + }, + '山东省': { + '济南市': ['历下区', '市中区', '槐荫区', '天桥区', '历城区', '长清区', '章丘区', '济阳区', '莱芜区', '钢城区', '平阴县', '商河县'], + '青岛市': ['市南区', '市北区', '黄岛区', '崂山区', '李沧区', '城阳区', '即墨区', '胶州市', '平度市', '莱西市'], + '淄博市': ['淄川区', '张店区', '博山区', '临淄区', '周村区', '桓台县', '高青县', '沂源县'], + '枣庄市': ['市中区', '薛城区', '峄城区', '台儿庄区', '山亭区', '滕州市'], + '东营市': ['东营区', '河口区', '垦利区', '利津县', '广饶县'], + '烟台市': ['芝罘区', '福山区', '牟平区', '莱山区', '长岛县', '龙口市', '莱阳市', '莱州市', '蓬莱市', '招远市', '栖霞市', '海阳市'], + '潍坊市': ['潍城区', '寒亭区', '坊子区', '奎文区', '临朐县', '昌乐县', '青州市', '诸城市', '寿光市', '安丘市', '高密市', '昌邑市'], + '济宁市': ['任城区', '兖州区', '微山县', '鱼台县', '金乡县', '嘉祥县', '汶上县', '泗水县', '梁山县', '曲阜市', '邹城市'], + '泰安市': ['泰山区', '岱岳区', '宁阳县', '东平县', '新泰市', '肥城市'], + '威海市': ['环翠区', '文登区', '荣成市', '乳山市'], + '日照市': ['东港区', '岚山区', '五莲县', '莒县'], + '临沂市': ['兰山区', '罗庄区', '河东区', '沂南县', '郯城县', '沂水县', '兰陵县', '费县', '平邑县', '莒南县', '蒙阴县', '临沭县'], + '德州市': ['德城区', '陵城区', '宁津县', '庆云县', '临邑县', '齐河县', '平原县', '夏津县', '武城县', '乐陵市', '禹城市'], + '聊城市': ['东昌府区', '茌平区', '阳谷县', '莘县', '茌平县', '东阿县', '冠县', '高唐县', '临清市'], + '滨州市': ['滨城区', '沾化区', '惠民县', '阳信县', '无棣县', '博兴县', '邹平市'], + '菏泽市': ['牡丹区', '定陶区', '曹县', '单县', '成武县', '巨野县', '郓城县', '鄄城县', '东明县'] + }, + '河南省': { + '郑州市': ['中原区', '二七区', '管城回族区', '金水区', '上街区', '惠济区', '中牟县', '巩义市', '荥阳市', '新密市', '新郑市', '登封市'], + '开封市': ['龙亭区', '顺河回族区', '鼓楼区', '禹王台区', '祥符区', '杞县', '通许县', '尉氏县', '兰考县'], + '洛阳市': ['老城区', '西工区', '瀍河回族区', '涧西区', '吉利区', '洛龙区', '孟津县', '新安县', '栾川县', '嵩县', '汝阳县', '宜阳县', '洛宁县', '伊川县', '偃师市'], + '平顶山市': ['新华区', '卫东区', '石龙区', '湛河区', '宝丰县', '叶县', '鲁山县', '郏县', '舞钢市', '汝州市'], + '安阳市': ['文峰区', '北关区', '殷都区', '龙安区', '安阳县', '汤阴县', '滑县', '内黄县', '林州市'], + '鹤壁市': ['鹤山区', '山城区', '淇滨区', '浚县', '淇县'], + '新乡市': ['红旗区', '卫滨区', '凤泉区', '牧野区', '新乡县', '获嘉县', '原阳县', '延津县', '封丘县', '长垣市', '卫辉市', '辉县市'], + '焦作市': ['解放区', '中站区', '马村区', '山阳区', '修武县', '博爱县', '武陟县', '温县', '沁阳市', '孟州市'], + '濮阳市': ['华龙区', '清丰县', '南乐县', '范县', '台前县', '濮阳县'], + '许昌市': ['魏都区', '建安区', '鄢陵县', '襄城县', '禹州市', '长葛市'], + '漯河市': ['源汇区', '郾城区', '召陵区', '舞阳县', '临颍县'], + '三门峡市': ['湖滨区', '陕州区', '渑池县', '卢氏县', '义马市', '灵宝市'], + '南阳市': ['宛城区', '卧龙区', '南召县', '方城县', '西峡县', '镇平县', '内乡县', '淅川县', '社旗县', '唐河县', '新野县', '桐柏县', '邓州市'], + '商丘市': ['梁园区', '睢阳区', '民权县', '睢县', '宁陵县', '柘城县', '虞城县', '夏邑县', '永城市'], + '信阳市': ['浉河区', '平桥区', '罗山县', '光山县', '新县', '商城县', '固始县', '潢川县', '淮滨县', '息县'], + '周口市': ['川汇区', '扶沟县', '西华县', '商水县', '沈丘县', '郸城县', '淮阳区', '太康县', '鹿邑县', '项城市'], + '驻马店市': ['驿城区', '西平县', '上蔡县', '平舆县', '正阳县', '确山县', '泌阳县', '汝南县', '遂平县', '新蔡县'], + '济源市': ['济源市'] + }, + '湖北省': { + '武汉市': ['江岸区', '江汉区', '硚口区', '汉阳区', '武昌区', '青山区', '洪山区', '东西湖区', '汉南区', '蔡甸区', '江夏区', '黄陂区', '新洲区'], + '黄石市': ['黄石港区', '西塞山区', '下陆区', '铁山区', '阳新县', '大冶市'], + '十堰市': ['茅箭区', '张湾区', '郧阳区', '郧西县', '竹山县', '竹溪县', '房县', '丹江口市'], + '宜昌市': ['西陵区', '伍家岗区', '点军区', '猇亭区', '夷陵区', '远安县', '兴山县', '秭归县', '长阳土家族自治县', '五峰土家族自治县', '宜都市', '当阳市', '枝江市'], + '襄阳市': ['襄城区', '樊城区', '襄州区', '南漳县', '谷城县', '保康县', '老河口市', '枣阳市', '宜城市'], + '鄂州市': ['梁子湖区', '华容区', '鄂城区'], + '荆门市': ['东宝区', '掇刀区', '京山市', '沙洋县', '钟祥市'], + '孝感市': ['孝南区', '孝昌县', '大悟县', '云梦县', '应城市', '安陆市', '汉川市'], + '荆州市': ['沙市区', '荆州区', '公安县', '监利县', '江陵县', '石首市', '洪湖市', '松滋市'], + '黄冈市': ['黄州区', '团风县', '红安县', '罗田县', '英山县', '浠水县', '蕲春县', '黄梅县', '麻城市', '武穴市'], + '咸宁市': ['咸安区', '嘉鱼县', '通城县', '崇阳县', '通山县', '赤壁市'], + '随州市': ['曾都区', '随县', '广水市'], + '恩施土家族苗族自治州': ['恩施市', '利川市', '建始县', '巴东县', '宣恩县', '咸丰县', '来凤县', '鹤峰县'], + '仙桃市': ['仙桃市'], + '潜江市': ['潜江市'], + '天门市': ['天门市'], + '神农架林区': ['神农架林区'] + }, + '湖南省': { + '长沙市': ['芙蓉区', '天心区', '岳麓区', '开福区', '雨花区', '望城区', '长沙县', '宁乡市', '浏阳市'], + '株洲市': ['荷塘区', '芦淞区', '石峰区', '天元区', '渌口区', '攸县', '茶陵县', '炎陵县', '醴陵市'], + '湘潭市': ['雨湖区', '岳塘区', '湘潭县', '湘乡市', '韶山市'], + '衡阳市': ['珠晖区', '雁峰区', '石鼓区', '蒸湘区', '南岳区', '衡阳县', '衡南县', '衡山县', '衡东县', '祁东县', '耒阳市', '常宁市'], + '邵阳市': ['双清区', '大祥区', '北塔区', '邵东市', '新邵县', '邵阳县', '隆回县', '洞口县', '绥宁县', '新宁县', '城步苗族自治县', '武冈市'], + '岳阳市': ['岳阳楼区', '云溪区', '君山区', '岳阳县', '华容县', '湘阴县', '平江县', '汨罗市', '临湘市'], + '常德市': ['武陵区', '鼎城区', '安乡县', '汉寿县', '澧县', '临澧县', '桃源县', '石门县', '津市市'], + '张家界市': ['永定区', '武陵源区', '慈利县', '桑植县'], + '益阳市': ['资阳区', '赫山区', '南县', '桃江县', '安化县', '沅江市'], + '郴州市': ['北湖区', '苏仙区', '桂阳县', '宜章县', '永兴县', '嘉禾县', '临武县', '汝城县', '桂东县', '安仁县', '资兴市'], + '永州市': ['零陵区', '冷水滩区', '祁阳县', '东安县', '双牌县', '道县', '江永县', '宁远县', '蓝山县', '新田县', '江华瑶族自治县'], + '怀化市': ['鹤城区', '中方县', '沅陵县', '辰溪县', '溆浦县', '会同县', '麻阳苗族自治县', '新晃侗族自治县', '芷江侗族自治县', '靖州苗族侗族自治县', '通道侗族自治县', '洪江市'], + '娄底市': ['娄星区', '双峰县', '新化县', '冷水江市', '涟源市'], + '湘西土家族苗族自治州': ['吉首市', '泸溪县', '凤凰县', '花垣县', '保靖县', '古丈县', '永顺县', '龙山县'] + }, + '广东省': { + '广州市': ['荔湾区', '越秀区', '海珠区', '天河区', '白云区', '黄埔区', '番禺区', '花都区', '南沙区', '从化区', '增城区'], + '深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区', '龙华区', '坪山区', '光明区', '大鹏新区'], + '珠海市': ['香洲区', '斗门区', '金湾区'], + '汕头市': ['龙湖区', '金平区', '濠江区', '潮阳区', '潮南区', '澄海区', '南澳县'], + '佛山市': ['禅城区', '南海区', '顺德区', '三水区', '高明区'], + '韶关市': ['武江区', '浈江区', '曲江区', '始兴县', '仁化县', '翁源县', '乳源瑶族自治县', '新丰县', '乐昌市', '南雄市'], + '湛江市': ['赤坎区', '霞山区', '坡头区', '麻章区', '遂溪县', '徐闻县', '廉江市', '雷州市', '吴川市'], + '肇庆市': ['端州区', '鼎湖区', '高要区', '广宁县', '怀集县', '封开县', '德庆县', '四会市'], + '江门市': ['蓬江区', '江海区', '新会区', '台山市', '开平市', '鹤山市', '恩平市'], + '茂名市': ['茂南区', '电白区', '高州市', '化州市', '信宜市'], + '惠州市': ['惠城区', '惠阳区', '博罗县', '惠东县', '龙门县'], + '梅州市': ['梅江区', '梅县区', '大埔县', '丰顺县', '五华县', '平远县', '蕉岭县', '兴宁市'], + '汕尾市': ['城区', '海丰县', '陆河县', '陆丰市'], + '河源市': ['源城区', '紫金县', '龙川县', '连平县', '和平县', '东源县'], + '阳江市': ['江城区', '阳东区', '阳西县', '阳春市'], + '清远市': ['清城区', '清新区', '佛冈县', '阳山县', '连山壮族瑶族自治县', '连南瑶族自治县', '英德市', '连州市'], + '东莞市': ['东莞市'], + '中山市': ['中山市'], + '潮州市': ['湘桥区', '潮安区', '饶平县'], + '揭阳市': ['榕城区', '揭东区', '揭西县', '惠来县', '普宁市'], + '云浮市': ['云城区', '云安区', '新兴县', '郁南县', '罗定市'] + }, + '广西壮族自治区': { + '南宁市': ['兴宁区', '青秀区', '江南区', '西乡塘区', '良庆区', '邕宁区', '武鸣区', '隆安县', '马山县', '上林县', '宾阳县', '横县'], + '柳州市': ['城中区', '鱼峰区', '柳南区', '柳北区', '柳江区', '柳城县', '鹿寨县', '融安县', '融水苗族自治县', '三江侗族自治县'], + '桂林市': ['秀峰区', '叠彩区', '象山区', '七星区', '雁山区', '临桂区', '阳朔县', '灵川县', '全州县', '兴安县', '永福县', '灌阳县', '龙胜各族自治县', '资源县', '平乐县', '荔浦市', '恭城瑶族自治县'], + '梧州市': ['万秀区', '长洲区', '龙圩区', '苍梧县', '藤县', '蒙山县', '岑溪市'], + '北海市': ['海城区', '银海区', '铁山港区', '合浦县'], + '防城港市': ['港口区', '防城区', '上思县', '东兴市'], + '钦州市': ['钦南区', '钦北区', '灵山县', '浦北县'], + '贵港市': ['港北区', '港南区', '覃塘区', '平南县', '桂平市'], + '玉林市': ['玉州区', '福绵区', '容县', '陆川县', '博白县', '兴业县', '北流市'], + '百色市': ['右江区', '田阳区', '田东县', '平果市', '德保县', '那坡县', '凌云县', '乐业县', '田林县', '西林县', '隆林各族自治县', '靖西市'], + '贺州市': ['八步区', '平桂区', '昭平县', '钟山县', '富川瑶族自治县'], + '河池市': ['金城江区', '宜州区', '南丹县', '天峨县', '凤山县', '东兰县', '罗城仫佬族自治县', '环江毛南族自治县', '巴马瑶族自治县', '都安瑶族自治县', '大化瑶族自治县'], + '来宾市': ['兴宾区', '忻城县', '象州县', '武宣县', '金秀瑶族自治县', '合山市'], + '崇左市': ['江州区', '扶绥县', '宁明县', '龙州县', '大新县', '天等县', '凭祥市'] + }, + '海南省': { + '海口市': ['秀英区', '龙华区', '琼山区', '美兰区'], + '三亚市': ['海棠区', '吉阳区', '天涯区', '崖州区'], + '三沙市': ['西沙群岛', '南沙群岛', '中沙群岛'], + '儋州市': ['儋州市'], + '五指山市': ['五指山市'], + '琼海市': ['琼海市'], + '文昌市': ['文昌市'], + '万宁市': ['万宁市'], + '东方市': ['东方市'], + '定安县': ['定安县'], + '屯昌县': ['屯昌县'], + '澄迈县': ['澄迈县'], + '临高县': ['临高县'], + '白沙黎族自治县': ['白沙黎族自治县'], + '昌江黎族自治县': ['昌江黎族自治县'], + '乐东黎族自治县': ['乐东黎族自治县'], + '陵水黎族自治县': ['陵水黎族自治县'], + '保亭黎族苗族自治县': ['保亭黎族苗族自治县'], + '琼中黎族苗族自治县': ['琼中黎族苗族自治县'] + }, + '四川省': { + '成都市': ['锦江区', '青羊区', '金牛区', '武侯区', '成华区', '龙泉驿区', '青白江区', '新都区', '温江区', '双流区', '郫都区', '新津区', '金堂县', '大邑县', '蒲江县', '都江堰市', '彭州市', '邛崃市', '崇州市', '简阳市'], + '自贡市': ['自流井区', '贡井区', '大安区', '沿滩区', '荣县', '富顺县'], + '攀枝花市': ['东区', '西区', '仁和区', '米易县', '盐边县'], + '泸州市': ['江阳区', '纳溪区', '龙马潭区', '泸县', '合江县', '叙永县', '古蔺县'], + '德阳市': ['旌阳区', '罗江区', '中江县', '广汉市', '什邡市', '绵竹市'], + '绵阳市': ['涪城区', '游仙区', '安州区', '三台县', '盐亭县', '梓潼县', '北川羌族自治县', '平武县', '江油市'], + '广元市': ['利州区', '昭化区', '朝天区', '旺苍县', '青川县', '剑阁县', '苍溪县'], + '遂宁市': ['船山区', '安居区', '蓬溪县', '射洪市', '大英县'], + '内江市': ['市中区', '东兴区', '威远县', '资中县', '隆昌市'], + '乐山市': ['市中区', '沙湾区', '五通桥区', '金口河区', '犍为县', '井研县', '夹江县', '沐川县', '峨边彝族自治县', '马边彝族自治县', '峨眉山市'], + '南充市': ['顺庆区', '高坪区', '嘉陵区', '南部县', '营山县', '蓬安县', '仪陇县', '西充县', '阆中市'], + '眉山市': ['东坡区', '彭山区', '仁寿县', '洪雅县', '丹棱县', '青神县'], + '宜宾市': ['翠屏区', '南溪区', '叙州区', '江安县', '长宁县', '高县', '珙县', '筠连县', '兴文县', '屏山县'], + '广安市': ['广安区', '前锋区', '岳池县', '武胜县', '邻水县', '华蓥市'], + '达州市': ['通川区', '达川区', '宣汉县', '开江县', '大竹县', '渠县', '万源市'], + '雅安市': ['雨城区', '名山区', '荥经县', '汉源县', '石棉县', '天全县', '芦山县', '宝兴县'], + '巴中市': ['巴州区', '恩阳区', '通江县', '南江县', '平昌县'], + '资阳市': ['雁江区', '安岳县', '乐至县'], + '阿坝藏族羌族自治州': ['马尔康市', '汶川县', '理县', '茂县', '松潘县', '九寨沟县', '金川县', '小金县', '黑水县', '壤塘县', '阿坝县', '若尔盖县', '红原县'], + '甘孜藏族自治州': ['康定市', '泸定县', '丹巴县', '九龙县', '雅江县', '道孚县', '炉霍县', '甘孜县', '新龙县', '德格县', '白玉县', '石渠县', '色达县', '理塘县', '巴塘县', '乡城县', '稻城县', '得荣县'], + '凉山彝族自治州': ['西昌市', '木里藏族自治县', '盐源县', '德昌县', '会理市', '会东县', '宁南县', '普格县', '布拖县', '金阳县', '昭觉县', '喜德县', '冕宁县', '越西县', '甘洛县', '美姑县', '雷波县'] + }, + '贵州省': { + '贵阳市': ['南明区', '云岩区', '花溪区', '乌当区', '白云区', '观山湖区', '开阳县', '息烽县', '修文县', '清镇市'], + '六盘水市': ['钟山区', '六枝特区', '水城区', '盘州市'], + '遵义市': ['红花岗区', '汇川区', '播州区', '桐梓县', '绥阳县', '正安县', '道真仡佬族苗族自治县', '务川仡佬族苗族自治县', '凤冈县', '湄潭县', '余庆县', '习水县', '赤水市', '仁怀市'], + '安顺市': ['西秀区', '平坝区', '普定县', '镇宁布依族苗族自治县', '关岭布依族苗族自治县', '紫云苗族布依族自治县'], + '毕节市': ['七星关区', '大方县', '黔西市', '金沙县', '织金县', '纳雍县', '威宁彝族回族苗族自治县', '赫章县'], + '铜仁市': ['碧江区', '万山区', '江口县', '玉屏侗族自治县', '石阡县', '思南县', '印江土家族苗族自治县', '德江县', '沿河土家族自治县', '松桃苗族自治县'], + '黔西南布依族苗族自治州': ['兴义市', '兴仁市', '普安县', '晴隆县', '贞丰县', '望谟县', '册亨县', '安龙县'], + '黔东南苗族侗族自治州': ['凯里市', '黄平县', '施秉县', '三穗县', '镇远县', '岑巩县', '天柱县', '锦屏县', '剑河县', '台江县', '黎平县', '榕江县', '从江县', '雷山县', '麻江县', '丹寨县'], + '黔南布依族苗族自治州': ['都匀市', '福泉市', '荔波县', '贵定县', '瓮安县', '独山县', '平塘县', '罗甸县', '长顺县', '龙里县', '惠水县', '三都水族自治县'] + }, + '云南省': { + '昆明市': ['五华区', '盘龙区', '官渡区', '西山区', '东川区', '呈贡区', '晋宁区', '富民县', '宜良县', '石林彝族自治县', '嵩明县', '禄劝彝族苗族自治县', '寻甸回族彝族自治县', '安宁市'], + '曲靖市': ['麒麟区', '沾益区', '马龙区', '陆良县', '师宗县', '罗平县', '富源县', '会泽县', '宣威市'], + '玉溪市': ['红塔区', '江川区', '澄江市', '通海县', '华宁县', '易门县', '峨山彝族自治县', '新平彝族傣族自治县', '元江哈尼族彝族傣族自治县'], + '保山市': ['隆阳区', '施甸县', '龙陵县', '昌宁县', '腾冲市'], + '昭通市': ['昭阳区', '鲁甸县', '巧家县', '盐津县', '大关县', '永善县', '绥江县', '镇雄县', '彝良县', '威信县', '水富市'], + '丽江市': ['古城区', '玉龙纳西族自治县', '永胜县', '华坪县', '宁蒗彝族自治县'], + '普洱市': ['思茅区', '宁洱哈尼族彝族自治县', '墨江哈尼族自治县', '景东彝族自治县', '景谷傣族彝族自治县', '镇沅彝族哈尼族拉祜族自治县', '江城哈尼族彝族自治县', '孟连傣族拉祜族佤族自治县', '澜沧拉祜族自治县', '西盟佤族自治县'], + '临沧市': ['临翔区', '凤庆县', '云县', '永德县', '镇康县', '双江拉祜族佤族布朗族傣族自治县', '耿马傣族佤族自治县', '沧源佤族自治县'], + '楚雄彝族自治州': ['楚雄市', '双柏县', '牟定县', '南华县', '姚安县', '大姚县', '永仁县', '元谋县', '武定县', '禄丰市'], + '红河哈尼族彝族自治州': ['个旧市', '开远市', '蒙自市', '弥勒市', '屏边苗族自治县', '建水县', '石屏县', '泸西县', '元阳县', '红河县', '金平苗族瑶族傣族自治县', '绿春县', '河口瑶族自治县'], + '文山壮族苗族自治州': ['文山市', '砚山县', '西畴县', '麻栗坡县', '马关县', '丘北县', '广南县', '富宁县'], + '西双版纳傣族自治州': ['景洪市', '勐海县', '勐腊县'], + '大理白族自治州': ['大理市', '漾濞彝族自治县', '祥云县', '宾川县', '弥渡县', '南涧彝族自治县', '巍山彝族回族自治县', '永平县', '云龙县', '洱源县', '剑川县', '鹤庆县'], + '德宏傣族景颇族自治州': ['瑞丽市', '芒市', '梁河县', '盈江县', '陇川县'], + '怒江傈僳族自治州': ['泸水市', '福贡县', '贡山独龙族怒族自治县', '兰坪白族普米族自治县'], + '迪庆藏族自治州': ['香格里拉市', '德钦县', '维西傈僳族自治县'] + }, + '西藏自治区': { + '拉萨市': ['城关区', '堆龙德庆区', '达孜区', '林周县', '当雄县', '尼木县', '曲水县', '墨竹工卡县'], + '日喀则市': ['桑珠孜区', '南木林县', '江孜县', '定日县', '萨迦县', '拉孜县', '昂仁县', '谢通门县', '白朗县', '仁布县', '康马县', '定结县', '仲巴县', '亚东县', '吉隆县', '聂拉木县', '萨嘎县', '岗巴县'], + '昌都市': ['卡若区', '江达县', '贡觉县', '类乌齐县', '丁青县', '察雅县', '八宿县', '左贡县', '芒康县', '洛隆县', '边坝县'], + '林芝市': ['巴宜区', '工布江达县', '米林县', '墨脱县', '波密县', '察隅县', '朗县'], + '山南市': ['乃东区', '扎囊县', '贡嘎县', '桑日县', '琼结县', '曲松县', '措美县', '洛扎县', '加查县', '隆子县', '错那县', '浪卡子县'], + '那曲市': ['色尼区', '嘉黎县', '比如县', '聂荣县', '安多县', '申扎县', '索县', '班戈县', '巴青县', '尼玛县', '双湖县'], + '阿里地区': ['普兰县', '札达县', '噶尔县', '日土县', '革吉县', '改则县', '措勤县'] + }, + '陕西省': { + '西安市': ['新城区', '碑林区', '莲湖区', '灞桥区', '未央区', '雁塔区', '阎良区', '临潼区', '长安区', '高陵区', '鄠邑区', '蓝田县', '周至县'], + '铜川市': ['王益区', '印台区', '耀州区', '宜君县'], + '宝鸡市': ['渭滨区', '金台区', '陈仓区', '凤翔区', '岐山县', '扶风县', '眉县', '陇县', '千阳县', '麟游县', '凤县', '太白县'], + '咸阳市': ['秦都区', '杨陵区', '渭城区', '三原县', '泾阳县', '乾县', '礼泉县', '永寿县', '长武县', '旬邑县', '淳化县', '武功县', '兴平市', '彬州市'], + '渭南市': ['临渭区', '华州区', '潼关县', '大荔县', '合阳县', '澄城县', '蒲城县', '白水县', '富平县', '韩城市', '华阴市'], + '延安市': ['宝塔区', '安塞区', '延长县', '延川县', '志丹县', '吴起县', '甘泉县', '富县', '洛川县', '宜川县', '黄龙县', '黄陵县', '子长市'], + '汉中市': ['汉台区', '南郑区', '城固县', '洋县', '西乡县', '勉县', '宁强县', '略阳县', '镇巴县', '留坝县', '佛坪县'], + '榆林市': ['榆阳区', '横山区', '府谷县', '靖边县', '定边县', '绥德县', '米脂县', '佳县', '吴堡县', '清涧县', '子洲县', '神木市'], + '安康市': ['汉滨区', '汉阴县', '石泉县', '宁陕县', '紫阳县', '岚皋县', '平利县', '镇坪县', '旬阳县', '白河县'], + '商洛市': ['商州区', '洛南县', '丹凤县', '商南县', '山阳县', '镇安县', '柞水县'] + }, + '甘肃省': { + '兰州市': ['城关区', '七里河区', '西固区', '安宁区', '红古区', '永登县', '皋兰县', '榆中县'], + '嘉峪关市': ['嘉峪关市'], + '金昌市': ['金川区', '永昌县'], + '白银市': ['白银区', '平川区', '靖远县', '会宁县', '景泰县'], + '天水市': ['秦州区', '麦积区', '清水县', '秦安县', '甘谷县', '武山县', '张家川回族自治县'], + '武威市': ['凉州区', '民勤县', '古浪县', '天祝藏族自治县'], + '张掖市': ['甘州区', '肃南裕固族自治县', '民乐县', '临泽县', '高台县', '山丹县'], + '平凉市': ['崆峒区', '泾川县', '灵台县', '崇信县', '华亭市', '庄浪县', '静宁县'], + '酒泉市': ['肃州区', '金塔县', '瓜州县', '肃北蒙古族自治县', '阿克塞哈萨克族自治县', '玉门市', '敦煌市'], + '庆阳市': ['西峰区', '庆城县', '环县', '华池县', '合水县', '正宁县', '宁县', '镇原县'], + '定西市': ['安定区', '通渭县', '陇西县', '渭源县', '临洮县', '漳县', '岷县'], + '陇南市': ['武都区', '成县', '文县', '宕昌县', '康县', '西和县', '礼县', '徽县', '两当县'], + '临夏回族自治州': ['临夏市', '临夏县', '康乐县', '永靖县', '广河县', '和政县', '东乡族自治县', '积石山保安族东乡族撒拉族自治县'], + '甘南藏族自治州': ['合作市', '临潭县', '卓尼县', '舟曲县', '迭部县', '玛曲县', '碌曲县', '夏河县'] + }, + '青海省': { + '西宁市': ['城东区', '城中区', '城西区', '城北区', '大通回族土族自治县', '湟中区', '湟源县'], + '海东市': ['乐都区', '平安区', '民和回族土族自治县', '互助土族自治县', '化隆回族自治县', '循化撒拉族自治县'], + '海北藏族自治州': ['门源回族自治县', '祁连县', '海晏县', '刚察县'], + '黄南藏族自治州': ['同仁市', '尖扎县', '泽库县', '河南蒙古族自治县'], + '海南藏族自治州': ['共和县', '同德县', '贵德县', '兴海县', '贵南县'], + '果洛藏族自治州': ['玛沁县', '班玛县', '甘德县', '达日县', '久治县', '玛多县'], + '玉树藏族自治州': ['玉树市', '杂多县', '称多县', '治多县', '囊谦县', '曲麻莱县'], + '海西蒙古族藏族自治州': ['德令哈市', '格尔木市', '茫崖市', '乌兰县', '都兰县', '天峻县', '大柴旦行委'] + }, + '宁夏回族自治区': { + '银川市': ['兴庆区', '西夏区', '金凤区', '永宁县', '贺兰县', '灵武市'], + '石嘴山市': ['大武口区', '惠农区', '平罗县'], + '吴忠市': ['利通区', '红寺堡区', '盐池县', '同心县', '青铜峡市'], + '固原市': ['原州区', '西吉县', '隆德县', '泾源县', '彭阳县'], + '中卫市': ['沙坡头区', '中宁县', '海原县'] + }, + '新疆维吾尔自治区': { + '乌鲁木齐市': ['天山区', '沙依巴克区', '新市区', '水磨沟区', '头屯河区', '达坂城区', '米东区', '乌鲁木齐县'], + '克拉玛依市': ['独山子区', '克拉玛依区', '白碱滩区', '乌尔禾区'], + '吐鲁番市': ['高昌区', '鄯善县', '托克逊县'], + '哈密市': ['伊州区', '巴里坤哈萨克自治县', '伊吾县'], + '昌吉回族自治州': ['昌吉市', '阜康市', '呼图壁县', '玛纳斯县', '奇台县', '吉木萨尔县', '木垒哈萨克自治县'], + '博尔塔拉蒙古自治州': ['博乐市', '阿拉山口市', '精河县', '温泉县'], + '巴音郭楞蒙古自治州': ['库尔勒市', '轮台县', '尉犁县', '若羌县', '且末县', '焉耆回族自治县', '和静县', '和硕县', '博湖县'], + '阿克苏地区': ['阿克苏市', '温宿县', '库车市', '沙雅县', '新和县', '拜城县', '乌什县', '阿瓦提县', '柯坪县'], + '克孜勒苏柯尔克孜自治州': ['阿图什市', '阿克陶县', '阿合奇县', '乌恰县'], + '喀什地区': ['喀什市', '疏附县', '疏勒县', '英吉沙县', '泽普县', '莎车县', '叶城县', '麦盖提县', '岳普湖县', '伽师县', '巴楚县', '塔什库尔干塔吉克自治县'], + '和田地区': ['和田市', '和田县', '墨玉县', '皮山县', '洛浦县', '策勒县', '于田县', '民丰县'], + '伊犁哈萨克自治州': ['伊宁市', '奎屯市', '霍尔果斯市', '伊宁县', '察布查尔锡伯自治县', '霍城县', '巩留县', '新源县', '昭苏县', '特克斯县', '尼勒克县'], + '塔城地区': ['塔城市', '乌苏市', '额敏县', '沙湾市', '托里县', '裕民县', '和布克赛尔蒙古自治县'], + '阿勒泰地区': ['阿勒泰市', '布尔津县', '富蕴县', '福海县', '哈巴河县', '青河县', '吉木乃县'], + '石河子市': ['石河子市'], + '阿拉尔市': ['阿拉尔市'], + '图木舒克市': ['图木舒克市'], + '五家渠市': ['五家渠市'], + '北屯市': ['北屯市'], + '铁门关市': ['铁门关市'], + '双河市': ['双河市'], + '可克达拉市': ['可克达拉市'], + '昆玉市': ['昆玉市'], + '胡杨河市': ['胡杨河市'] + }, + '香港特别行政区': { + '香港岛': ['中西区', '湾仔区', '东区', '南区'], + '九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'], + '新界': ['北区', '大埔区', '沙田区', '西贡区', '荃湾区', '屯门区', '元朗区', '葵青区', '离岛区'] + }, + '澳门特别行政区': { + '澳门半岛': ['花地玛堂区', '圣安多尼堂区', '大堂区', '望德堂区', '风顺堂区'], + '氹仔': ['氹仔'], + '路环': ['路环'] + }, + '台湾省': { + '台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'], + '新北市': ['万里区', '金山区', '板桥区', '汐止区', '深坑区', '石碇区', '瑞芳区', '平溪区', '双溪区', '贡寮区', '新店区', '坪林区', '乌来区', '永和区', '中和区', '土城区', '三峡区', '树林区', '莺歌区', '三重区', '新庄区', '泰山区', '林口区', '芦洲区', '五股区', '八里区', '淡水区', '三芝区', '石门区'], + '桃园市': ['中坜区', '平镇区', '龙潭区', '杨梅区', '新屋区', '观音区', '桃园区', '龟山区', '八德区', '大溪区', '复兴区', '大园区', '芦竹区'], + '台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'], + '台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '善化区', '大内区', '山上区', '新市区', '安定区'], + '高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'], + '基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'], + '新竹市': ['东区', '北区', '香山区'], + '嘉义市': ['东区', '西区'], + '新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'], + '苗栗县': ['竹南镇', '头份市', '三湾乡', '南庄乡', '狮潭乡', '后龙镇', '通霄镇', '苑里镇', '苗栗市', '造桥乡', '头屋乡', '公馆乡', '大湖乡', '泰安乡', '铜锣乡', '三义乡', '西湖乡', '卓兰镇'], + '彰化县': ['彰化市', '芬园乡', '花坛乡', '秀水乡', '鹿港镇', '福兴乡', '线西乡', '和美镇', '伸港乡', '员林市', '社头乡', '永靖乡', '埔心乡', '溪湖镇', '大村乡', '埔盐乡', '田中镇', '北斗镇', '田尾乡', '埤头乡', '溪州乡', '竹塘乡', '二林镇', '大城乡', '芳苑乡', '二水乡'], + '南投县': ['南投市', '中寮乡', '草屯镇', '国姓乡', '埔里镇', '仁爱乡', '名间乡', '集集镇', '水里乡', '鱼池乡', '信义乡', '竹山镇', '鹿谷乡'], + '云林县': ['斗南镇', '大埤乡', '虎尾镇', '土库镇', '褒忠乡', '东势乡', '台西乡', '仑背乡', '麦寮乡', '斗六市', '林内乡', '古坑乡', '莿桐乡', '西螺镇', '二仑乡', '北港镇', '水林乡', '口湖乡', '四湖乡', '元长乡'], + '嘉义县': ['番路乡', '梅山乡', '竹崎乡', '阿里山乡', '中埔乡', '大埔乡', '水上乡', '鹿草乡', '太保市', '朴子市', '东石乡', '六脚乡', '新港乡', '民雄乡', '大林镇', '溪口乡', '义竹乡', '布袋镇'], + '屏东县': ['屏东市', '三地门乡', '雾台乡', '玛家乡', '九如乡', '里港乡', '高树乡', '盐埔乡', '长治乡', '麟洛乡', '竹田乡', '内埔乡', '万丹乡', '潮州镇', '泰武乡', '来义乡', '万峦乡', '崁顶乡', '新埤乡', '南州乡', '林边乡', '东港镇', '琉球乡', '佳冬乡', '新园乡', '枋寮乡', '枋山乡', '春日乡', '狮子乡', '车城乡', '牡丹乡', '恒春镇', '满州乡'], + '宜兰县': ['宜兰市', '头城镇', '礁溪乡', '壮围乡', '员山乡', '罗东镇', '三星乡', '大同乡', '五结乡', '冬山乡', '苏澳镇', '南澳乡'], + '花莲县': ['花莲市', '新城乡', '秀林乡', '吉安乡', '寿丰乡', '凤林镇', '光复乡', '丰滨乡', '瑞穗乡', '万荣乡', '玉里镇', '卓溪乡', '富里乡'], + '台东县': ['台东市', '绿岛乡', '兰屿乡', '延平乡', '卑南乡', '鹿野乡', '关山镇', '海端乡', '池上乡', '东河乡', '成功镇', '长滨乡', '太麻里乡', '金峰乡', '大武乡', '达仁乡'], + '澎湖县': ['马公市', '西屿乡', '望安乡', '七美乡', '白沙乡', '湖西乡'], + '金门县': ['金沙镇', '金湖镇', '金宁乡', '金城镇', '烈屿乡', '乌坵乡'], + '连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡'] + } +}; diff --git a/app/static/js/order_detail.js b/app/static/js/order_detail.js new file mode 100644 index 0000000..a4e161b --- /dev/null +++ b/app/static/js/order_detail.js @@ -0,0 +1,49 @@ +// 订单详情页面脚本 + +// 取消订单 +function cancelOrder(orderId) { + if (confirm('确定要取消这个订单吗?取消后无法恢复。')) { + fetch(`/order/cancel/${orderId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('操作失败,请重试', 'error'); + }); + } +} + +// 确认收货 +function confirmReceipt(orderId) { + if (confirm('确定已收到商品吗?确认后订单将完成。')) { + fetch(`/order/confirm_receipt/${orderId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('操作失败,请重试', 'error'); + }); + } +} diff --git a/app/static/js/orders.js b/app/static/js/orders.js new file mode 100644 index 0000000..0dd552d --- /dev/null +++ b/app/static/js/orders.js @@ -0,0 +1,109 @@ +// 订单页面JavaScript功能 + +// 强制设置商品图片样式的函数 +function forceProductImageStyle(imgElement) { + if (!imgElement) return; + + // 强制设置所有样式属性 + imgElement.style.width = '80px'; + imgElement.style.height = '80px'; + imgElement.style.objectFit = 'cover'; + imgElement.style.borderRadius = '4px'; + imgElement.style.display = 'block'; + imgElement.style.maxWidth = '80px'; + imgElement.style.maxHeight = '80px'; + imgElement.style.minWidth = '80px'; + imgElement.style.minHeight = '80px'; + + // 设置属性避免被覆盖 + imgElement.setAttribute('width', '80'); + imgElement.setAttribute('height', '80'); +} + +// 页面加载完成后的处理 +document.addEventListener('DOMContentLoaded', function() { + // 强制设置所有商品图片样式 + const productImages = document.querySelectorAll('.product-image'); + productImages.forEach(function(img) { + forceProductImageStyle(img); + + // 图片加载完成后再次强制设置 + if (img.complete) { + forceProductImageStyle(img); + } else { + img.onload = function() { + forceProductImageStyle(img); + }; + } + }); + + // 为订单卡片添加悬停效果 + const orderCards = document.querySelectorAll('.order-card'); + orderCards.forEach(card => { + card.addEventListener('mouseenter', function() { + this.style.boxShadow = '0 4px 15px rgba(0,0,0,0.1)'; + }); + + card.addEventListener('mouseleave', function() { + this.style.boxShadow = ''; + }); + }); +}); + +// 额外的保险措施:定期检查并修正商品图片样式 +setInterval(function() { + const productImages = document.querySelectorAll('.product-image'); + productImages.forEach(function(img) { + // 检查图片是否超出预期尺寸 + const rect = img.getBoundingClientRect(); + if (rect.width > 85 || rect.height > 85) { + forceProductImageStyle(img); + } + }); +}, 1000); // 每秒检查一次 + +function cancelOrder(orderId) { + if (confirm('确定要取消这个订单吗?取消后无法恢复。')) { + fetch(`/order/cancel/${orderId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('操作失败,请重试', 'error'); + }); + } +} + +function confirmReceipt(orderId) { + if (confirm('确定已收到商品吗?确认后订单将完成。')) { + fetch(`/order/confirm_receipt/${orderId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert(data.message, 'success'); + setTimeout(() => location.reload(), 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('操作失败,请重试', 'error'); + }); + } +} diff --git a/app/static/js/pay.js b/app/static/js/pay.js new file mode 100644 index 0000000..97cc7cb --- /dev/null +++ b/app/static/js/pay.js @@ -0,0 +1,200 @@ +// 订单支付页面脚本 +let countdownTimer; +let statusCheckTimer; +let timeLeft = 15 * 60; // 15分钟 + +// 页面加载时开始倒计时 +document.addEventListener('DOMContentLoaded', function() { + startCountdown(); +}); + +// 页面卸载时清理定时器 +window.addEventListener('beforeunload', function() { + if (countdownTimer) clearInterval(countdownTimer); + if (statusCheckTimer) clearInterval(statusCheckTimer); +}); + +// 开始倒计时 +function startCountdown() { + countdownTimer = setInterval(() => { + timeLeft--; + + const minutes = Math.floor(timeLeft / 60); + const seconds = timeLeft % 60; + + document.getElementById('countdown').textContent = + `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + if (timeLeft <= 0) { + clearInterval(countdownTimer); + showAlert('订单已过期,请重新下单', 'warning'); + setTimeout(() => { + window.location.href = '/order/list'; + }, 2000); + } + }, 1000); +} + +// 开始支付 +function startPayment() { + const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; + const paymentMethod = document.querySelector('[data-payment-method]')?.dataset.paymentMethod; + + if (!paymentSn || !paymentMethod) { + showAlert('支付信息获取失败', 'error'); + return; + } + + fetch('/payment/process', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + payment_sn: paymentSn, + payment_method: paymentMethod + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.payment_type === 'qrcode') { + showQRCode(data.qr_code_url); + startStatusCheck(); + } else if (data.payment_type === 'redirect') { + window.open(data.pay_url, '_blank'); + startStatusCheck(); + } + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('支付启动失败,请重试', 'error'); + }); +} + +// 显示二维码 +function showQRCode(qrUrl) { + const qrArea = document.getElementById('qrCodeArea'); + const qrImage = document.getElementById('qrCodeImage'); + + // 这里应该使用真实的二维码生成库,现在用文本模拟 + qrImage.innerHTML = ` +
+
+
+ 微信支付二维码 +
+
+ `; + + qrArea.style.display = 'block'; +} + +// 开始检查支付状态 +function startStatusCheck() { + statusCheckTimer = setInterval(() => { + checkPaymentStatus(); + }, 3000); // 每3秒检查一次 +} + +// 检查支付状态 +function checkPaymentStatus() { + const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; + + if (!paymentSn) return; + + fetch(`/payment/check_status/${paymentSn}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.status === 2) { // 支付成功 + clearInterval(statusCheckTimer); + clearInterval(countdownTimer); + showPaymentSuccess(); + } + } + }) + .catch(error => { + console.error('状态检查失败:', error); + }); +} + +// 显示支付成功 +function showPaymentSuccess() { + document.getElementById('paymentArea').style.display = 'none'; + document.getElementById('paymentStatus').style.display = 'block'; + + const orderId = document.querySelector('[data-order-id]')?.dataset.orderId; + setTimeout(() => { + window.location.href = `/order/detail/${orderId}`; + }, 2000); +} + +// 取消订单 +function cancelOrder() { + const orderId = document.querySelector('[data-order-id]')?.dataset.orderId; + + if (!orderId) { + showAlert('订单信息获取失败', 'error'); + return; + } + + if (confirm('确定要取消这个订单吗?')) { + fetch(`/order/cancel/${orderId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert('订单已取消', 'success'); + setTimeout(() => { + window.location.href = '/order/list'; + }, 1500); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('取消失败,请重试', 'error'); + }); + } +} + +// 模拟支付成功(开发测试用) +function simulatePayment() { + const paymentSn = document.querySelector('[data-payment-sn]')?.dataset.paymentSn; + + if (!paymentSn) { + showAlert('支付信息获取失败', 'error'); + return; + } + + if (confirm('这是测试功能,确定要模拟支付成功吗?')) { + fetch(`/payment/simulate_success/${paymentSn}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showAlert('模拟支付成功', 'success'); + setTimeout(() => { + showPaymentSuccess(); + }, 1000); + } else { + showAlert(data.message, 'error'); + } + }) + .catch(error => { + showAlert('模拟支付失败', 'error'); + }); + } +} diff --git a/app/static/js/product_detail.js b/app/static/js/product_detail.js new file mode 100644 index 0000000..a94a72e --- /dev/null +++ b/app/static/js/product_detail.js @@ -0,0 +1,236 @@ +// 获取库存数据 +const inventoryData = JSON.parse(document.getElementById('inventoryData').textContent); +let selectedSpecs = {}; +let currentSku = null; + +// 初始化 +document.addEventListener('DOMContentLoaded', function() { + // 如果只有一个SKU,自动选择 + if (inventoryData.length === 1) { + currentSku = inventoryData[0]; + updateStockInfo(); + } + + // 绑定规格选择事件 + document.querySelectorAll('.spec-option').forEach(button => { + button.addEventListener('click', function() { + selectSpec(this); + }); + }); + + // 初始化购物车数量显示 + if (typeof loadCartCount === 'function') { + loadCartCount(); + } +}); + +// 规格选择 +function selectSpec(button) { + const specName = button.getAttribute('data-spec-name'); + const specValue = button.getAttribute('data-spec-value'); + + // 清除同组其他选择 + document.querySelectorAll(`[data-spec-name="${specName}"]`).forEach(btn => { + btn.classList.remove('btn-primary'); + btn.classList.add('btn-outline-secondary'); + }); + + // 选中当前项 + button.classList.remove('btn-outline-secondary'); + button.classList.add('btn-primary'); + + // 更新选择状态 + selectedSpecs[specName] = specValue; + + // 查找匹配的SKU + findMatchingSku(); +} + +// 查找匹配的SKU +function findMatchingSku() { + for (let sku of inventoryData) { + if (sku.spec_combination) { + let isMatch = true; + for (let [specName, specValue] of Object.entries(selectedSpecs)) { + if (sku.spec_combination[specName] !== specValue) { + isMatch = false; + break; + } + } + if (isMatch && Object.keys(selectedSpecs).length === Object.keys(sku.spec_combination).length) { + currentSku = sku; + updateStockInfo(); + return; + } + } + } + + // 未找到完全匹配的SKU + currentSku = null; + updateStockInfo(); +} + +// 更新库存信息 +function updateStockInfo() { + const stockElement = document.getElementById('stockCount'); + const priceElement = document.getElementById('currentPrice'); + const addToCartBtn = document.getElementById('addToCartBtn'); + const buyNowBtn = document.getElementById('buyNowBtn'); + const quantityInput = document.getElementById('quantity'); + + if (currentSku) { + stockElement.textContent = currentSku.stock; + stockElement.className = currentSku.stock > 0 ? 'text-success' : 'text-danger'; + priceElement.textContent = currentSku.final_price.toFixed(2); + + if (currentSku.stock > 0) { + addToCartBtn.disabled = false; + buyNowBtn.disabled = false; + quantityInput.max = currentSku.stock; + } else { + addToCartBtn.disabled = true; + buyNowBtn.disabled = true; + quantityInput.max = 0; + } + } else if (inventoryData.length > 1) { + stockElement.textContent = '请选择规格'; + stockElement.className = 'text-warning'; + addToCartBtn.disabled = true; + buyNowBtn.disabled = true; + } +} + +// 数量变更 +function changeQuantity(delta) { + const quantityInput = document.getElementById('quantity'); + let quantity = parseInt(quantityInput.value) + delta; + + const min = parseInt(quantityInput.min) || 1; + const max = parseInt(quantityInput.max) || 999; + + quantity = Math.max(min, Math.min(max, quantity)); + quantityInput.value = quantity; +} + +// 轮播图跳转 +function goToSlide(index) { + const carousel = new bootstrap.Carousel(document.getElementById('productImageCarousel')); + carousel.to(index); +} + +// 加载购物车数量 +function loadCartCount() { + fetch('/cart/count') + .then(response => response.json()) + .then(data => { + updateCartBadge(data.cart_count); + }) + .catch(error => { + console.error('Error loading cart count:', error); + }); +} + +// 加入购物车 +function addToCart() { + if (!currentSku) { + alert('请选择商品规格'); + return; + } + + const quantity = parseInt(document.getElementById('quantity').value); + if (quantity <= 0 || quantity > currentSku.stock) { + alert('请选择正确的购买数量'); + return; + } + + // 禁用按钮,防止重复点击 + const addToCartBtn = document.getElementById('addToCartBtn'); + addToCartBtn.disabled = true; + addToCartBtn.innerHTML = ' 添加中...'; + + // 提交到购物车 + fetch('/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + product_id: window.productId, + sku_code: currentSku.sku_code, + spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '', + quantity: quantity + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showSuccessMessage(data.message); + updateCartBadge(data.cart_count); + + // 询问是否查看购物车 + setTimeout(() => { + if (confirm('商品已添加到购物车,是否查看购物车?')) { + window.location.href = '/cart/'; + } + }, 500); + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('加入购物车失败,请稍后再试'); + }) + .finally(() => { + // 恢复按钮状态 + addToCartBtn.disabled = false; + addToCartBtn.innerHTML = ' 加入购物车'; + }); +} + +// 立即购买 +function buyNow() { + if (!currentSku) { + alert('请选择商品规格'); + return; + } + + const quantity = parseInt(document.getElementById('quantity').value); + if (quantity <= 0 || quantity > currentSku.stock) { + alert('请选择正确的购买数量'); + return; + } + + // 先添加到购物车,然后跳转到结算页面 + fetch('/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + product_id: window.productId, + sku_code: currentSku.sku_code, + spec_combination: Object.keys(selectedSpecs).length > 0 ? JSON.stringify(selectedSpecs) : '', + quantity: quantity + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 立即跳转到购物车结算 + window.location.href = '/cart/'; + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('购买失败,请稍后再试'); + }); +} + +// 收藏商品 +function addToFavorites() { + // TODO: 实现收藏功能 + alert('收藏功能开发中...'); +} diff --git a/app/static/js/product_list.js b/app/static/js/product_list.js new file mode 100644 index 0000000..ea3227a --- /dev/null +++ b/app/static/js/product_list.js @@ -0,0 +1,6 @@ +function changeSort(sortType) { + const url = new URL(window.location); + url.searchParams.set('sort', sortType); + url.searchParams.set('page', '1'); // 重置到第一页 + window.location.href = url.toString(); +} diff --git a/app/static/js/profile.js b/app/static/js/profile.js new file mode 100644 index 0000000..1a1b443 --- /dev/null +++ b/app/static/js/profile.js @@ -0,0 +1,343 @@ +// 个人中心页面JavaScript功能 +let selectedFile = null; + +// 强制设置头像样式的函数 +function forceAvatarStyle(imgElement) { + if (!imgElement) return; + + // 强制设置所有样式属性 + imgElement.style.width = '120px'; + imgElement.style.height = '120px'; + imgElement.style.borderRadius = '50%'; + imgElement.style.border = '3px solid #ddd'; + imgElement.style.objectFit = 'cover'; + imgElement.style.cursor = 'pointer'; + imgElement.style.transition = 'all 0.3s ease'; + imgElement.style.display = 'block'; + imgElement.style.maxWidth = '120px'; + imgElement.style.maxHeight = '120px'; + imgElement.style.minWidth = '120px'; + imgElement.style.minHeight = '120px'; + + // 设置属性避免被覆盖 + imgElement.setAttribute('width', '120'); + imgElement.setAttribute('height', '120'); +} + +// 页面加载完成后的处理 +document.addEventListener('DOMContentLoaded', function() { + // 隐藏进度条 + const progressContainer = document.getElementById('uploadProgress'); + if (progressContainer) { + progressContainer.classList.remove('show'); + progressContainer.style.display = 'none'; + } + + // *** 关键:强制设置已存在的头像样式 *** + const existingAvatar = document.getElementById('avatarPreview'); + if (existingAvatar) { + forceAvatarStyle(existingAvatar); + + // 图片加载完成后再次强制设置 + if (existingAvatar.complete) { + forceAvatarStyle(existingAvatar); + } else { + existingAvatar.onload = function() { + forceAvatarStyle(existingAvatar); + }; + } + } + + // 添加拖拽上传支持 + initDragAndDrop(); +}); + +// 触发文件选择 +function triggerFileInput() { + document.getElementById('avatarInput').click(); +} + +// 处理文件选择 +function handleFileSelect(event) { + const file = event.target.files[0]; + if (!file) return; + + // 验证文件类型 + if (!file.type.match('image.*')) { + showAlert('请选择图片文件!', 'error'); + return; + } + + // 验证文件大小 (2MB) + if (file.size > 2 * 1024 * 1024) { + showAlert('图片大小不能超过 2MB!', 'error'); + return; + } + + selectedFile = file; + + // 预览图片 + const reader = new FileReader(); + reader.onload = function(e) { + const previewImage = document.getElementById('previewImage'); + previewImage.src = e.target.result; + + // 更新文件信息 + updateFileInfo(file); + + // 确保图片加载完成后再显示模态框 + previewImage.onload = function() { + // *** 强制设置预览图片样式 *** + previewImage.style.maxWidth = '280px'; + previewImage.style.maxHeight = '280px'; + previewImage.style.width = 'auto'; + previewImage.style.height = 'auto'; + previewImage.style.objectFit = 'contain'; + previewImage.style.borderRadius = '12px'; + previewImage.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)'; + previewImage.style.border = '3px solid #fff'; + + // 更新图片尺寸信息 + document.getElementById('imageWidth').textContent = previewImage.naturalWidth; + document.getElementById('imageHeight').textContent = previewImage.naturalHeight; + + const modal = new bootstrap.Modal(document.getElementById('imagePreviewModal')); + modal.show(); + }; + }; + reader.readAsDataURL(file); +} + +// 更新文件信息 +function updateFileInfo(file) { + // 文件大小 + const sizeInMB = (file.size / 1024 / 1024).toFixed(2); + document.getElementById('imageSize').textContent = sizeInMB + ' MB'; + + // 文件类型 + const fileType = file.type.split('/')[1].toUpperCase(); + document.getElementById('imageType').textContent = fileType; +} + +// 确认上传 +function confirmUpload() { + if (!selectedFile) return; + + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('imagePreviewModal')); + modal.hide(); + + // 开始上传 + uploadAvatar(selectedFile); +} + +// 上传头像 +function uploadAvatar(file) { + const formData = new FormData(); + formData.append('avatar', file); + + // 显示上传进度 + const progressContainer = document.getElementById('uploadProgress'); + const progressBar = progressContainer.querySelector('.progress-bar'); + + progressContainer.style.display = 'block'; + progressContainer.classList.add('show'); + progressBar.style.width = '0%'; + progressBar.textContent = '0%'; + + // 创建XMLHttpRequest以支持进度显示 + const xhr = new XMLHttpRequest(); + + // 上传进度 + xhr.upload.addEventListener('progress', function(e) { + if (e.lengthComputable) { + const percentComplete = (e.loaded / e.total) * 100; + progressBar.style.width = percentComplete + '%'; + progressBar.textContent = Math.round(percentComplete) + '%'; + } + }); + + // 上传完成 + xhr.addEventListener('load', function() { + // 隐藏进度条 + progressContainer.style.display = 'none'; + progressContainer.classList.remove('show'); + + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + if (response.success) { + // 更新头像显示 + updateAvatarDisplay(response.avatar_url); + showAlert('头像上传成功!', 'success'); + } else { + showAlert(response.message || '上传失败', 'error'); + } + } catch (e) { + showAlert('服务器响应错误', 'error'); + } + } else { + showAlert('上传失败,请重试', 'error'); + } + + // 清理文件输入 + document.getElementById('avatarInput').value = ''; + selectedFile = null; + }); + + // 上传错误 + xhr.addEventListener('error', function() { + progressContainer.style.display = 'none'; + progressContainer.classList.remove('show'); + showAlert('网络错误,请重试', 'error'); + + // 清理文件输入 + document.getElementById('avatarInput').value = ''; + selectedFile = null; + }); + + // 发送请求 + xhr.open('POST', '/upload/avatar'); + xhr.send(formData); +} + +// *** 关键:更新头像显示函数 *** +function updateAvatarDisplay(avatarUrl) { + const avatarPreview = document.getElementById('avatarPreview'); + const avatarPlaceholder = document.getElementById('avatarPlaceholder'); + + if (avatarPreview) { + // 更新现有头像 + avatarPreview.src = avatarUrl + '?t=' + new Date().getTime(); + + // *** 强制设置头像样式 *** + avatarPreview.onload = function() { + forceAvatarStyle(avatarPreview); + + // 延迟再次确保样式生效 + setTimeout(function() { + forceAvatarStyle(avatarPreview); + }, 100); + }; + + } else if (avatarPlaceholder) { + // 替换占位符为头像 + const avatarUpload = avatarPlaceholder.parentElement; + avatarPlaceholder.remove(); + + const img = document.createElement('img'); + img.src = avatarUrl + '?t=' + new Date().getTime(); + img.alt = '头像'; + img.className = 'avatar-preview'; + img.id = 'avatarPreview'; + + // *** 创建新头像时强制设置样式 *** + img.onload = function() { + forceAvatarStyle(img); + + // 延迟再次确保样式生效 + setTimeout(function() { + forceAvatarStyle(img); + }, 100); + }; + + avatarUpload.insertBefore(img, avatarUpload.firstChild); + } +} + +// 显示提示信息 +function showAlert(message, type = 'info') { + // 移除现有的提示框 + const existingAlerts = document.querySelectorAll('.alert.position-fixed'); + existingAlerts.forEach(alert => alert.remove()); + + // 创建提示框 + const alertDiv = document.createElement('div'); + alertDiv.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`; + alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px; max-width: 400px;'; + + const icon = type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-triangle' : 'info-circle'; + + alertDiv.innerHTML = ` + + ${message} + + `; + + document.body.appendChild(alertDiv); + + // 3秒后自动消失 + setTimeout(() => { + if (alertDiv.parentElement) { + alertDiv.classList.remove('show'); + setTimeout(() => { + if (alertDiv.parentElement) { + alertDiv.remove(); + } + }, 150); + } + }, 3000); +} + +// 初始化拖拽上传功能 +function initDragAndDrop() { + const avatarUpload = document.querySelector('.avatar-upload'); + + if (avatarUpload) { + // 防止默认拖拽行为 + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + avatarUpload.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + // 拖拽进入和离开的视觉反馈 + ['dragenter', 'dragover'].forEach(eventName => { + avatarUpload.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + avatarUpload.addEventListener(eventName, unhighlight, false); + }); + + function highlight(e) { + avatarUpload.style.transform = 'scale(1.05)'; + avatarUpload.style.boxShadow = '0 0 20px rgba(0,123,255,0.5)'; + } + + function unhighlight(e) { + avatarUpload.style.transform = ''; + avatarUpload.style.boxShadow = ''; + } + + // 处理文件拖拽 + avatarUpload.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + + if (files.length > 0) { + const file = files[0]; + // 模拟文件输入事件 + const event = { target: { files: [file] } }; + handleFileSelect(event); + } + } + } +} + +// 额外的保险措施:定期检查并修正头像样式 +setInterval(function() { + const avatar = document.getElementById('avatarPreview'); + if (avatar) { + // 检查头像是否超出预期尺寸 + const rect = avatar.getBoundingClientRect(); + if (rect.width > 125 || rect.height > 125) { + forceAvatarStyle(avatar); + } + } +}, 1000); // 每秒检查一次 diff --git a/app/static/js/register.js b/app/static/js/register.js new file mode 100644 index 0000000..651c55d --- /dev/null +++ b/app/static/js/register.js @@ -0,0 +1,116 @@ +// 注册页面JavaScript功能 +document.addEventListener('DOMContentLoaded', function() { + const sendBtn = document.getElementById('sendEmailCodeBtn'); + const emailInput = document.getElementById('emailInput'); + const btnText = document.getElementById('btnText'); + const passwordInput = document.getElementById('passwordInput'); + const confirmPasswordInput = document.getElementById('confirmPasswordInput'); + const passwordMatchMessage = document.getElementById('passwordMatchMessage'); + let countdown = 0; + let timer = null; + + // 密码确认实时验证 + function checkPasswordMatch() { + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (confirmPassword === '') { + passwordMatchMessage.textContent = ''; + passwordMatchMessage.className = 'form-text'; + return; + } + + if (password === confirmPassword) { + passwordMatchMessage.textContent = '✓ 密码匹配'; + passwordMatchMessage.className = 'form-text text-success'; + confirmPasswordInput.classList.remove('is-invalid'); + confirmPasswordInput.classList.add('is-valid'); + } else { + passwordMatchMessage.textContent = '✗ 密码不匹配'; + passwordMatchMessage.className = 'form-text text-danger'; + confirmPasswordInput.classList.remove('is-valid'); + confirmPasswordInput.classList.add('is-invalid'); + } + } + + // 监听密码输入 + passwordInput.addEventListener('input', checkPasswordMatch); + confirmPasswordInput.addEventListener('input', checkPasswordMatch); + + // 发送验证码 + sendBtn.addEventListener('click', function() { + const email = emailInput.value.trim(); + + // 简单的邮箱格式验证 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!email) { + alert('请输入邮箱地址'); + emailInput.focus(); + return; + } + + if (!emailRegex.test(email)) { + alert('请输入有效的邮箱地址'); + emailInput.focus(); + return; + } + + // 发送AJAX请求 + fetch('/auth/send_email_code', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value + }, + body: JSON.stringify({ + email: email, + type: 1 // 1表示注册 + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('验证码已发送到您的邮箱,请查收!'); + startCountdown(); + } else { + alert(data.message || '发送失败,请重试'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('发送失败,请检查网络连接'); + }); + }); + + // 倒计时功能 + function startCountdown() { + countdown = 60; + sendBtn.disabled = true; + sendBtn.classList.add('disabled'); + + timer = setInterval(function() { + btnText.textContent = `${countdown}秒后重发`; + countdown--; + + if (countdown < 0) { + clearInterval(timer); + sendBtn.disabled = false; + sendBtn.classList.remove('disabled'); + btnText.textContent = '发送验证码'; + } + }, 1000); + } + + // 表单提交前验证 + document.getElementById('registerForm').addEventListener('submit', function(e) { + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (password !== confirmPassword) { + e.preventDefault(); + alert('两次输入的密码不一致,请重新输入'); + confirmPasswordInput.focus(); + return false; + } + }); +}); diff --git a/app/static/uploads/.gitkeep b/app/static/uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html new file mode 100644 index 0000000..2e75765 --- /dev/null +++ b/app/templates/admin/base.html @@ -0,0 +1,123 @@ + + + + + + {% 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 %} + + diff --git a/app/templates/admin/categories.html b/app/templates/admin/categories.html new file mode 100644 index 0000000..4a0f0eb --- /dev/null +++ b/app/templates/admin/categories.html @@ -0,0 +1,326 @@ +{% 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 %} diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 0000000..01db375 --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,207 @@ +{% extends "admin/base.html" %} + +{% block title %}仪表板 - 太白购物商城管理后台{% endblock %} + +{% block page_title %}仪表板{% endblock %} +{% block page_description %}系统概览和数据统计{% endblock %} + +{% block extra_css %} + +{% 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 %} diff --git a/app/templates/admin/login.html b/app/templates/admin/login.html new file mode 100644 index 0000000..93a87ca --- /dev/null +++ b/app/templates/admin/login.html @@ -0,0 +1,69 @@ + + + + + + 管理员登录 - 太白购物商城 + + + + + + + + + + + + + + + diff --git a/app/templates/admin/orders.html b/app/templates/admin/orders.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/admin/product_form.html b/app/templates/admin/product_form.html new file mode 100644 index 0000000..95e7d13 --- /dev/null +++ b/app/templates/admin/product_form.html @@ -0,0 +1,859 @@ +{% 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 %} diff --git a/app/templates/admin/products.html b/app/templates/admin/products.html new file mode 100644 index 0000000..279768f --- /dev/null +++ b/app/templates/admin/products.html @@ -0,0 +1,441 @@ +{% 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 %} diff --git a/app/templates/admin/profile.html b/app/templates/admin/profile.html new file mode 100644 index 0000000..b7d00e4 --- /dev/null +++ b/app/templates/admin/profile.html @@ -0,0 +1,138 @@ +{% extends "admin/base.html" %} + +{% block title %}个人资料 - 太白购物商城管理后台{% endblock %} + +{% block page_title %}个人资料{% endblock %} +{% block page_description %}管理员个人信息设置{% endblock %} + +{% block extra_css %} + +{% 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 %} diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..ef39781 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,161 @@ + + + + + + {% block title %}太白购物商城{% endblock %} + + + + {% block styles %}{% 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 %} + + diff --git a/app/templates/cart/index.html b/app/templates/cart/index.html new file mode 100644 index 0000000..8a5f23a --- /dev/null +++ b/app/templates/cart/index.html @@ -0,0 +1,192 @@ +{% extends "base.html" %} + +{% block title %}购物车 - 太白购物商城{% endblock %} + +{% block head %} + +{% 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 %} diff --git a/app/templates/common/footer.html b/app/templates/common/footer.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/common/header.html b/app/templates/common/header.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/common/pagination.html b/app/templates/common/pagination.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..d1943b2 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,227 @@ +{% extends "base.html" %} + +{% block title %}首页 - 太白购物商城{% endblock %} + +{% block styles %} + +{% 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 %} diff --git a/app/templates/order/checkout.html b/app/templates/order/checkout.html new file mode 100644 index 0000000..c580617 --- /dev/null +++ b/app/templates/order/checkout.html @@ -0,0 +1,224 @@ +{% 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 %} diff --git a/app/templates/order/detail.html b/app/templates/order/detail.html new file mode 100644 index 0000000..1de4f1a --- /dev/null +++ b/app/templates/order/detail.html @@ -0,0 +1,253 @@ +{% 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 %} diff --git a/app/templates/order/pay.html b/app/templates/order/pay.html new file mode 100644 index 0000000..de02cea --- /dev/null +++ b/app/templates/order/pay.html @@ -0,0 +1,122 @@ +{% 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 %} diff --git a/app/templates/product/detail.html b/app/templates/product/detail.html new file mode 100644 index 0000000..499f3e9 --- /dev/null +++ b/app/templates/product/detail.html @@ -0,0 +1,374 @@ +{% extends "base.html" %} + +{% block title %}{{ product.name }} - 太白购物商城{% endblock %} + +{% block head %} + +{% 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 %} diff --git a/app/templates/product/list.html b/app/templates/product/list.html new file mode 100644 index 0000000..193d050 --- /dev/null +++ b/app/templates/product/list.html @@ -0,0 +1,280 @@ +{% extends "base.html" %} + +{% block title %} +{% if current_category %}{{ current_category.name }} - {% endif %} +{% if search %}搜索"{{ search }}" - {% endif %} +商品列表 - 太白购物商城 +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ +
+ +
+
+
商品搜索
+
+
+
+
+ + +
+ + {% if category_id %}{% endif %} + {% if sort %}{% endif %} +
+
+
+ + +
+
+
商品分类
+
+
+ +
+
+ + +
+
+
价格筛选
+
+
+
+
+
+ +
+
+ +
+
+
+ + 重置 +
+ + {% if search %}{% endif %} + {% if category_id %}{% endif %} + {% if sort %}{% endif %} +
+ + +
+
+ {% set filtered_args = request.args.copy() %} + {% set _ = filtered_args.pop('min_price', None) %} + {% set _ = filtered_args.pop('max_price', None) %} + + 100元以下 + 100-500元 + 500-1000元 + 1000元以上 +
+
+
+
+
+ + +
+ + + + +
+
+
+ {% 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 %} diff --git a/app/templates/test_upload.html b/app/templates/test_upload.html new file mode 100644 index 0000000..379a955 --- /dev/null +++ b/app/templates/test_upload.html @@ -0,0 +1,378 @@ + + + + + + COS上传测试 + + + +
+
+
+
+
+

腾讯云COS上传测试

+
+
+ +
+
头像上传测试
+
+
+ +
+ +
+
+
+ +
+ + +
+
通用图片上传测试
+
+
+ +
+
+ +
+ +
+
+
+ + +
+
上传历史
+
+
+
+
+
+
+
+ + + + + + + + + + + COS上传测试 + + + +
+
+
+
+
+

腾讯云COS上传测试

+
+
+ +
+
头像上传测试
+
+
+ +
+ +
+
+
+ +
+ + +
+
通用图片上传测试
+
+
+ +
+
+ +
+ +
+
+
+ + +
+
上传历史
+
+
+
+
+
+
+
+ + + + + diff --git a/app/templates/user/address_form.html b/app/templates/user/address_form.html new file mode 100644 index 0000000..d41a43b --- /dev/null +++ b/app/templates/user/address_form.html @@ -0,0 +1,150 @@ +{% extends "base.html" %} +{% block title %}{% if action == 'add' %}添加地址{% else %}编辑地址{% endif %} - 太白购物商城{% endblock %} + +{% block head %} + +{% 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 %} diff --git a/app/templates/user/addresses.html b/app/templates/user/addresses.html new file mode 100644 index 0000000..1d43eac --- /dev/null +++ b/app/templates/user/addresses.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} +{% block title %}收货地址 - 太白购物商城{% endblock %} + +{% block head %} + +{% 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 %} diff --git a/app/templates/user/login.html b/app/templates/user/login.html new file mode 100644 index 0000000..5a84caa --- /dev/null +++ b/app/templates/user/login.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block title %}用户登录 - 太白购物商城{% endblock %} + +{% block styles %} + +{% 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 %} diff --git a/app/templates/user/orders.html b/app/templates/user/orders.html new file mode 100644 index 0000000..dc7c768 --- /dev/null +++ b/app/templates/user/orders.html @@ -0,0 +1,230 @@ +{% 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 %} + 暂无该状态订单 + {% else %} + 暂无订单 + {% endif %} +
+

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

+ + 去购物 + +
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/user/profile.html b/app/templates/user/profile.html new file mode 100644 index 0000000..79c7cc4 --- /dev/null +++ b/app/templates/user/profile.html @@ -0,0 +1,234 @@ +{% 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 %} diff --git a/app/templates/user/register.html b/app/templates/user/register.html new file mode 100644 index 0000000..4f698d9 --- /dev/null +++ b/app/templates/user/register.html @@ -0,0 +1,117 @@ +{% extends "base.html" %} + +{% block title %}用户注册 - 太白购物商城{% endblock %} + +{% block head %} + +{% 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 %} + +{% block scripts %} + +{% endblock %} diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/auth.py b/app/utils/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/cos_client.py b/app/utils/cos_client.py new file mode 100644 index 0000000..670332a --- /dev/null +++ b/app/utils/cos_client.py @@ -0,0 +1,274 @@ +""" +腾讯云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() diff --git a/app/utils/cos_upload.py b/app/utils/cos_upload.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/database.py b/app/utils/database.py new file mode 100644 index 0000000..13b414b --- /dev/null +++ b/app/utils/database.py @@ -0,0 +1,36 @@ +""" +数据库工具模块 +""" +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)}" diff --git a/app/utils/decorators.py b/app/utils/decorators.py new file mode 100644 index 0000000..83c77c5 --- /dev/null +++ b/app/utils/decorators.py @@ -0,0 +1,312 @@ +""" +装饰器工具模块 +提供登录验证、权限控制等装饰器功能 +""" +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 + diff --git a/app/utils/email_service.py b/app/utils/email_service.py new file mode 100644 index 0000000..231423a --- /dev/null +++ b/app/utils/email_service.py @@ -0,0 +1,75 @@ +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) diff --git a/app/utils/file_upload.py b/app/utils/file_upload.py new file mode 100644 index 0000000..e926c02 --- /dev/null +++ b/app/utils/file_upload.py @@ -0,0 +1,428 @@ +""" +文件上传处理工具 +""" +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() diff --git a/app/utils/helpers.py b/app/utils/helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/sms.py b/app/utils/sms.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/wechat_pay.py b/app/utils/wechat_pay.py new file mode 100644 index 0000000..e69de29 diff --git a/app/views/__init__.py b/app/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/views/address.py b/app/views/address.py new file mode 100644 index 0000000..ea1f971 --- /dev/null +++ b/app/views/address.py @@ -0,0 +1,220 @@ +""" +地址管理视图 +""" +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] + }) diff --git a/app/views/admin.py b/app/views/admin.py new file mode 100644 index 0000000..cdb5016 --- /dev/null +++ b/app/views/admin.py @@ -0,0 +1,249 @@ +""" +管理员视图 +""" +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) diff --git a/app/views/auth.py b/app/views/auth.py new file mode 100644 index 0000000..bd5c12f --- /dev/null +++ b/app/views/auth.py @@ -0,0 +1,141 @@ +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')) diff --git a/app/views/cart.py b/app/views/cart.py new file mode 100644 index 0000000..7d0b69d --- /dev/null +++ b/app/views/cart.py @@ -0,0 +1,245 @@ +""" +购物车视图 +""" +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) + diff --git a/app/views/main.py b/app/views/main.py new file mode 100644 index 0000000..3709d38 --- /dev/null +++ b/app/views/main.py @@ -0,0 +1,186 @@ +""" +主页面视图 +""" +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') diff --git a/app/views/order.py b/app/views/order.py new file mode 100644 index 0000000..2e69c2f --- /dev/null +++ b/app/views/order.py @@ -0,0 +1,340 @@ +""" +订单视图 +""" +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) diff --git a/app/views/payment.py b/app/views/payment.py new file mode 100644 index 0000000..07c340b --- /dev/null +++ b/app/views/payment.py @@ -0,0 +1,212 @@ +""" +支付视图 +""" +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)}'}) diff --git a/app/views/product.py b/app/views/product.py new file mode 100644 index 0000000..e768e95 --- /dev/null +++ b/app/views/product.py @@ -0,0 +1,667 @@ +""" +商品管理视图 +""" +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)}) diff --git a/app/views/upload.py b/app/views/upload.py new file mode 100644 index 0000000..4987a30 --- /dev/null +++ b/app/views/upload.py @@ -0,0 +1,185 @@ +""" +文件上传视图 +""" +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"]}' diff --git a/app/views/user.py b/app/views/user.py new file mode 100644 index 0000000..09fab95 --- /dev/null +++ b/app/views/user.py @@ -0,0 +1,37 @@ +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(): + """用户订单 - 重定向到订单列表""" + return redirect(url_for('order.list')) diff --git a/check_avatar.py b/check_avatar.py new file mode 100644 index 0000000..4df7099 --- /dev/null +++ b/check_avatar.py @@ -0,0 +1,29 @@ +""" +检查用户头像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 +""" diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..7364884 --- /dev/null +++ b/config/config.py @@ -0,0 +1,63 @@ +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 +} diff --git a/config/cos_config.py b/config/cos_config.py new file mode 100644 index 0000000..84dcbb5 --- /dev/null +++ b/config/cos_config.py @@ -0,0 +1,99 @@ +""" +腾讯云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}' diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..96234cf --- /dev/null +++ b/config/database.py @@ -0,0 +1,25 @@ +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("⚠️ 继续运行,但可能会有数据库相关问题") diff --git a/create_admin.py b/create_admin.py new file mode 100644 index 0000000..7878169 --- /dev/null +++ b/create_admin.py @@ -0,0 +1,187 @@ +#!/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() diff --git a/create_sample_categories.py b/create_sample_categories.py new file mode 100644 index 0000000..37174cf --- /dev/null +++ b/create_sample_categories.py @@ -0,0 +1,111 @@ +#!/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() diff --git a/create_sample_specs.py b/create_sample_specs.py new file mode 100644 index 0000000..87dfef9 --- /dev/null +++ b/create_sample_specs.py @@ -0,0 +1,68 @@ +""" +创建示例规格数据 +""" +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() diff --git a/create_test_order.py b/create_test_order.py new file mode 100644 index 0000000..10ab0fd --- /dev/null +++ b/create_test_order.py @@ -0,0 +1,82 @@ +#!/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() diff --git a/create_test_user.py b/create_test_user.py new file mode 100644 index 0000000..f7037db --- /dev/null +++ b/create_test_user.py @@ -0,0 +1,72 @@ +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() diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..e69de29 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/export_code.py b/export_code.py new file mode 100644 index 0000000..4d41aca --- /dev/null +++ b/export_code.py @@ -0,0 +1,305 @@ +#!/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() diff --git a/project_code_export_20250704_033545.txt b/project_code_export_20250704_033545.txt new file mode 100644 index 0000000..aa0b436 --- /dev/null +++ b/project_code_export_20250704_033545.txt @@ -0,0 +1,15860 @@ +================================================================================ +项目代码导出文件 +================================================================================ +项目名称: 基于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 +================================================================================ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..59aead6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +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 diff --git a/run.py b/run.py new file mode 100644 index 0000000..05b3792 --- /dev/null +++ b/run.py @@ -0,0 +1,7 @@ +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) diff --git a/test_cos_connection.py b/test_cos_connection.py new file mode 100644 index 0000000..a61083b --- /dev/null +++ b/test_cos_connection.py @@ -0,0 +1,218 @@ +""" +测试腾讯云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() diff --git a/test_db_connection.py b/test_db_connection.py new file mode 100644 index 0000000..b35cc04 --- /dev/null +++ b/test_db_connection.py @@ -0,0 +1,120 @@ +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) diff --git a/test_email_detailed.py b/test_email_detailed.py new file mode 100644 index 0000000..753600c --- /dev/null +++ b/test_email_detailed.py @@ -0,0 +1,260 @@ +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. 服务器防火墙阻止了连接")