1. 项目概述
1.1 项目背景
随着高校学生消费意识的增强和环保理念的普及,校园二手物品交易需求日益增长。传统的线下交易方式存在信息不对称、交易范围有限、安全性难以保障等问题。本项目旨在设计并实现一个基于Python的校园二手物品交易平台,为高校师生提供一个便捷、安全、高效的二手物品交易环境。
1.2 项目目标
- 构建一个功能完善的校园二手物品交易平台
- 实现用户注册、登录、身份验证等基础功能
- 支持物品发布、搜索、收藏、交易等核心功能
- 提供即时通讯功能,方便买卖双方沟通
- 实现交易评价和信用体系,保障交易安全
- 提供数据分析功能,了解平台运营情况
1.3 技术栈选择
- 后端框架:Flask/Django
- 前端技术:HTML5、CSS3、JavaScript、Bootstrap
- 数据库:MySQL/PostgreSQL
- 缓存系统:Redis
- 搜索引擎:Elasticsearch
- 消息队列:RabbitMQ
- 即时通讯:WebSocket
- 图片存储:阿里云OSS/七牛云
- 部署环境:Docker + Nginx
2. 系统设计
2.1 系统架构
本项目采用前后端分离的架构设计,主要分为以下几个部分:
- 表示层:负责与用户交互的Web界面
- 应用层:处理业务逻辑的后端服务
- 数据层:负责数据存储和管理
- 基础设施层:提供系统运行所需的基础服务
系统架构图如下:
+------------------+ +------------------+ +------------------+
| 表示层 | | 应用层 | | 数据层 |
| | | | | |
| - Web界面 | | - 用户服务 | | - MySQL数据库 |
| - 移动端界面 |<--->| - 商品服务 |<--->| - Redis缓存 |
| - 管理后台 | | - 交易服务 | | - Elasticsearch |
| | | - 消息服务 | | - 文件存储 |
+------------------+ +------------------+ +------------------+
^
|
v
+------------------+
| 基础设施层 |
| |
| - 日志系统 |
| - 监控系统 |
| - 消息队列 |
| - 定时任务 |
+------------------+
2.2 数据库设计
2.2.1 E-R图
系统的核心实体包括:用户(User)、商品(Item)、订单(Order)、评价(Review)、消息(Message)等。
2.2.2 主要数据表
-
用户表(users)
- id: 用户ID
- username: 用户名
- password: 密码(加密存储)
- email: 邮箱
- phone: 手机号
- avatar: 头像URL
- school_id: 学校ID
- credit_score: 信用分
- create_time: 创建时间
- update_time: 更新时间
-
商品表(items)
- id: 商品ID
- title: 标题
- description: 描述
- price: 价格
- original_price: 原价
- category_id: 分类ID
- user_id: 发布者ID
- status: 状态(在售/已售/下架)
- view_count: 浏览次数
- favorite_count: 收藏次数
- create_time: 创建时间
- update_time: 更新时间
-
商品图片表(item_images)
- id: 图片ID
- item_id: 商品ID
- image_url: 图片URL
- is_cover: 是否为封面
- create_time: 创建时间
-
订单表(orders)
- id: 订单ID
- item_id: 商品ID
- seller_id: 卖家ID
- buyer_id: 买家ID
- price: 成交价格
- status: 状态(待付款/待发货/待收货/已完成/已取消)
- create_time: 创建时间
- update_time: 更新时间
-
评价表(reviews)
- id: 评价ID
- order_id: 订单ID
- user_id: 评价用户ID
- target_user_id: 被评价用户ID
- content: 评价内容
- rating: 评分(1-5)
- create_time: 创建时间
-
消息表(messages)
- id: 消息ID
- sender_id: 发送者ID
- receiver_id: 接收者ID
- content: 消息内容
- is_read: 是否已读
- create_time: 创建时间
2.3 功能模块设计
系统主要包含以下功能模块:
- 用户模块:负责用户注册、登录、个人信息管理等功能
- 商品模块:负责商品发布、编辑、下架、搜索等功能
- 交易模块:负责订单创建、支付、确认收货等功能
- 消息模块:负责用户间即时通讯、系统通知等功能
- 评价模块:负责交易评价、信用评分等功能
- 管理模块:负责平台运营、内容审核等功能
3. 系统实现
3.1 开发环境搭建
3.1.1 Python环境配置
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate
# 安装依赖
pip install -r requirements.txt
3.1.2 项目依赖
# requirements.txt
Flask==2.0.1
Flask-SQLAlchemy==2.5.1
Flask-Login==0.5.0
Flask-WTF==0.15.1
Flask-Mail==0.9.1
Flask-Migrate==3.1.0
Flask-SocketIO==5.1.1
Pillow==8.3.1
PyMySQL==1.0.2
redis==3.5.3
elasticsearch==7.14.0
pika==1.2.0
gunicorn==20.1.0
3.2 核心功能实现
3.2.1 用户认证模块
# app/models/user.py
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login_manager
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(120), unique=True, index=True)
password_hash = db.Column(db.String(128))
phone = db.Column(db.String(20))
avatar = db.Column(db.String(200))
school_id = db.Column(db.Integer, db.ForeignKey('schools.id'))
credit_score = db.Column(db.Integer, default=100)
create_time = db.Column(db.DateTime, default=datetime.utcnow)
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
items = db.relationship('Item', backref='seller', lazy='dynamic')
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
3.2.2 商品管理模块
# app/models/item.py
from app import db
from datetime import datetime
class Item(db.Model):
__tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), index=True)
description = db.Column(db.Text)
price = db.Column(db.Float)
original_price = db.Column(db.Float)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
status = db.Column(db.Integer, default=0) # 0: 在售, 1: 已售, 2: 下架
view_count = db.Column(db.Integer, default=0)
favorite_count = db.Column(db.Integer, default=0)
create_time = db.Column(db.DateTime, default=datetime.utcnow)
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
images = db.relationship('ItemImage', backref='item', lazy='dynamic')
orders = db.relationship('Order', backref='item', lazy='dynamic')
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'price': self.price,
'original_price': self.original_price,
'category_id': self.category_id,
'user_id': self.user_id,
'status': self.status,
'view_count': self.view_count,
'favorite_count': self.favorite_count,
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'cover_image': self.get_cover_image()
}
def get_cover_image(self):
cover = self.images.filter_by(is_cover=True).first()
if cover:
return cover.image_url
image = self.images.first()
return image.image_url if image else ''
3.2.3 交易流程实现
# app/models/order.py
from app import db
from datetime import datetime
class Order(db.Model):
__tablename__ = 'orders'
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey('items.id'))
seller_id = db.Column(db.Integer, db.ForeignKey('users.id'))
buyer_id = db.Column(db.Integer, db.ForeignKey('users.id'))
price = db.Column(db.Float)
status = db.Column(db.Integer, default=0) # 0: 待付款, 1: 待发货, 2: 待收货, 3: 已完成, 4: 已取消
create_time = db.Column(db.DateTime, default=datetime.utcnow)
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
reviews = db.relationship('Review', backref='order', lazy='dynamic')
def to_dict(self):
return {
'id': self.id,
'item_id': self.item_id,
'seller_id': self.seller_id,
'buyer_id': self.buyer_id,
'price': self.price,
'status': self.status,
'create_time': self.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'update_time': self.update_time.strftime('%Y-%m-%d %H:%M:%S')
}
3.2.4 即时通讯实现
# app/socket.py
from flask_socketio import SocketIO, emit, join_room, leave_room
from flask_login import current_user
from app import socketio, db
from app.models import Message, User
@socketio.on('connect')
def handle_connect():
if current_user.is_authenticated:
join_room(current_user.id)
emit('response', {'data': '连接成功'})
@socketio.on('send_message')
def handle_send_message(data):
if current_user.is_authenticated:
receiver_id = data.get('receiver_id')
content = data.get('content')
# 保存消息到数据库
message = Message(
sender_id=current_user.id,
receiver_id=receiver_id,
content=content
)
db.session.add(message)
db.session.commit()
# 发送消息给接收者
emit('new_message', {
'id': message.id,
'sender_id': current_user.id,
'sender_name': current_user.username,
'content': content,
'create_time': message.create_time.strftime('%Y-%m-%d %H:%M:%S')
}, room=receiver_id)
3.3 系统安全性实现
3.3.1 用户密码加密
使用Werkzeug提供的安全哈希函数对用户密码进行加密存储,确保即使数据库泄露,用户密码也不会被直接获取。
3.3.2 CSRF防护
使用Flask-WTF提供的CSRF保护功能,防止跨站请求伪造攻击。
# app/__init__.py
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
def create_app(config_name):
app = Flask(__name__)
# ...
csrf.init_app(app)
# ...
return app
3.3.3 XSS防护
使用Jinja2模板引擎的自动转义功能,防止跨站脚本攻击。
<!-- 在模板中安全地显示用户输入内容 -->
<div class="item-description">{{ item.description|safe }}</div>
3.3.4 SQL注入防护
使用SQLAlchemy ORM,避免直接拼接SQL语句,防止SQL注入攻击。
# 安全的查询方式
items = Item.query.filter(Item.title.like(f'%{keyword}%')).all()
# 不安全的查询方式(避免使用)
# items = db.session.execute(f"SELECT * FROM items WHERE title LIKE '%{keyword}%'").fetchall()
3.4 性能优化
3.4.1 数据库优化
- 为常用查询字段添加索引
- 使用连接池管理数据库连接
- 对大表进行分区
# 为常用查询字段添加索引
class Item(db.Model):
__tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), index=True) # 添加索引
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), index=True) # 添加索引
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), index=True) # 添加索引
status = db.Column(db.Integer, default=0, index=True) # 添加索引
3.4.2 缓存优化
使用Redis缓存热门商品、首页数据等,减轻数据库压力。
# app/utils/cache.py
import redis
import json
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def set_cache(key, value, expire=3600):
"""设置缓存"""
if isinstance(value, (dict, list)):
value = json.dumps(value)
redis_client.set(key, value, ex=expire)
def get_cache(key):
"""获取缓存"""
value = redis_client.get(key)
if value:
try:
return json.loads(value)
except:
return value.decode('utf-8')
return None
def clear_cache(pattern):
"""清除匹配模式的缓存"""
keys = redis_client.keys(pattern)
if keys:
redis_client.delete(*keys)
3.4.3 搜索优化
使用Elasticsearch实现全文搜索,提高搜索效率和准确性。
# app/utils/search.py
from elasticsearch import Elasticsearch
from app.models import Item
es = Elasticsearch(['http://localhost:9200'])
def index_item(item):
"""索引商品数据"""
doc = {
'id': item.id,
'title': item.title,
'description': item.description,
'price': item.price,
'category_id': item.category_id,
'status': item.status,
'create_time': item.create_time.strftime('%Y-%m-%d %H:%M:%S')
}
es.index(index='items', id=item.id, body=doc)
def search_items(keyword, page=1, per_page=10):
"""搜索商品"""
query = {
'query': {
'multi_match': {
'query': keyword,
'fields': ['title^3', 'description'] # 标题权重更高
}
},
'from': (page - 1) * per_page,
'size': per_page
}
result = es.search(index='items', body=query)
return result['hits']
4. 系统部署
4.1 Docker容器化部署
4.1.1 Dockerfile
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_APP=run.py
ENV FLASK_ENV=production
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"]
4.1.2 Docker Compose配置
# docker-compose.yml
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- db
- redis
- elasticsearch
environment:
- DATABASE_URL=mysql+pymysql://user:password@db:3306/campus_trading
- REDIS_URL=redis://redis:6379/0
- ELASTICSEARCH_URL=http://elasticsearch:9200
volumes:
- ./uploads:/app/uploads
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- MYSQL_DATABASE=campus_trading
- MYSQL_USER=user
- MYSQL_PASSWORD=password
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
volumes:
- redis_data:/data
elasticsearch:
image: elasticsearch:7.14.0
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- es_data:/usr/share/elasticsearch/data
nginx:
image: nginx:1.21
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./uploads:/usr/share/nginx/html/uploads
depends_on:
- web
volumes:
mysql_data:
redis_data:
es_data:
4.2 Nginx配置
# nginx.conf
server {
listen 80;
server_name campus-trading.example.com;
location /static {
alias /usr/share/nginx/html/static;
expires 30d;
}
location /uploads {
alias /usr/share/nginx/html/uploads;
expires 30d;
}
location / {
proxy_pass http://web:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /socket.io {
proxy_pass http://web:5000/socket.io;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
4.3 数据备份策略
- 数据库定时备份:每日凌晨自动备份MySQL数据库
- 文件定时备份:每周备份一次上传的图片文件
- 备份文件存储:备份文件存储在云存储服务中,确保数据安全
# 数据库备份脚本 backup_db.sh
#!/bin/bash
DATE=$(date +%Y%m%d)
BACKUP_DIR=/backup/mysql
mkdir -p $BACKUP_DIR
docker exec -it campus-trading_db_1 mysqldump -u root -p<password> campus_trading > $BACKUP_DIR/campus_trading_$DATE.sql
# 保留最近30天的备份
find $BACKUP_DIR -name "*.sql" -type f -mtime +30 -delete
5. 系统测试
5.1 单元测试
使用Python的unittest框架对各个模块进行单元测试,确保每个功能模块的正确性。
# tests/test_user_model.py
import unittest
from app import create_app, db
from app.models import User
class UserModelTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_password_setter(self):
u = User(password='cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password='cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
5.2 接口测试
使用Postman或Python的requests库对API接口进行测试,确保接口的正确性和稳定性。
# tests/test_api.py
import unittest
import json
from app import create_app, db
from app.models import User, Item
class APITestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
self.client = self.app.test_client()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_get_items(self):
# 创建测试数据
user = User(username='test', email='test@example.com', password='password')
db.session.add(user)
item = Item(title='Test Item', description='Test Description', price=100, user_id=1)
db.session.add(item)
db.session.commit()
# 测试获取商品列表接口
response = self.client.get('/api/items')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(len(data['items']), 1)
self.assertEqual(data['items'][0]['title'], 'Test Item')
5.3 性能测试
使用Apache JMeter或Locust对系统进行性能测试,评估系统在高并发情况下的表现。
# locustfile.py
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
@task(2)
def index(self):
self.client.get("/")
@task(1)
def view_item(self):
item_id = 1
self.client.get(f"/items/{item_id}")
@task(1)
def search_items(self):
self.client.get("/search?keyword=book")
6. 项目总结
6.1 项目成果
- 成功构建了一个功能完善的校园二手物品交易平台
- 实现了用户注册、登录、身份验证等基础功能
- 支持物品发布、搜索、收藏、交易等核心功能
- 提供了即时通讯功能,方便买卖双方沟通
- 实现了交易评价和信用体系,保障交易安全
- 提供了数据分析功能,了解平台运营情况
6.2 技术亮点
- 前后端分离架构:提高了开发效率和系统可维护性
- RESTful API设计:规范的API设计,便于前端调用和第三方集成
- WebSocket实时通讯:实现了用户间的即时通讯功能
- Elasticsearch全文搜索:提供了高效的商品搜索功能
- Redis缓存优化:减轻了数据库压力,提高了系统响应速度
- Docker容器化部署:简化了部署流程,提高了系统可移植性
6.3 项目不足与改进方向
- 移动端适配:当前系统主要针对PC端设计,移动端体验有待提升
- 支付系统集成:可以集成第三方支付系统,提供更便捷的支付方式
- 智能推荐系统:基于用户行为和偏好,推荐可能感兴趣的商品
- 社交功能增强:增加关注、点赞、分享等社交功能,提高用户粘性
- 安全性增强:加强对敏感操作的风控措施,防止欺诈行为
源代码
Directory Content Summary
Source Directory: ./campus_trading_platform
Directory Structure
campus_trading_platform/
.gitignore
README.md
requirements.txt
run.py
app/
__init__.py
forms/
auth.py
item.py
transaction.py
user.py
models/
item.py
user.py
routes/
auth.py
item.py
main.py
order.py
transaction.py
user.py
static/
css/
style.css
images/
js/
templates/
base.html
auth/
login.html
register.html
reset_password.html
reset_password_request.html
email/
reset_password.html
reset_password.txt
item/
buy.html
category.html
detail.html
edit.html
new.html
search.html
main/
about.html
help.html
index.html
order/
detail.html
list.html
review.html
transaction/
dispute.html
payment.html
records.html
statistics.html
user/
change_password.html
dashboard.html
edit_profile.html
profile.html
utils/
decorators.py
email.py
config/
config.py
database/
schema.sql
migrations/
tests/
File Contents
.gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Flask
instance/
.webassets-cache
# Virtual Environment
venv/
ENV/
env/
# Database
*.db
*.sqlite3
# Environment Variables
.env
.flaskenv
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Uploaded files
app/static/uploads/
# Logs
logs/
*.log
# Migrations
migrations/
README.md
# 校园二手物品交易平台
## 项目简介
校园二手物品交易平台是一个专为大学校园设计的二手物品交易网站,旨在为校园内的学生提供一个安全、便捷、高效的二手物品交易环境。通过这个平台,学生可以发布闲置物品、浏览他人发布的商品、进行线上沟通和线下交易。
## 功能特点
- **用户认证系统**:支持学生注册、登录、密码重置等功能
- **个人资料管理**:用户可以编辑个人信息、上传头像、修改密码等
- **商品管理**:发布、编辑、下架商品,上传商品图片
- **商品浏览**:按分类浏览商品,搜索商品,查看商品详情
- **收藏功能**:收藏感兴趣的商品,方便后续查看
- **即时通讯**:买卖双方可以通过站内消息进行沟通
- **交易管理**:记录交易状态,确认交易完成
- **评价系统**:交易完成后可以对对方进行评价
- **信用体系**:基于用户评价建立信用评分
## 技术栈
- **后端**:Python + Flask
- **前端**:HTML + CSS + JavaScript + Bootstrap 5
- **数据库**:SQLite (开发) / MySQL (生产)
- **ORM**:SQLAlchemy
- **表单处理**:Flask-WTF
- **用户认证**:Flask-Login
- **邮件发送**:Flask-Mail
## 安装指南
### 环境要求
- Python 3.8+
- pip
### 安装步骤
1. 克隆仓库
```bash
git clone https://github.com/yourusername/campus_trading_platform.git
cd campus_trading_platform
- 创建并激活虚拟环境
# Windows
python -m venv venv
venv\Scripts\activate
# Linux/Mac
python -m venv venv
source venv/bin/activate
- 安装依赖
pip install -r requirements.txt
- 设置环境变量
创建一个.env
文件在项目根目录,并添加以下内容:
SECRET_KEY=your-secret-key
DATABASE_URL=sqlite:///app.db
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password
- 初始化数据库
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
- 运行应用
python run.py
应用将在 http://localhost:5000 运行。
项目结构
campus_trading_platform/
├── app/ # 应用主目录
│ ├── __init__.py # 应用初始化
│ ├── models/ # 数据模型
│ ├── routes/ # 路由处理
│ ├── forms/ # 表单定义
│ ├── utils/ # 工具函数
│ ├── static/ # 静态文件
│ └── templates/ # HTML模板
├── config/ # 配置文件
├── migrations/ # 数据库迁移文件
├── tests/ # 测试代码
├── .env # 环境变量
├── requirements.txt # 依赖列表
└── run.py # 应用入口
贡献指南
- Fork 项目
- 创建特性分支 (
git checkout -b feature/amazing-feature
) - 提交更改 (
git commit -m 'Add some amazing feature'
) - 推送到分支 (
git push origin feature/amazing-feature
) - 创建 Pull Request
许可证
本项目采用 MIT 许可证 - 详情请参阅 LICENSE 文件。
联系方式
项目维护者 - your-email@example.com
项目链接:https://github.com/yourusername/campus_trading_platform
### requirements.txt
```text/plain
Flask==2.2.3
Flask-SQLAlchemy==3.0.3
Flask-Login==0.6.2
Flask-WTF==1.1.1
Flask-Mail==0.9.1
Flask-Migrate==4.0.4
email-validator==2.0.0
Pillow==9.5.0
python-dotenv==1.0.0
itsdangerous==2.1.2
Werkzeug==2.2.3
Jinja2==3.1.2
SQLAlchemy==2.0.4
WTForms==3.0.1
alembic==1.10.2
blinker==1.5.0
click==8.1.3
MarkupSafe==2.1.2
run.py
from app import create_app
import os
app = create_app()
if __name__ == '__main__':
# 获取环境变量中的端口,如果没有则默认使用5000
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port, debug=True)
app_init_.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from config.config import Config
# 初始化扩展
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = 'auth.login' # 设置登录视图的端点
login_manager.login_message = '请先登录才能访问此页面'
mail = Mail()
def create_app(config_class=Config):
"""创建并配置Flask应用"""
app = Flask(__name__)
app.config.from_object(config_class)
# 初始化扩展
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
mail.init_app(app)
# 注册蓝图
from app.routes.auth import auth_bp
from app.routes.main import main_bp
from app.routes.user import user_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(user_bp)
return app
app\forms\auth.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo, ValidationError
from app.models.user import User
class LoginForm(FlaskForm):
"""用户登录表单"""
email = StringField('邮箱', validators=[
DataRequired(message='请输入邮箱'),
Email(message='请输入有效的邮箱地址')
])
password = PasswordField('密码', validators=[
DataRequired(message='请输入密码')
])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
"""用户注册表单"""
username = StringField('用户名', validators=[
DataRequired(message='请输入用户名'),
Length(min=2, max=20, message='用户名长度必须在2-20个字符之间'),
Regexp('^[A-Za-z0-9_\u4e00-\u9fa5]+$', message='用户名只能包含字母、数字、下划线和汉字')
])
email = StringField('邮箱', validators=[
DataRequired(message='请输入邮箱'),
Email(message='请输入有效的邮箱地址')
])
student_id = StringField('学号', validators=[
DataRequired(message='请输入学号'),
Length(min=8, max=12, message='学号长度必须在8-12个字符之间'),
Regexp('^[0-9]+$', message='学号只能包含数字')
])
phone = StringField('手机号', validators=[
DataRequired(message='请输入手机号'),
Length(min=11, max=11, message='手机号必须是11位'),
Regexp('^1[3-9]\d{9}$', message='请输入有效的手机号')
])
password = PasswordField('密码', validators=[
DataRequired(message='请输入密码'),
Length(min=6, message='密码长度不能少于6个字符')
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired(message='请确认密码'),
EqualTo('password', message='两次输入的密码不一致')
])
submit = SubmitField('注册')
def validate_username(self, field):
"""验证用户名是否已存在"""
user = User.query.filter_by(username=field.data).first()
if user:
raise ValidationError('该用户名已被使用')
def validate_email(self, field):
"""验证邮箱是否已存在"""
user = User.query.filter_by(email=field.data).first()
if user:
raise ValidationError('该邮箱已被注册')
def validate_student_id(self, field):
"""验证学号是否已存在"""
user = User.query.filter_by(student_id=field.data).first()
if user:
raise ValidationError('该学号已被注册')
def validate_phone(self, field):
"""验证手机号是否已存在"""
user = User.query.filter_by(phone=field.data).first()
if user:
raise ValidationError('该手机号已被注册')
class ResetPasswordRequestForm(FlaskForm):
"""请求重置密码表单"""
email = StringField('邮箱', validators=[
DataRequired(message='请输入邮箱'),
Email(message='请输入有效的邮箱地址')
])
submit = SubmitField('重置密码')
class ResetPasswordForm(FlaskForm):
"""重置密码表单"""
password = PasswordField('新密码', validators=[
DataRequired(message='请输入新密码'),
Length(min=6, message='密码长度不能少于6个字符')
])
confirm_password = PasswordField('确认新密码', validators=[
DataRequired(message='请确认新密码'),
EqualTo('password', message='两次输入的密码不一致')
])
submit = SubmitField('重置密码')
app\forms\item.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired
from wtforms import StringField, TextAreaField, FloatField, SelectField, SubmitField, MultipleFileField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
class ItemForm(FlaskForm):
"""商品发布/编辑表单"""
title = StringField('商品标题', validators=[
DataRequired(message='请输入商品标题'),
Length(min=2, max=100, message='标题长度必须在2-100个字符之间')
])
category = SelectField('商品分类', coerce=int, validators=[
DataRequired(message='请选择商品分类')
])
description = TextAreaField('商品描述', validators=[
DataRequired(message='请输入商品描述'),
Length(min=10, max=2000, message='描述长度必须在10-2000个字符之间')
])
price = FloatField('售价(¥)', validators=[
DataRequired(message='请输入售价'),
NumberRange(min=0.01, message='价格必须大于0')
])
original_price = FloatField('原价(¥)', validators=[
Optional(),
NumberRange(min=0.01, message='价格必须大于0')
])
condition = SelectField('物品状况', choices=[
('brand_new', '全新'),
('like_new', '几乎全新'),
('slightly_used', '轻微使用痕迹'),
('used', '使用过'),
('heavily_used', '重度使用')
], validators=[DataRequired(message='请选择物品状况')])
location = StringField('交易地点', validators=[
DataRequired(message='请输入交易地点'),
Length(max=100, message='地点长度不能超过100个字符')
])
images = MultipleFileField('商品图片', validators=[
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], message='只允许上传jpg, jpeg, png, gif格式的图片')
])
submit = SubmitField('发布商品')
class ItemSearchForm(FlaskForm):
"""商品搜索表单"""
keyword = StringField('关键词', validators=[
Optional(),
Length(max=50, message='关键词长度不能超过50个字符')
])
category = SelectField('分类', coerce=int, choices=[(0, '所有分类')], validators=[
Optional()
])
min_price = FloatField('最低价格', validators=[
Optional(),
NumberRange(min=0, message='价格不能为负数')
])
max_price = FloatField('最高价格', validators=[
Optional(),
NumberRange(min=0, message='价格不能为负数')
])
condition = SelectField('物品状况', choices=[
('', '所有状况'),
('brand_new', '全新'),
('like_new', '几乎全新'),
('slightly_used', '轻微使用痕迹'),
('used', '使用过'),
('heavily_used', '重度使用')
], validators=[Optional()])
sort = SelectField('排序方式', choices=[
('newest', '最新发布'),
('price_asc', '价格从低到高'),
('price_desc', '价格从高到低'),
('popular', '最受欢迎')
], default='newest', validators=[Optional()])
submit = SubmitField('搜索')
class OrderForm(FlaskForm):
"""订单创建表单"""
message = TextAreaField('留言', validators=[
Optional(),
Length(max=500, message='留言长度不能超过500个字符')
])
submit = SubmitField('确认购买')
class ReviewForm(FlaskForm):
"""评价表单"""
rating = SelectField('评分', choices=[
(5, '★★★★★ 非常满意'),
(4, '★★★★☆ 满意'),
(3, '★★★☆☆ 一般'),
(2, '★★☆☆☆ 不满意'),
(1, '★☆☆☆☆ 非常不满意')
], coerce=int, validators=[DataRequired(message='请选择评分')])
content = TextAreaField('评价内容', validators=[
DataRequired(message='请输入评价内容'),
Length(min=5, max=500, message='评价内容长度必须在5-500个字符之间')
])
submit = SubmitField('提交评价')
app\forms\transaction.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, DecimalField, SubmitField, HiddenField, RadioField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
from flask_wtf.file import FileField, FileAllowed
class PaymentForm(FlaskForm):
"""支付表单"""
payment_method = SelectField('支付方式',
choices=[
('alipay', '支付宝'),
('wechat', '微信支付'),
('campus_card', '校园卡')
],
validators=[DataRequired(message='请选择支付方式')])
agreement = RadioField('同意条款',
choices=[('agree', '我已阅读并同意《交易协议》和《支付条款》')],
validators=[DataRequired(message='请阅读并同意条款')])
submit = SubmitField('确认支付')
class DisputeForm(FlaskForm):
"""纠纷申请表单"""
reason = SelectField('纠纷原因',
choices=[
('item_not_as_described', '商品与描述不符'),
('item_damaged', '商品损坏'),
('seller_not_responding', '卖家不回应'),
('buyer_not_responding', '买家不回应'),
('payment_issue', '支付问题'),
('other', '其他原因')
],
validators=[DataRequired(message='请选择纠纷原因')])
description = TextAreaField('详细说明',
validators=[DataRequired(message='请提供详细说明'),
Length(min=10, max=500, message='详细说明应在10-500个字符之间')])
evidence_images = FileField('证据图片(可选,最多3张)',
validators=[
Optional(),
FileAllowed(['jpg', 'jpeg', 'png'], '只允许上传jpg, jpeg, png格式的图片')
])
preferred_solution = SelectField('期望解决方案',
choices=[
('refund', '退款'),
('exchange', '换货'),
('partial_refund', '部分退款'),
('mediation', '平台调解'),
('other', '其他')
],
validators=[DataRequired(message='请选择期望的解决方案')])
contact_phone = StringField('联系电话',
validators=[DataRequired(message='请提供联系电话'),
Length(min=11, max=11, message='请输入11位手机号码')])
submit = SubmitField('提交纠纷申请')
class RefundForm(FlaskForm):
"""退款申请表单"""
refund_amount = DecimalField('退款金额',
validators=[DataRequired(message='请输入退款金额'),
NumberRange(min=0.01, message='退款金额必须大于0')])
reason = SelectField('退款原因',
choices=[
('change_mind', '买家改变主意'),
('item_not_as_described', '商品与描述不符'),
('item_damaged', '商品损坏'),
('wrong_item', '收到错误商品'),
('not_received', '未收到商品'),
('other', '其他原因')
],
validators=[DataRequired(message='请选择退款原因')])
description = TextAreaField('详细说明',
validators=[DataRequired(message='请提供详细说明'),
Length(min=10, max=500, message='详细说明应在10-500个字符之间')])
evidence_images = FileField('证据图片(可选,最多3张)',
validators=[
Optional(),
FileAllowed(['jpg', 'jpeg', 'png'], '只允许上传jpg, jpeg, png格式的图片')
])
submit = SubmitField('申请退款')
class TransactionFeedbackForm(FlaskForm):
"""交易反馈表单"""
satisfaction = SelectField('交易满意度',
choices=[
('5', '非常满意'),
('4', '满意'),
('3', '一般'),
('2', '不满意'),
('1', '非常不满意')
],
validators=[DataRequired(message='请选择交易满意度')])
platform_feedback = TextAreaField('平台使用反馈(可选)',
validators=[Optional(),
Length(max=500, message='反馈内容不能超过500个字符')])
improvement_suggestions = TextAreaField('改进建议(可选)',
validators=[Optional(),
Length(max=500, message='建议内容不能超过500个字符')])
submit = SubmitField('提交反馈')
app\forms\user.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo, ValidationError
from flask_login import current_user
from app.models.user import User
class EditProfileForm(FlaskForm):
"""编辑个人资料表单"""
username = StringField('用户名', validators=[
DataRequired(message='请输入用户名'),
Length(min=2, max=20, message='用户名长度必须在2-20个字符之间'),
Regexp('^[A-Za-z0-9_\u4e00-\u9fa5]+$', message='用户名只能包含字母、数字、下划线和汉字')
])
phone = StringField('手机号', validators=[
DataRequired(message='请输入手机号'),
Length(min=11, max=11, message='手机号必须是11位'),
Regexp('^1[3-9]\d{9}$', message='请输入有效的手机号')
])
bio = TextAreaField('个人简介', validators=[
Length(max=200, message='个人简介不能超过200个字符')
])
dormitory = StringField('宿舍地址', validators=[
Length(max=100, message='宿舍地址不能超过100个字符')
])
avatar = FileField('头像', validators=[
FileAllowed(['jpg', 'jpeg', 'png', 'gif'], '只允许上传图片文件')
])
submit = SubmitField('保存修改')
def validate_username(self, field):
"""验证用户名是否已存在"""
if field.data != current_user.username:
user = User.query.filter_by(username=field.data).first()
if user:
raise ValidationError('该用户名已被使用')
def validate_phone(self, field):
"""验证手机号是否已存在"""
if field.data != current_user.phone:
user = User.query.filter_by(phone=field.data).first()
if user:
raise ValidationError('该手机号已被注册')
class ChangePasswordForm(FlaskForm):
"""修改密码表单"""
current_password = PasswordField('当前密码', validators=[
DataRequired(message='请输入当前密码')
])
new_password = PasswordField('新密码', validators=[
DataRequired(message='请输入新密码'),
Length(min=6, message='密码长度不能少于6个字符')
])
confirm_password = PasswordField('确认新密码', validators=[
DataRequired(message='请确认新密码'),
EqualTo('new_password', message='两次输入的密码不一致')
])
submit = SubmitField('修改密码')
app\models\item.py
from datetime import datetime
import os
from flask import current_app
from app import db
import json
from sqlalchemy.ext.hybrid import hybrid_property
class Category(db.Model):
"""商品分类模型"""
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.String(200))
icon = db.Column(db.String(50)) # 分类图标
items = db.relationship('Item', backref='category', lazy='dynamic')
def __repr__(self):
return f'<Category {self.name}>'
class Item(db.Model):
"""商品模型"""
__tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
price = db.Column(db.Float, nullable=False)
original_price = db.Column(db.Float) # 原价
condition = db.Column(db.String(20), nullable=False) # 物品状况:全新、几成新等
images = db.Column(db.Text) # 存储图片路径的JSON字符串
location = db.Column(db.String(100)) # 交易地点
views = db.Column(db.Integer, default=0) # 浏览次数
is_sold = db.Column(db.Boolean, default=False) # 是否已售出
is_active = db.Column(db.Boolean, default=True) # 是否处于活跃状态
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 外键
seller_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
# 关系
orders = db.relationship('Order', backref='item', lazy='dynamic')
favorites = db.relationship('Favorite', backref='item', lazy='dynamic')
def __repr__(self):
return f'<Item {self.title}>'
@property
def image_list(self):
"""返回图片路径列表"""
if not self.images:
return []
return json.loads(self.images)
def add_image(self, image_path):
"""添加图片路径"""
images = self.image_list
images.append(image_path)
self.images = json.dumps(images)
def remove_image(self, image_path):
"""移除图片路径"""
images = self.image_list
if image_path in images:
images.remove(image_path)
self.images = json.dumps(images)
return True
return False
def get_main_image_url(self):
"""获取主图URL"""
images = self.image_list
if images:
return os.path.join('/static/uploads/items', images[0])
return '/static/images/default_item.jpg'
def increment_view(self):
"""增加浏览次数"""
self.views += 1
db.session.commit()
@hybrid_property
def is_new(self):
"""判断是否为新发布商品(7天内)"""
return (datetime.utcnow() - self.created_at).days <= 7
@property
def discount_percentage(self):
"""计算折扣百分比"""
if self.original_price and self.original_price > 0:
return int((1 - self.price / self.original_price) * 100)
return 0
class Favorite(db.Model):
"""收藏模型"""
__tablename__ = 'favorites'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
item_id = db.Column(db.Integer, db.ForeignKey('items.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (db.UniqueConstraint('user_id', 'item_id', name='unique_user_item'),)
def __repr__(self):
return f'<Favorite {self.user_id} - {self.item_id}>'
class Order(db.Model):
"""订单模型"""
__tablename__ = 'orders'
id = db.Column(db.Integer, primary_key=True)
order_number = db.Column(db.String(20), unique=True, nullable=False)
price = db.Column(db.Float, nullable=False)
status = db.Column(db.String(20), default='pending') # pending, paid, completed, cancelled
message = db.Column(db.Text) # 买家留言
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 外键
buyer_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
seller_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
item_id = db.Column(db.Integer, db.ForeignKey('items.id'), nullable=False)
# 关系
reviews = db.relationship('Review', backref='order', lazy='dynamic')
def __repr__(self):
return f'<Order {self.order_number}>'
@property
def is_reviewed(self):
"""判断订单是否已评价"""
return self.reviews.count() > 0
@staticmethod
def generate_order_number():
"""生成订单号"""
import random
import time
return f"O{int(time.time())}{random.randint(1000, 9999)}"
class Review(db.Model):
"""评价模型"""
__tablename__ = 'reviews'
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
rating = db.Column(db.Integer, nullable=False) # 1-5星评价
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 外键
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
reviewer_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
target_user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
def __repr__(self):
return f'<Review {self.id} - {self.rating}星>'
app\models\user.py
from datetime import datetime
from flask import current_app
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
from time import time
import uuid
import os
from app import db, login_manager
class User(UserMixin, db.Model):
"""用户模型"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True, nullable=False)
email = db.Column(db.String(120), unique=True, index=True, nullable=False)
password_hash = db.Column(db.String(128))
student_id = db.Column(db.String(20), unique=True, index=True, nullable=False)
phone = db.Column(db.String(20), unique=True)
avatar = db.Column(db.String(120), default='default_avatar.png')
bio = db.Column(db.Text)
dormitory = db.Column(db.String(100))
is_active = db.Column(db.Boolean, default=True)
is_admin = db.Column(db.Boolean, default=False)
credit_score = db.Column(db.Integer, default=100) # 信用评分,初始为100分
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
items = db.relationship('Item', backref='seller', lazy='dynamic')
orders = db.relationship('Order', backref='buyer', lazy='dynamic', foreign_keys='Order.buyer_id')
sold_orders = db.relationship('Order', backref='seller', lazy='dynamic', foreign_keys='Order.seller_id')
reviews_received = db.relationship('Review', backref='reviewee', lazy='dynamic', foreign_keys='Review.reviewee_id')
reviews_given = db.relationship('Review', backref='reviewer', lazy='dynamic', foreign_keys='Review.reviewer_id')
messages_sent = db.relationship('Message', backref='sender', lazy='dynamic', foreign_keys='Message.sender_id')
messages_received = db.relationship('Message', backref='recipient', lazy='dynamic', foreign_keys='Message.recipient_id')
notifications = db.relationship('Notification', backref='user', lazy='dynamic')
favorites = db.relationship('Favorite', backref='user', lazy='dynamic')
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# 如果用户邮箱是管理员邮箱,则设置为管理员
if self.email == current_app.config['ADMIN_EMAIL']:
self.is_admin = True
def __repr__(self):
return f'<User {self.username}>'
@property
def password(self):
"""密码属性不可读"""
raise AttributeError('密码不是可读属性')
@password.setter
def password(self, password):
"""设置密码"""
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""验证密码"""
return check_password_hash(self.password_hash, password)
def ping(self):
"""更新用户最后访问时间"""
self.last_seen = datetime.utcnow()
db.session.add(self)
def get_reset_password_token(self, expires_in=3600):
"""生成密码重置令牌"""
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'],
algorithm='HS256'
)
@staticmethod
def verify_reset_password_token(token):
"""验证密码重置令牌"""
try:
id = jwt.decode(
token,
current_app.config['SECRET_KEY'],
algorithms=['HS256']
)['reset_password']
except:
return None
return User.query.get(id)
def update_credit_score(self, points):
"""更新用户信用评分"""
self.credit_score += points
# 确保信用评分在0-100之间
if self.credit_score > 100:
self.credit_score = 100
elif self.credit_score < 0:
self.credit_score = 0
db.session.add(self)
def add_notification(self, name, data):
"""添加通知"""
# 先删除同名的旧通知
self.notifications.filter_by(name=name).delete()
notification = Notification(name=name, payload_json=data, user=self)
db.session.add(notification)
return notification
def get_avatar_url(self):
"""获取用户头像URL"""
if self.avatar and self.avatar != current_app.config['DEFAULT_AVATAR']:
return f'/static/uploads/avatars/{self.avatar}'
return f'/static/images/{current_app.config["DEFAULT_AVATAR"]}'
def save_avatar(self, avatar_file):
"""保存用户头像"""
# 生成唯一文件名
filename = str(uuid.uuid4()) + os.path.splitext(avatar_file.filename)[1]
# 确保上传目录存在
avatar_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'avatars')
if not os.path.exists(avatar_dir):
os.makedirs(avatar_dir)
# 保存文件
avatar_path = os.path.join(avatar_dir, filename)
avatar_file.save(avatar_path)
# 更新用户头像
self.avatar = filename
db.session.add(self)
return filename
@login_manager.user_loader
def load_user(user_id):
"""加载用户的回调函数"""
return User.query.get(int(user_id))
class Notification(db.Model):
"""通知模型"""
__tablename__ = 'notifications'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), index=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
timestamp = db.Column(db.Float, index=True, default=time)
payload_json = db.Column(db.Text)
def __repr__(self):
return f'<Notification {self.name}>'
app\routes\auth.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
import jwt
from time import time
from app import db
from app.models.user import User
from app.forms.auth import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
from app.utils.email import send_password_reset_email
# 创建认证蓝图
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""用户登录视图"""
# 如果用户已登录,重定向到首页
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
# 查找用户
user = User.query.filter_by(email=form.email.data).first()
# 验证用户和密码
if user is None or not user.verify_password(form.password.data):
flash('邮箱或密码无效', 'danger')
return redirect(url_for('auth.login'))
# 登录用户
login_user(user, remember=form.remember_me.data)
# 更新最后访问时间
user.ping()
db.session.commit()
# 重定向到下一页或首页
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
flash('登录成功!', 'success')
return redirect(next_page)
return render_template('auth/login.html', title='登录', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
"""用户登出视图"""
logout_user()
flash('您已成功登出', 'info')
return redirect(url_for('main.index'))
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""用户注册视图"""
# 如果用户已登录,重定向到首页
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
# 创建新用户
user = User(
username=form.username.data,
email=form.email.data,
student_id=form.student_id.data,
phone=form.phone.data
)
user.password = form.password.data
# 保存到数据库
db.session.add(user)
db.session.commit()
flash('注册成功!现在您可以登录了。', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='注册', form=form)
@auth_bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
"""请求重置密码视图"""
# 如果用户已登录,重定向到首页
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
# 无论用户是否存在,都显示相同的消息,避免泄露用户信息
flash('重置密码的邮件已发送,请检查您的邮箱', 'info')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password_request.html', title='重置密码', form=form)
@auth_bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
"""重置密码视图"""
# 如果用户已登录,重定向到首页
if current_user.is_authenticated:
return redirect(url_for('main.index'))
# 验证令牌
user = User.verify_reset_password_token(token)
if not user:
flash('令牌无效或已过期', 'danger')
return redirect(url_for('main.index'))
form = ResetPasswordForm()
if form.validate_on_submit():
# 设置新密码
user.password = form.password.data
db.session.commit()
flash('您的密码已重置', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', title='重置密码', form=form)
app\routes\item.py
import os
import json
import uuid
from datetime import datetime
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, jsonify, abort
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from app import db
from app.models.item import Item, Category, Favorite, Order, Review
from app.forms.item import ItemForm, ItemSearchForm, OrderForm, ReviewForm
from app.utils.decorators import confirmed_required
item_bp = Blueprint('item', __name__)
def allowed_file(filename):
"""检查文件是否为允许的扩展名"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ['jpg', 'jpeg', 'png', 'gif']
def save_image(file):
"""保存图片并返回文件名"""
if not file or not allowed_file(file.filename):
return None
# 生成唯一文件名
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()
new_filename = f"{uuid.uuid4().hex}.{ext}"
# 确保目录存在
upload_folder = os.path.join(current_app.root_path, 'static/uploads/items')
os.makedirs(upload_folder, exist_ok=True)
# 保存文件
file_path = os.path.join(upload_folder, new_filename)
file.save(file_path)
return new_filename
@item_bp.route('/new', methods=['GET', 'POST'])
@login_required
@confirmed_required
def new_item():
"""发布新商品"""
form = ItemForm()
# 获取所有分类并填充到表单选择框
form.category.choices = [(c.id, c.name) for c in Category.query.all()]
if form.validate_on_submit():
# 创建新商品
item = Item(
title=form.title.data,
description=form.description.data,
price=form.price.data,
original_price=form.original_price.data,
condition=form.condition.data,
location=form.location.data,
seller_id=current_user.id,
category_id=form.category.data,
images=json.dumps([]) # 初始化为空列表
)
db.session.add(item)
db.session.commit()
# 处理图片上传
image_filenames = []
if form.images.data:
for image in form.images.data:
if image and allowed_file(image.filename):
filename = save_image(image)
if filename:
image_filenames.append(filename)
# 更新商品图片列表
item.images = json.dumps(image_filenames)
db.session.commit()
flash('商品发布成功!', 'success')
return redirect(url_for('item.detail', item_id=item.id))
return render_template('item/new.html', form=form, title='发布商品')
@item_bp.route('/<int:item_id>')
def detail(item_id):
"""商品详情页"""
item = Item.query.get_or_404(item_id)
# 增加浏览次数
if current_user.is_authenticated and current_user.id != item.seller_id:
item.increment_view()
# 检查当前用户是否已收藏该商品
is_favorite = False
if current_user.is_authenticated:
is_favorite = Favorite.query.filter_by(
user_id=current_user.id, item_id=item.id
).first() is not None
# 获取相似商品
similar_items = Item.query.filter(
Item.category_id == item.category_id,
Item.id != item.id,
Item.is_sold == False,
Item.is_active == True
).order_by(Item.created_at.desc()).limit(4).all()
# 获取卖家其他商品
seller_other_items = Item.query.filter(
Item.seller_id == item.seller_id,
Item.id != item.id,
Item.is_sold == False,
Item.is_active == True
).order_by(Item.created_at.desc()).limit(4).all()
return render_template('item/detail.html',
item=item,
is_favorite=is_favorite,
similar_items=similar_items,
seller_other_items=seller_other_items,
title=item.title)
@item_bp.route('/<int:item_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_item(item_id):
"""编辑商品"""
item = Item.query.get_or_404(item_id)
# 检查是否为商品卖家
if item.seller_id != current_user.id:
flash('您没有权限编辑此商品', 'danger')
return redirect(url_for('item.detail', item_id=item.id))
# 检查商品是否已售出
if item.is_sold:
flash('已售出的商品不能编辑', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
form = ItemForm()
form.category.choices = [(c.id, c.name) for c in Category.query.all()]
if form.validate_on_submit():
# 更新商品信息
item.title = form.title.data
item.description = form.description.data
item.price = form.price.data
item.original_price = form.original_price.data
item.condition = form.condition.data
item.location = form.location.data
item.category_id = form.category.data
# 处理图片上传
if form.images.data and any(form.images.data):
image_filenames = item.image_list
for image in form.images.data:
if image and allowed_file(image.filename):
filename = save_image(image)
if filename:
image_filenames.append(filename)
item.images = json.dumps(image_filenames)
db.session.commit()
flash('商品信息已更新', 'success')
return redirect(url_for('item.detail', item_id=item.id))
# GET请求,填充表单数据
elif request.method == 'GET':
form.title.data = item.title
form.description.data = item.description
form.price.data = item.price
form.original_price.data = item.original_price
form.condition.data = item.condition
form.location.data = item.location
form.category.data = item.category_id
return render_template('item/edit.html', form=form, item=item, title='编辑商品')
@item_bp.route('/<int:item_id>/delete', methods=['POST'])
@login_required
def delete_item(item_id):
"""删除商品"""
item = Item.query.get_or_404(item_id)
# 检查是否为商品卖家
if item.seller_id != current_user.id:
flash('您没有权限删除此商品', 'danger')
return redirect(url_for('item.detail', item_id=item.id))
# 检查是否有关联订单
if item.orders.count() > 0:
flash('此商品已有订单,不能删除', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
# 删除商品图片
for image_filename in item.image_list:
image_path = os.path.join(current_app.root_path, 'static/uploads/items', image_filename)
if os.path.exists(image_path):
os.remove(image_path)
# 删除收藏记录
Favorite.query.filter_by(item_id=item.id).delete()
# 删除商品
db.session.delete(item)
db.session.commit()
flash('商品已删除', 'success')
return redirect(url_for('user.dashboard'))
@item_bp.route('/<int:item_id>/toggle_status', methods=['POST'])
@login_required
def toggle_status(item_id):
"""切换商品状态(上架/下架)"""
item = Item.query.get_or_404(item_id)
# 检查是否为商品卖家
if item.seller_id != current_user.id:
flash('您没有权限操作此商品', 'danger')
return redirect(url_for('item.detail', item_id=item.id))
# 检查商品是否已售出
if item.is_sold:
flash('已售出的商品不能操作', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
# 切换状态
item.is_active = not item.is_active
db.session.commit()
status = "上架" if item.is_active else "下架"
flash(f'商品已{status}', 'success')
return redirect(url_for('item.detail', item_id=item.id))
@item_bp.route('/<int:item_id>/favorite', methods=['POST'])
@login_required
def toggle_favorite(item_id):
"""收藏/取消收藏商品"""
item = Item.query.get_or_404(item_id)
# 检查是否已收藏
favorite = Favorite.query.filter_by(
user_id=current_user.id, item_id=item.id
).first()
if favorite:
# 取消收藏
db.session.delete(favorite)
is_favorite = False
message = '已取消收藏'
else:
# 添加收藏
favorite = Favorite(user_id=current_user.id, item_id=item.id)
db.session.add(favorite)
is_favorite = True
message = '已收藏'
db.session.commit()
# 如果是AJAX请求,返回JSON响应
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'success': True,
'is_favorite': is_favorite,
'message': message
})
# 普通请求,重定向回商品详情页
flash(message, 'success')
return redirect(url_for('item.detail', item_id=item.id))
@item_bp.route('/<int:item_id>/buy', methods=['GET', 'POST'])
@login_required
@confirmed_required
def buy_item(item_id):
"""购买商品"""
item = Item.query.get_or_404(item_id)
# 检查商品是否可购买
if item.is_sold:
flash('此商品已售出', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
if not item.is_active:
flash('此商品已下架', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
# 检查是否为自己的商品
if item.seller_id == current_user.id:
flash('不能购买自己的商品', 'warning')
return redirect(url_for('item.detail', item_id=item.id))
form = OrderForm()
if form.validate_on_submit():
# 创建订单
order = Order(
order_number=Order.generate_order_number(),
price=item.price,
message=form.message.data,
buyer_id=current_user.id,
seller_id=item.seller_id,
item_id=item.id,
status='pending' # 待付款状态
)
# 标记商品为已售出
item.is_sold = True
db.session.add(order)
db.session.commit()
flash('订单已创建,请尽快完成支付', 'success')
return redirect(url_for('order.detail', order_number=order.order_number))
return render_template('item/buy.html', form=form, item=item, title='购买商品')
@item_bp.route('/search')
def search():
"""搜索商品"""
form = ItemSearchForm(request.args, meta={'csrf': False})
form.category.choices = [(0, '所有分类')] + [(c.id, c.name) for c in Category.query.all()]
page = request.args.get('page', 1, type=int)
per_page = current_app.config.get('ITEMS_PER_PAGE', 12)
# 构建查询
query = Item.query.filter_by(is_active=True, is_sold=False)
# 关键词搜索
if form.keyword.data:
search_term = f"%{form.keyword.data}%"
query = query.filter(
(Item.title.like(search_term)) |
(Item.description.like(search_term))
)
# 分类筛选
if form.category.data and form.category.data != 0:
query = query.filter_by(category_id=form.category.data)
# 价格范围
if form.min_price.data is not None:
query = query.filter(Item.price >= form.min_price.data)
if form.max_price.data is not None:
query = query.filter(Item.price <= form.max_price.data)
# 物品状况
if form.condition.data:
query = query.filter_by(condition=form.condition.data)
# 排序
if form.sort.data == 'newest':
query = query.order_by(Item.created_at.desc())
elif form.sort.data == 'price_asc':
query = query.order_by(Item.price.asc())
elif form.sort.data == 'price_desc':
query = query.order_by(Item.price.desc())
elif form.sort.data == 'popular':
query = query.order_by(Item.views.desc())
# 分页
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
items = pagination.items
return render_template('item/search.html',
form=form,
items=items,
pagination=pagination,
title='搜索商品')
@item_bp.route('/category/<int:category_id>')
def category(category_id):
"""按分类浏览商品"""
category = Category.query.get_or_404(category_id)
page = request.args.get('page', 1, type=int)
per_page = current_app.config.get('ITEMS_PER_PAGE', 12)
# 获取该分类下的所有活跃且未售出的商品
query = Item.query.filter_by(
category_id=category_id,
is_active=True,
is_sold=False
).order_by(Item.created_at.desc())
# 分页
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
items = pagination.items
return render_template('item/category.html',
category=category,
items=items,
pagination=pagination,
title=f'{category.name} - 商品分类')
@item_bp.route('/api/items/<int:item_id>/favorite', methods=['POST'])
@login_required
def api_toggle_favorite(item_id):
"""API: 收藏/取消收藏商品"""
item = Item.query.get_or_404(item_id)
# 检查是否已收藏
favorite = Favorite.query.filter_by(
user_id=current_user.id, item_id=item.id
).first()
if favorite:
# 取消收藏
db.session.delete(favorite)
is_favorite = False
else:
# 添加收藏
favorite = Favorite(user_id=current_user.id, item_id=item.id)
db.session.add(favorite)
is_favorite = True
db.session.commit()
return jsonify({
'success': True,
'is_favorite': is_favorite
})
@item_bp.route('/api/items/<int:item_id>/images/<path:filename>', methods=['DELETE'])
@login_required
def api_delete_image(item_id, filename):
"""API: 删除商品图片"""
item = Item.query.get_or_404(item_id)
# 检查是否为商品卖家
if item.seller_id != current_user.id:
return jsonify({
'success': False,
'message': '您没有权限删除此图片'
}), 403
# 从商品图片列表中移除
if item.remove_image(filename):
# 删除实际文件
image_path = os.path.join(current_app.root_path, 'static/uploads/items', filename)
if os.path.exists(image_path):
os.remove(image_path)
db.session.commit()
return jsonify({
'success': True,
'message': '图片已删除'
})
return jsonify({
'success': False,
'message': '图片不存在'
}), 404
app\routes\main.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app
from flask_login import current_user, login_required
from app import db
from app.models.user import User
# 创建主蓝图
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
@main_bp.route('/index')
def index():
"""首页视图"""
return render_template('main/index.html', title='首页')
@main_bp.route('/about')
def about():
"""关于页面"""
return render_template('main/about.html', title='关于我们')
@main_bp.route('/contact')
def contact():
"""联系我们页面"""
return render_template('main/contact.html', title='联系我们')
@main_bp.route('/help')
def help():
"""帮助页面"""
return render_template('main/help.html', title='帮助中心')
@main_bp.before_app_request
def before_request():
"""在每个请求之前执行"""
if current_user.is_authenticated:
current_user.ping() # 更新用户最后访问时间
db.session.commit()
app\routes\order.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, current_app
from flask_login import login_required, current_user
from app import db
from app.models.item import Order, Item, Review
from app.forms.item import ReviewForm
from app.utils.decorators import confirmed_required
order_bp = Blueprint('order', __name__)
@order_bp.route('/orders')
@login_required
def list_orders():
"""订单列表页"""
# 获取查询参数
role = request.args.get('role', 'buyer') # 默认查看作为买家的订单
status = request.args.get('status', 'all') # 默认查看所有状态的订单
# 构建查询
if role == 'buyer':
query = Order.query.filter_by(buyer_id=current_user.id)
else: # seller
query = Order.query.filter_by(seller_id=current_user.id)
# 按状态筛选
if status != 'all':
query = query.filter_by(status=status)
# 按创建时间倒序排序
orders = query.order_by(Order.created_at.desc()).all()
return render_template('order/list.html',
orders=orders,
role=role,
status=status,
title='我的订单')
@order_bp.route('/order/<string:order_number>')
@login_required
def detail(order_number):
"""订单详情页"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 检查是否为订单买家或卖家
if order.buyer_id != current_user.id and order.seller_id != current_user.id:
abort(403)
# 获取商品信息
item = Item.query.get_or_404(order.item_id)
# 获取评价信息
review = Review.query.filter_by(order_id=order.id).first()
# 判断当前用户角色
is_buyer = (order.buyer_id == current_user.id)
return render_template('order/detail.html',
order=order,
item=item,
review=review,
is_buyer=is_buyer,
title='订单详情')
@order_bp.route('/order/<string:order_number>/pay', methods=['POST'])
@login_required
@confirmed_required
def pay_order(order_number):
"""支付订单"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 检查是否为订单买家
if order.buyer_id != current_user.id:
flash('您没有权限支付此订单', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
# 检查订单状态
if order.status != 'pending':
flash('此订单状态不允许支付', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
# 更新订单状态为已支付
order.status = 'paid'
db.session.commit()
flash('支付成功!请联系卖家进行交易', 'success')
return redirect(url_for('order.detail', order_number=order_number))
@order_bp.route('/order/<string:order_number>/cancel', methods=['POST'])
@login_required
def cancel_order(order_number):
"""取消订单"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 检查是否为订单买家或卖家
if order.buyer_id != current_user.id and order.seller_id != current_user.id:
flash('您没有权限取消此订单', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
# 检查订单状态
if order.status not in ['pending', 'paid']:
flash('此订单状态不允许取消', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
# 更新订单状态为已取消
order.status = 'cancelled'
# 将商品重新设为可售状态
item = Item.query.get(order.item_id)
item.is_sold = False
db.session.commit()
flash('订单已取消', 'success')
return redirect(url_for('order.detail', order_number=order_number))
@order_bp.route('/order/<string:order_number>/complete', methods=['POST'])
@login_required
def complete_order(order_number):
"""完成订单"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 检查是否为订单买家
if order.buyer_id != current_user.id:
flash('只有买家可以确认完成订单', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
# 检查订单状态
if order.status != 'paid':
flash('此订单状态不允许确认完成', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
# 更新订单状态为已完成
order.status = 'completed'
db.session.commit()
flash('订单已完成,请对卖家进行评价', 'success')
return redirect(url_for('order.review', order_number=order_number))
@order_bp.route('/order/<string:order_number>/review', methods=['GET', 'POST'])
@login_required
def review(order_number):
"""评价订单"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 检查是否为订单买家
if order.buyer_id != current_user.id:
flash('只有买家可以评价订单', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
# 检查订单状态
if order.status != 'completed':
flash('只有已完成的订单才能评价', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
# 检查是否已评价
existing_review = Review.query.filter_by(order_id=order.id).first()
if existing_review:
flash('您已经评价过此订单', 'info')
return redirect(url_for('order.detail', order_number=order_number))
form = ReviewForm()
if form.validate_on_submit():
# 创建评价
review = Review(
content=form.content.data,
rating=form.rating.data,
order_id=order.id,
reviewer_id=current_user.id,
target_user_id=order.seller_id
)
db.session.add(review)
db.session.commit()
flash('评价提交成功,感谢您的反馈!', 'success')
return redirect(url_for('order.detail', order_number=order_number))
return render_template('order/review.html',
form=form,
order=order,
title='评价订单')
app\routes\transaction.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
from flask_login import login_required, current_user
from app.models.item import Item, Order, Review
from app.models.user import User, Notification
from app.forms.transaction import PaymentForm, DisputeForm
from app import db
import datetime
import uuid
import os
# 创建交易蓝图
transaction = Blueprint('transaction', __name__)
@transaction.route('/payment/<string:order_number>', methods=['GET', 'POST'])
@login_required
def payment(order_number):
"""处理订单支付"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 验证当前用户是否为买家
if order.buyer_id != current_user.id:
flash('您无权访问此页面', 'danger')
return redirect(url_for('main.index'))
# 验证订单状态是否为待支付
if order.status != 'pending':
flash('该订单状态不允许支付', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
form = PaymentForm()
if form.validate_on_submit():
# 模拟支付处理
# 实际项目中应集成第三方支付API
try:
# 更新订单状态
order.status = 'paid'
order.updated_at = datetime.datetime.now()
# 创建通知给卖家
notification = Notification(
user_id=order.seller_id,
title='订单已支付',
content=f'订单 {order.order_number} 已支付,请及时与买家联系。',
notification_type='order_paid'
)
db.session.add(notification)
db.session.commit()
flash('支付成功!', 'success')
return redirect(url_for('order.detail', order_number=order_number))
except Exception as e:
db.session.rollback()
current_app.logger.error(f"支付处理失败: {str(e)}")
flash('支付处理失败,请稍后再试', 'danger')
item = order.item
return render_template('transaction/payment.html', order=order, item=item, form=form)
@transaction.route('/complete/<string:order_number>', methods=['POST'])
@login_required
def complete_transaction(order_number):
"""完成交易"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 验证当前用户是否为买家
if order.buyer_id != current_user.id:
flash('您无权执行此操作', 'danger')
return redirect(url_for('main.index'))
# 验证订单状态是否为已支付
if order.status != 'paid':
flash('该订单状态不允许完成交易', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
try:
# 更新订单状态
order.status = 'completed'
order.updated_at = datetime.datetime.now()
# 更新商品状态为已售出
item = order.item
item.status = 'sold'
# 创建通知给卖家
notification = Notification(
user_id=order.seller_id,
title='交易已完成',
content=f'订单 {order.order_number} 已完成,买家已确认收货。',
notification_type='order_completed'
)
db.session.add(notification)
db.session.commit()
flash('交易已完成!感谢您的购买', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f"完成交易失败: {str(e)}")
flash('操作失败,请稍后再试', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
@transaction.route('/cancel/<string:order_number>', methods=['POST'])
@login_required
def cancel_transaction(order_number):
"""取消交易"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 验证当前用户是否为买家或卖家
if order.buyer_id != current_user.id and order.seller_id != current_user.id:
flash('您无权执行此操作', 'danger')
return redirect(url_for('main.index'))
# 验证订单状态是否允许取消
if order.status not in ['pending', 'paid']:
flash('该订单状态不允许取消', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
try:
# 更新订单状态
order.status = 'cancelled'
order.updated_at = datetime.datetime.now()
# 如果订单已支付,则需要处理退款逻辑
# 实际项目中应集成第三方支付API的退款功能
# 如果是买家取消,通知卖家;如果是卖家取消,通知买家
if current_user.id == order.buyer_id:
notify_user_id = order.seller_id
action_user = "买家"
else:
notify_user_id = order.buyer_id
action_user = "卖家"
notification = Notification(
user_id=notify_user_id,
title='订单已取消',
content=f'订单 {order.order_number} 已被{action_user}取消。',
notification_type='order_cancelled'
)
db.session.add(notification)
db.session.commit()
flash('订单已成功取消', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f"取消交易失败: {str(e)}")
flash('操作失败,请稍后再试', 'danger')
return redirect(url_for('order.detail', order_number=order_number))
@transaction.route('/dispute/<string:order_number>', methods=['GET', 'POST'])
@login_required
def dispute(order_number):
"""提交交易纠纷"""
order = Order.query.filter_by(order_number=order_number).first_or_404()
# 验证当前用户是否为买家或卖家
if order.buyer_id != current_user.id and order.seller_id != current_user.id:
flash('您无权访问此页面', 'danger')
return redirect(url_for('main.index'))
# 验证订单状态是否允许提交纠纷
if order.status not in ['paid', 'completed']:
flash('该订单状态不允许提交纠纷', 'warning')
return redirect(url_for('order.detail', order_number=order_number))
form = DisputeForm()
if form.validate_on_submit():
try:
# 创建纠纷记录
# 实际项目中应有专门的纠纷表
# 更新订单状态
order.status = 'disputed'
order.updated_at = datetime.datetime.now()
# 通知对方
if current_user.id == order.buyer_id:
notify_user_id = order.seller_id
action_user = "买家"
else:
notify_user_id = order.buyer_id
action_user = "卖家"
notification = Notification(
user_id=notify_user_id,
title='订单纠纷',
content=f'订单 {order.order_number} 已被{action_user}提交纠纷申请。',
notification_type='order_disputed'
)
# 通知管理员
admin_users = User.query.filter_by(role='admin').all()
for admin in admin_users:
admin_notification = Notification(
user_id=admin.id,
title='新订单纠纷',
content=f'订单 {order.order_number} 已提交纠纷申请,请尽快处理。',
notification_type='admin_dispute'
)
db.session.add(admin_notification)
db.session.add(notification)
db.session.commit()
flash('纠纷申请已提交,请等待平台处理', 'success')
return redirect(url_for('order.detail', order_number=order_number))
except Exception as e:
db.session.rollback()
current_app.logger.error(f"提交纠纷失败: {str(e)}")
flash('操作失败,请稍后再试', 'danger')
item = order.item
return render_template('transaction/dispute.html', order=order, item=item, form=form)
@transaction.route('/transaction_records')
@login_required
def transaction_records():
"""查看交易记录"""
page = request.args.get('page', 1, type=int)
# 获取用户作为买家的订单
buyer_orders = Order.query.filter_by(buyer_id=current_user.id).order_by(Order.created_at.desc())
# 获取用户作为卖家的订单
seller_orders = Order.query.filter_by(seller_id=current_user.id).order_by(Order.created_at.desc())
# 合并订单并按时间排序
all_orders = buyer_orders.union(seller_orders).order_by(Order.created_at.desc())
# 分页
pagination = all_orders.paginate(page=page, per_page=10, error_out=False)
orders = pagination.items
return render_template('transaction/records.html', orders=orders, pagination=pagination)
@transaction.route('/statistics')
@login_required
def statistics():
"""交易统计"""
# 获取用户作为买家的已完成订单
buyer_completed = Order.query.filter_by(buyer_id=current_user.id, status='completed').count()
# 获取用户作为卖家的已完成订单
seller_completed = Order.query.filter_by(seller_id=current_user.id, status='completed').count()
# 获取用户作为买家的总消费
buyer_total = db.session.query(db.func.sum(Order.price)).filter_by(buyer_id=current_user.id, status='completed').scalar() or 0
# 获取用户作为卖家的总收入
seller_total = db.session.query(db.func.sum(Order.price)).filter_by(seller_id=current_user.id, status='completed').scalar() or 0
# 获取用户最近的交易记录
recent_orders = Order.query.filter(
((Order.buyer_id == current_user.id) | (Order.seller_id == current_user.id)) &
(Order.status == 'completed')
).order_by(Order.updated_at.desc()).limit(5).all()
# 获取用户收到的评价
reviews = Review.query.filter_by(target_user_id=current_user.id).order_by(Review.created_at.desc()).limit(5).all()
# 计算平均评分
avg_rating = db.session.query(db.func.avg(Review.rating)).filter_by(target_user_id=current_user.id).scalar() or 0
return render_template(
'transaction/statistics.html',
buyer_completed=buyer_completed,
seller_completed=seller_completed,
buyer_total=buyer_total,
seller_total=seller_total,
recent_orders=recent_orders,
reviews=reviews,
avg_rating=avg_rating
)
app\routes\user.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
import os
from datetime import datetime
from app import db
from app.models.user import User
from app.forms.user import EditProfileForm, ChangePasswordForm
from app.utils.decorators import check_confirmed
# 创建用户蓝图
user_bp = Blueprint('user', __name__, url_prefix='/user')
@user_bp.route('/profile/<username>')
def profile(username):
"""用户个人资料页面"""
user = User.query.filter_by(username=username).first_or_404()
# 获取用户发布的物品
page = request.args.get('page', 1, type=int)
items = user.items.order_by(Item.created_at.desc()).paginate(
page=page,
per_page=current_app.config['ITEMS_PER_PAGE'],
error_out=False
)
return render_template('user/profile.html', user=user, items=items)
@user_bp.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
"""编辑个人资料"""
form = EditProfileForm()
if form.validate_on_submit():
# 更新用户信息
current_user.username = form.username.data
current_user.phone = form.phone.data
current_user.bio = form.bio.data
current_user.dormitory = form.dormitory.data
# 处理头像上传
if form.avatar.data:
# 检查文件类型
filename = secure_filename(form.avatar.data.filename)
if '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']:
# 保存头像
current_user.save_avatar(form.avatar.data)
else:
flash('不支持的文件类型', 'danger')
db.session.commit()
flash('个人资料已更新', 'success')
return redirect(url_for('user.profile', username=current_user.username))
# 预填表单
elif request.method == 'GET':
form.username.data = current_user.username
form.phone.data = current_user.phone
form.bio.data = current_user.bio
form.dormitory.data = current_user.dormitory
return render_template('user/edit_profile.html', title='编辑个人资料', form=form)
@user_bp.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
"""修改密码"""
form = ChangePasswordForm()
if form.validate_on_submit():
# 验证当前密码
if not current_user.verify_password(form.current_password.data):
flash('当前密码不正确', 'danger')
return redirect(url_for('user.change_password'))
# 更新密码
current_user.password = form.new_password.data
db.session.commit()
flash('密码已更新', 'success')
return redirect(url_for('user.profile', username=current_user.username))
return render_template('user/change_password.html', title='修改密码', form=form)
@user_bp.route('/dashboard')
@login_required
def dashboard():
"""用户控制面板"""
# 获取用户发布的物品
user_items = current_user.items.order_by(Item.created_at.desc()).limit(5).all()
# 获取用户的购买订单
user_orders = current_user.orders.order_by(Order.created_at.desc()).limit(5).all()
# 获取用户的销售订单
sold_orders = current_user.sold_orders.order_by(Order.created_at.desc()).limit(5).all()
# 获取用户的未读消息数
unread_messages_count = current_user.messages_received.filter_by(is_read=False).count()
# 获取用户的收藏物品
favorites = current_user.favorites.join(Item).filter(Item.is_active==True).limit(5).all()
return render_template('user/dashboard.html',
title='用户控制面板',
user_items=user_items,
user_orders=user_orders,
sold_orders=sold_orders,
unread_messages_count=unread_messages_count,
favorites=favorites)
@user_bp.route('/notifications')
@login_required
def notifications():
"""用户通知页面"""
# 标记所有通知为已读
current_user.last_notification_read_time = datetime.utcnow()
db.session.commit()
# 获取通知
page = request.args.get('page', 1, type=int)
notifications = current_user.notifications.order_by(Notification.timestamp.desc()).paginate(
page=page,
per_page=current_app.config['ITEMS_PER_PAGE'],
error_out=False
)
return render_template('user/notifications.html',
title='我的通知',
notifications=notifications)
app\static\css\style.css
/* 全局样式 */
:root {
--primary-color: #3490dc;
--secondary-color: #6c757d;
--success-color: #38c172;
--danger-color: #e3342f;
--warning-color: #ffed4a;
--info-color: #6cb2eb;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
body {
font-family: 'Nunito', 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f8fa;
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.content-wrapper {
flex: 1;
}
/* 导航栏样式 */
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
}
.navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-link {
font-weight: 600;
}
.dropdown-item:active {
background-color: var(--primary-color);
}
/* 卡片样式 */
.card {
border: none;
border-radius: 0.5rem;
transition: transform 0.3s, box-shadow 0.3s;
}
.card-header {
border-top-left-radius: 0.5rem !important;
border-top-right-radius: 0.5rem !important;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.shadow {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
}
/* 按钮样式 */
.btn {
font-weight: 600;
border-radius: 0.25rem;
transition: all 0.3s;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: #2779bd;
border-color: #2779bd;
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
color: white;
}
/* 表单样式 */
.form-control {
border-radius: 0.25rem;
border: 1px solid #ddd;
padding: 0.75rem 1rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(52, 144, 220, 0.25);
}
.form-label {
font-weight: 600;
}
/* 头像样式 */
.avatar-small {
width: 40px;
height: 40px;
object-fit: cover;
}
.avatar-medium {
width: 100px;
height: 100px;
object-fit: cover;
}
.avatar-large {
width: 150px;
height: 150px;
object-fit: cover;
}
/* 商品卡片样式 */
.item-card {
transition: transform 0.3s, box-shadow 0.3s;
}
.item-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.item-thumbnail {
height: 200px;
object-fit: cover;
}
/* 分类图标样式 */
.category-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transition: all 0.3s;
}
.category-link:hover .category-icon {
background-color: var(--primary-color);
color: white;
}
/* 页脚样式 */
.footer {
background-color: #343a40;
color: #f8f9fa;
padding: 2rem 0;
margin-top: 3rem;
}
.footer a {
color: #f8f9fa;
text-decoration: none;
}
.footer a:hover {
color: var(--primary-color);
}
.footer-heading {
font-weight: 700;
margin-bottom: 1.5rem;
}
/* 响应式调整 */
@media (max-width: 767.98px) {
.item-thumbnail {
height: 180px;
}
.avatar-medium {
width: 80px;
height: 80px;
}
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 通知样式 */
.notification-badge {
position: absolute;
top: 0;
right: 0;
background-color: var(--danger-color);
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.7rem;
display: flex;
align-items: center;
justify-content: center;
}
/* 评分样式 */
.star-rating .fa-star {
color: #ccc;
}
.star-rating .fa-star.checked {
color: #ffc107;
}
/* 标签样式 */
.tag {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
/* 加载动画 */
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
app\templates\base.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}校园二手物品交易平台{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
{% block styles %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<i class="fas fa-exchange-alt"></i> 校园二手交易
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">首页</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="#">浏览商品</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">发布商品</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.about') }}">关于我们</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.help') }}">帮助中心</a>
</li>
</ul>
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
<img src="{{ current_user.get_avatar_url() }}" alt="头像" class="avatar-small rounded-circle me-1">
{{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('user.dashboard') }}">
<i class="fas fa-tachometer-alt me-2"></i>控制面板
</a></li>
<li><a class="dropdown-item" href="{{ url_for('user.profile', username=current_user.username) }}">
<i class="fas fa-user me-2"></i>个人资料
</a></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-shopping-bag me-2"></i>我的商品
</a></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-heart me-2"></i>我的收藏
</a></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-envelope me-2"></i>我的消息
{% if current_user.messages_received.filter_by(is_read=False).count() > 0 %}
<span class="badge bg-danger">{{ current_user.messages_received.filter_by(is_read=False).count() }}</span>
{% endif %}
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt me-2"></i>退出登录
</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">注册</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- 消息提示 -->
<div class="container mt-3">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- 主要内容 -->
<main class="container mt-4">
{% block content %}{% endblock %}
</main>
<!-- 页脚 -->
<footer class="bg-light text-center text-lg-start mt-5">
<div class="container p-4">
<div class="row">
<div class="col-lg-6 col-md-12 mb-4 mb-md-0">
<h5 class="text-uppercase">校园二手物品交易平台</h5>
<p>
我们致力于为校园师生提供一个安全、便捷、高效的二手物品交易平台,
让闲置资源得到充分利用,促进校园资源的循环利用。
</p>
</div>
<div class="col-lg-3 col-md-6 mb-4 mb-md-0">
<h5 class="text-uppercase">链接</h5>
<ul class="list-unstyled mb-0">
<li><a href="{{ url_for('main.about') }}" class="text-dark">关于我们</a></li>
<li><a href="{{ url_for('main.help') }}" class="text-dark">帮助中心</a></li>
<li><a href="{{ url_for('main.contact') }}" class="text-dark">联系我们</a></li>
<li><a href="#" class="text-dark">使用条款</a></li>
</ul>
</div>
<div class="col-lg-3 col-md-6 mb-4 mb-md-0">
<h5 class="text-uppercase">联系我们</h5>
<ul class="list-unstyled mb-0">
<li><i class="fas fa-envelope me-2"></i>contact@campus-trading.com</li>
<li><i class="fas fa-phone me-2"></i>(+86) 123-4567-8901</li>
<li><i class="fas fa-map-marker-alt me-2"></i>中国某大学</li>
</ul>
</div>
</div>
</div>
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.05);">
© 2025 校园二手物品交易平台 - 保留所有权利
</div>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- Custom JS -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
app\templates\auth\login.html
{% extends "base.html" %}
{% block title %}登录 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-sign-in-alt me-2"></i>用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.login') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{% if form.email.errors %}
{{ form.email(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.email(class="form-control", placeholder="请输入您的邮箱") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control", placeholder="请输入您的密码") }}
{% endif %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between">
<a href="{{ url_for('auth.reset_password_request') }}">忘记密码?</a>
<a href="{{ url_for('auth.register') }}">没有账号?立即注册</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\auth\register.html
{% extends "base.html" %}
{% block title %}注册 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-user-plus me-2"></i>用户注册</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.register') }}">
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
{{ form.username.label(class="form-label") }}
{% if form.username.errors %}
{{ form.username(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.username(class="form-control", placeholder="请输入用户名") }}
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.email.label(class="form-label") }}
{% if form.email.errors %}
{{ form.email(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.email(class="form-control", placeholder="请输入邮箱") }}
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.student_id.label(class="form-label") }}
{% if form.student_id.errors %}
{{ form.student_id(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.student_id.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.student_id(class="form-control", placeholder="请输入学号") }}
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.phone.label(class="form-label") }}
{% if form.phone.errors %}
{{ form.phone(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.phone.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.phone(class="form-control", placeholder="请输入手机号") }}
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.password.label(class="form-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control", placeholder="请输入密码") }}
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.confirm_password.label(class="form-label") }}
{% if form.confirm_password.errors %}
{{ form.confirm_password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.confirm_password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.confirm_password(class="form-control", placeholder="请确认密码") }}
{% endif %}
</div>
</div>
<div class="mb-3">
<div class="form-text">
<small>注册即表示您同意我们的<a href="#">服务条款</a>和<a href="#">隐私政策</a></small>
</div>
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
已有账号?<a href="{{ url_for('auth.login') }}">立即登录</a>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\auth\reset_password.html
{% extends "base.html" %}
{% block title %}重置密码 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-key me-2"></i>设置新密码</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.reset_password', token=token) }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control", placeholder="请输入新密码") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{% if form.confirm_password.errors %}
{{ form.confirm_password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.confirm_password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.confirm_password(class="form-control", placeholder="请确认新密码") }}
{% endif %}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\auth\reset_password_request.html
{% extends "base.html" %}
{% block title %}重置密码 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-key me-2"></i>重置密码</h4>
</div>
<div class="card-body">
<p class="card-text">请输入您的注册邮箱,我们将发送密码重置链接到您的邮箱。</p>
<form method="POST" action="{{ url_for('auth.reset_password_request') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{% if form.email.errors %}
{{ form.email(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.email(class="form-control", placeholder="请输入您的邮箱") }}
{% endif %}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('auth.login') }}">返回登录</a>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\email\reset_password.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码 - 校园二手物品交易平台</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #3490dc;
}
.content {
background-color: #f8f9fa;
border-radius: 5px;
padding: 30px;
margin-bottom: 20px;
}
.button {
display: inline-block;
background-color: #3490dc;
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 5px;
margin-top: 15px;
margin-bottom: 15px;
}
.footer {
font-size: 12px;
color: #6c757d;
text-align: center;
margin-top: 30px;
border-top: 1px solid #e9ecef;
padding-top: 20px;
}
</style>
</head>
<body>
<div class="header">
<div class="logo">校园二手物品交易平台</div>
</div>
<div class="content">
<h2>密码重置请求</h2>
<p>您好,{{ user.username }}!</p>
<p>我们收到了您的密码重置请求。如果这不是您本人操作,请忽略此邮件。</p>
<p>要重置您的密码,请点击下面的按钮:</p>
<div style="text-align: center;">
<a href="{{ reset_link }}" class="button">重置密码</a>
</div>
<p>或者,您可以复制以下链接到浏览器地址栏:</p>
<p style="word-break: break-all; font-size: 14px;">{{ reset_link }}</p>
<p>此链接将在 <strong>1小时</strong> 后失效。</p>
<p>祝您使用愉快!</p>
<p>校园二手物品交易平台团队</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿直接回复。</p>
<p>© {{ year }} 校园二手物品交易平台 - 保留所有权利</p>
</div>
</body>
</html>
app\templates\email\reset_password.txt
校园二手物品交易平台 - 密码重置
您好,{{ user.username }}!
我们收到了您的密码重置请求。如果这不是您本人操作,请忽略此邮件。
要重置您的密码,请访问以下链接:
{{ reset_link }}
此链接将在1小时后失效。
祝您使用愉快!
校园二手物品交易平台团队
---
此邮件由系统自动发送,请勿直接回复。
© {{ year }} 校园二手物品交易平台 - 保留所有权利