目录
-
项目概述\](#项目概述)
-
数据库模型\](#数据库模型)
-
个人中心实现\](#个人中心实现)
-
API路由详解\](#API路由详解)
项目概述
本教程将基于您提供的Flask代码,详细讲解如何实现论坛与个人中心功能。您的代码已经包含了用户认证、帖子管理、评论系统、点赞收藏等核心功能,我们将在此基础上进行完善和扩展。
项目结构
your_flask_app/
├── app.py # 主应用文件
├── requirements.txt # 依赖包列表
├── migrations/ # 数据库迁移文件夹(由Flask-Migrate生成)
├── static/ # 静态文件目录
│ ├── css/ # 样式文件
│ ├── js/ # JavaScript文件
│ ├── images/ # 图片资源
│ └── avatars/ # 用户头像存储
└── templates/ # 模板文件目录
├── base.html # 基础模板
├── index.html # 首页
├── forum.html # 论坛页面
├── post.html # 帖子详情页
├── profile.html # 个人中心页
├── user_profile.html # 其他用户资料页
├── login.html # 登录页面
├── register.html # 注册页面
└── admin.html # 管理员页面
数据库模型
python
# 用户模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
register_time = db.Column(db.DateTime, default=datetime.utcnow)
last_login_time = db.Column(db.DateTime)
failed_login_count = db.Column(db.Integer, default=0)
locked = db.Column(db.Boolean, default=False)
_avatar_url = db.Column('avatar_url', db.String(200), default='default.png')
signature = db.Column(db.Text, default="这个人很懒,还没写个性签名~")
is_admin = db.Column(db.Boolean, default=False)
# 帖子模型
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
time = db.Column(db.DateTime, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author = db.relationship('User', backref=db.backref('posts', lazy='dynamic'))
likes = db.Column(db.Integer, default=0)
views = db.Column(db.Integer, default=0)
# 评论模型
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
time = db.Column(db.DateTime, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
author = db.relationship('User', backref=db.backref('comments', lazy='dynamic'))
post = db.relationship('Post', backref=db.backref('comments', lazy='dynamic', cascade="all, delete-orphan"))
# 收藏模型
class Favorite(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
time = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref='favorites')
post = db.relationship('Post', backref='favorited_by')
# 点赞模型
class Like(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
time = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', backref='likes')
post = db.relationship('Post', backref='post_likes')
论坛功能实现
- 论坛首页路由
python
@app.route('/forum.html')
def forum():
user = get_current_user()
# 获取当前页码,默认第1页,类型为整数
page = request.args.get('page', 1, type=int)
# 每页显示 5 个帖子
per_page = 5
# 按时间倒序排列,并进行分页查询
posts_pagination = Post.query.order_by(Post.time.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return render_template(
'forum.html',
current_user=user,
posts=posts_pagination.items, # 当前页的帖子列表
pagination=posts_pagination # 分页对象,用于前端生成分页导航
)
- 帖子详情页
python
@app.route('/post/<int:post_id>')
def post_detail(post_id):
current_user = get_current_user()
post = Post.query.get_or_404(post_id)
post.views = (post.views or 0) + 1
db.session.commit()
comments = Comment.query.filter_by(post_id=post_id).order_by(Comment.time.desc()).all()
author = post.author
author_posts_count = author.posts.count() if author else 0
author_comments_count = author.comments.count() if author else 0
is_favorite = False
if current_user:
is_favorite = Favorite.query.filter_by(
user_id=current_user.id, post_id=post_id
).first() is not None
is_liked = False
if current_user:
is_liked = Like.query.filter_by(
user_id=current_user.id, post_id=post_id
).first() is not None
# 计算该帖子总共被点赞多少次
like_count = Like.query.filter_by(post_id=post_id).count()
return render_template(
'post.html',
post=post,
comments=comments,
current_user=current_user,
is_favorite=is_favorite,
is_liked=is_liked,
author_posts_count=author_posts_count,
author_comments_count=author_comments_count,
like_count=like_count
)
- 发布帖子API
python
@app.route('/forum/posts', methods=['POST'])
def create_post():
try:
app.logger.debug(f"收到发帖请求: {request.method} {request.path}")
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
data = request.get_json() or request.form.to_dict()
if not data.get('title') or not data.get('content'):
return jsonify({'error': '标题和内容不能为空'}), 400
user = User.query.get(session['user_id'])
new_post = Post(
title=data['title'],
content=data['content'],
author_id=user.id
)
db.session.add(new_post)
db.session.commit()
return jsonify({
'success': True,
'message': '帖子发布成功',
'post_id': new_post.id
}), 201
except Exception as e:
db.session.rollback()
app.logger.error(f"发布帖子失败: {str(e)}", exc_info=True)
return jsonify({'error': '发布失败,请重试'}), 500
个人中心实现
- 个人资料页路由
python
@app.route('/profile.html')
def profile():
user = get_current_user()
if not user:
return redirect(url_for('login'))
# 修改计数方式
post_count = user.posts.count() if user.posts else 0
comment_count = user.comments.count() if user.comments else 0
favorite_count = Favorite.query.filter_by(user_id=user.id).count()
like_count = Like.query.filter_by(user_id=user.id).count() if user else 0
# 查询该用户发布过的所有帖子(按时间倒序)
user_posts = Post.query.filter_by(author_id=user.id).order_by(Post.time.desc()).all()
# 查询该用户收藏的所有帖子
favorite_posts = []
if user.favorites:
favorite_post_ids = [f.post_id for f in user.favorites]
favorite_posts = Post.query.filter(Post.id.in_(favorite_post_ids)).order_by(Post.time.desc()).all()
return render_template('profile.html',
current_user=user,
post_count=post_count,
comment_count=comment_count,
favorite_count=favorite_count,
like_count=like_count,
user_posts=user_posts,
favorite_posts=favorite_posts
)
- 查看其他用户资料
python
@app.route('/user_profile.html')
def user_profile():
# 从 URL 参数中获取 user_id
user_id = request.args.get('user_id')
if not user_id:
# 如果没有传 user_id,可以默认显示当前用户,或者跳转到首页/报错
user = get_current_user()
if not user:
return redirect(url_for('login'))
return render_template('user_profile.html', user=user)
# 根据 user_id 查询数据库中的用户
try:
user_id = int(user_id) # 确保是整数
except (TypeError, ValueError):
return render_template('404.html', error="无效的用户ID"), 404
user = User.query.get(user_id)
if not user:
return render_template('404.html', error="用户不存在"), 404
# 查询该用户发布的所有帖子,并按时间倒序排列
user_posts = Post.query.filter_by(author_id=user.id).order_by(Post.time.desc()).all()
return render_template('user_profile.html', user=user, user_posts=user_posts)
- 更新个人资料API
python
@app.route('/api/update_profile', methods=['POST'])
def update_profile():
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
user = User.query.get(session['user_id'])
if not user:
return jsonify({'error': '用户不存在'}), 404
data = request.get_json()
if not data or 'signature' not in data:
return jsonify({'error': '签名内容不能为空'}), 400
new_signature = data['signature'].strip()
if not new_signature:
return jsonify({'error': '签名内容不能为空'}), 400
user.signature = new_signature
db.session.commit()
return jsonify({
'success': True,
'user': {
'id': user.id,
'username': user.username,
'signature': user.signature
}
})
- 上传头像API
python
@app.route('/api/upload_avatar', methods=['POST'])
def upload_avatar():
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
user = User.query.get(session['user_id'])
if not user:
return jsonify({'error': '用户不存在'}), 404
if 'avatar' not in request.files:
return jsonify({'error': '未选择文件'}), 400
file = request.files['avatar']
if file.filename == '':
return jsonify({'error': '未选择文件'}), 400
if not allowed_file(file.filename):
return jsonify({'error': '不支持的文件格式,请上传 PNG、JPG、JPEG 或 GIF'}), 400
filename = secure_filename(file.filename)
# 防止文件名冲突,使用 uuid
unique_filename = f"{uuid.uuid4().hex}_{filename}"
static_dir = os.path.join(base_dir, 'static')
filepath = os.path.join(static_dir, 'avatars', unique_filename)
try:
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
file.save(filepath)
except Exception as e:
app.logger.error(f"保存头像失败: {e}")
return jsonify({'error': '头像保存失败'}), 500
# 更新用户的头像URL
user.avatar_url = unique_filename # 直接存文件名
db.session.commit()
avatar_url = url_for('static', filename=f'avatars/{unique_filename}')
return jsonify({
'success': True,
'avatar_url': avatar_url
})
前端模板示例
- 论坛页面 (forum.html)
html
{% extends "base.html" %}
{% block title %}论坛 - OurCraft{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>社区论坛</h2>
{% if current_user %}
<a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createPostModal">
<i class="fas fa-plus"></i> 发布新帖
</a>
{% endif %}
</div>
{% if posts %}
<div class="post-list">
{% for post in posts %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h5>
<div class="d-flex justify-content-between text-muted small mb-2">
<div>
<a href="{{ url_for('user_profile') }}?user_id={{ post.author.id }}" class="text-decoration-none">
<img src="{{ post.author.safe_avatar }}" alt="头像" class="rounded-circle me-1" width="20" height="20">
{{ post.author.username }}
</a>
· {{ post.time|datetimeformat }}
</div>
<div>
<span class="me-2"><i class="far fa-eye"></i> {{ post.views }}</span>
<span class="me-2"><i class="far fa-comment"></i> {{ post.comments.count() }}</span>
<span><i class="far fa-heart"></i> {{ post.likes }}</span>
</div>
</div>
<p class="card-text">{{ post.content|truncate(150) }}</p>
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">阅读更多</a>
</div>
</div>
{% endfor %}
</div>
<!-- 分页导航 -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('forum', page=pagination.prev_num) }}" aria-label="Previous">
<span aria-hidden="true"><<</span>
</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('forum', page=page_num) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('forum', page=pagination.next_num) }}" aria-label="Next">
<span aria-hidden="true">>></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% else %}
<div class="text-center py-5">
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无帖子,成为第一个发帖的人吧!</p>
{% if current_user %}
<a href="#" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createPostModal">
<i class="fas fa-plus"></i> 发布新帖
</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-primary">登录后发帖</a>
{% endif %}
</div>
{% endif %}
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">论坛统计</div>
<div class="card-body">
<p>总帖子数: {{ Post.query.count() }}</p>
<p>总评论数: {{ Comment.query.count() }}</p>
<p>注册用户: {{ User.query.count() }}</p>
</div>
</div>
<div class="card mt-3">
<div class="card-header">热门帖子</div>
<div class="list-group list-group-flush">
{% for post in Post.query.order_by(Post.views.desc()).limit(5).all() %}
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">{{ post.title|truncate(20) }}</h6>
<small>{{ post.views }} 浏览</small>
</div>
<small class="text-muted">by {{ post.author.username }}</small>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- 发布帖子模态框 -->
{% if current_user %}
<div class="modal fade" id="createPostModal" tabindex="-1" aria-labelledby="createPostModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createPostModalLabel">发布新帖子</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="createPostForm">
<div class="mb-3">
<label for="postTitle" class="form-label">标题</label>
<input type="text" class="form-control" id="postTitle" name="title" required>
</div>
<div class="mb-3">
<label for="postContent" class="form-label">内容</label>
<textarea class="form-control" id="postContent" name="content" rows="10" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="submitPost">发布</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{% if current_user %}
<script>
$(document).ready(function() {
$('#submitPost').click(function() {
const title = $('#postTitle').val();
const content = $('#postContent').val();
if (!title || !content) {
alert('标题和内容不能为空');
return;
}
$.ajax({
url: '{{ url_for("create_post") }}',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
title: title,
content: content
}),
success: function(response) {
if (response.success) {
$('#createPostModal').modal('hide');
alert('帖子发布成功');
window.location.reload();
} else {
alert('发布失败: ' + response.error);
}
},
error: function(xhr) {
alert('发布失败,请重试');
}
});
});
});
</script>
{% endif %}
{% endblock %}
- 个人中心页面 (profile.html)
html
{% extends "base.html" %}
{% block title %}个人中心 - OurCraft{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row">
<!-- 左侧个人信息 -->
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<img src="{{ current_user.safe_avatar }}" alt="头像" class="rounded-circle mb-3" width="120" height="120">
<h4>{{ current_user.username }}</h4>
<p class="text-muted">{{ current_user.signature }}</p>
<div class="d-flex justify-content-center mb-3">
<form id="avatarForm" enctype="multipart/form-data" class="d-none">
<input type="file" id="avatarInput" name="avatar" accept="image/*">
</form>
<button class="btn btn-outline-primary btn-sm me-2" onclick="$('#avatarInput').click()">
<i class="fas fa-camera"></i> 更换头像
</button>
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#editProfileModal">
<i class="fas fa-edit"></i> 编辑资料
</button>
</div>
<div class="row text-center">
<div class="col-4">
<h5>{{ post_count }}</h5>
<small class="text-muted">帖子</small>
</div>
<div class="col-4">
<h5>{{ comment_count }}</h5>
<small class="text-muted">评论</small>
</div>
<div class="col-4">
<h5>{{ favorite_count }}</h5>
<small class="text-muted">收藏</small>
</div>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">账号管理</div>
<div class="list-group list-group-flush">
<a href="#" class="list-group-item list-group-item-action" data-bs-toggle="modal" data-bs-target="#changePasswordModal">
<i class="fas fa-key me-2"></i>修改密码
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_page') }}" class="list-group-item list-group-item-action">
<i class="fas fa-crown me-2"></i>管理员面板
</a>
{% endif %}
<a href="{{ url_for('logout') }}" class="list-group-item list-group-item-action text-danger">
<i class="fas fa-sign-out-alt me-2"></i>退出登录
</a>
</div>
</div>
</div>
<!-- 右侧内容 -->
<div class="col-md-8">
<ul class="nav nav-tabs" id="profileTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="posts-tab" data-bs-toggle="tab" data-bs-target="#posts" type="button" role="tab">
我的帖子 ({{ user_posts|length }})
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="favorites-tab" data-bs-toggle="tab" data-bs-target="#favorites" type="button" role="tab">
我的收藏 ({{ favorite_posts|length }})
</button>
</li>
</ul>
<div class="tab-content mt-3" id="profileTabsContent">
<!-- 我的帖子 -->
<div class="tab-pane fade show active" id="posts" role="tabpanel">
{% if user_posts %}
<div class="list-group">
{% for post in user_posts %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h5>
<small>{{ post.time|datetimeformat }}</small>
</div>
<p class="mb-1">{{ post.content|truncate(100) }}</p>
<div class="d-flex justify-content-between align-items-center mt-2">
<small class="text-muted">
<i class="far fa-eye"></i> {{ post.views }} ·
<i class="far fa-comment"></i> {{ post.comments.count() }} ·
<i class="far fa-heart"></i> {{ post.likes }}
</small>
<div>
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">查看</a>
<button class="btn btn-sm btn-outline-danger delete-post" data-post-id="{{ post.id }}">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-file-alt fa-3x text-muted mb-3"></i>
<p class="text-muted">您还没有发布过帖子</p>
<a href="{{ url_for('forum') }}" class="btn btn-primary">去发帖</a>
</div>
{% endif %}
</div>
<!-- 我的收藏 -->
<div class="tab-pane fade" id="favorites" role="tabpanel">
{% if favorite_posts %}
<div class="list-group">
{% for post in favorite_posts %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="text-decoration-none">
{{ post.title }}
</a>
</h5>
<small>{{ post.time|datetimeformat }}</small>
</div>
<p class="mb-1">{{ post.content|truncate(100) }}</p>
<div class="d-flex justify-content-between align-items-center mt-2">
<small class="text-muted">
作者: <a href="{{ url_for('user_profile') }}?user_id={{ post.author.id }}">{{ post.author.username }}</a> ·
<i class="far fa-eye"></i> {{ post.views }} ·
<i class="far fa-comment"></i> {{ post.comments.count() }}
</small>
<div>
<a href="{{ url_for('post_detail', post_id=post.id) }}" class="btn btn-sm btn-outline-primary">查看</a>
<button class="btn btn-sm btn-outline-warning unfavorite-post" data-post-id="{{ post.id }}">
<i class="fas fa-star"></i> 取消收藏
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-star fa-3x text-muted mb-3"></i>
<p class="text-muted">您还没有收藏任何帖子</p>
<a href="{{ url_for('forum') }}" class="btn btn-primary">去发现</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 编辑资料模态框 -->
<div class="modal fade" id="editProfileModal" tabindex="-1" aria-labelledby="editProfileModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editProfileModalLabel">编辑个人资料</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editProfileForm">
<div class="mb-3">
<label for="profileSignature" class="form-label">个性签名</label>
<textarea class="form-control" id="profileSignature" name="signature" rows="3">{{ current_user.signature }}</textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveProfile">保存</button>
</div>
</div>
</div>
</div>
<!-- 修改密码模态框 -->
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">修改密码</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="changePasswordForm">
<div class="mb-3">
<label for="currentPassword" class="form-label">当前密码</label>
<input type="password" class="form-control" id="currentPassword" name="old_password" required>
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">新密码</label>
<input type="password" class="form-control" id="newPassword" name="new_password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">确认新密码</label>
<input type="password" class="form-control" id="confirmPassword" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="savePassword">保存</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// 头像上传
$('#avatarInput').change(function() {
if (this.files && this.files[0]) {
const formData = new FormData($('#avatarForm')[0]);
$.ajax({
url: '{{ url_for("upload_avatar") }}',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
alert('头像上传成功');
window.location.reload();
} else {
alert('上传失败: ' + response.error);
}
},
error: function() {
alert('上传失败,请重试');
}
});
}
});
// 保存个人资料
$('#saveProfile').click(function() {
const signature = $('#profileSignature').val().trim();
if (!signature) {
alert('个性签名不能为空');
return;
}
$.ajax({
url: '{{ url_for("update_profile") }}',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ signature: signature }),
success: function(response) {
if (response.success) {
$('#editProfileModal').modal('hide');
alert('资料更新成功');
window.location.reload();
} else {
alert('更新失败: ' + response.error);
}
},
error: function() {
alert('更新失败,请重试');
}
});
});
// 修改密码
$('#savePassword').click(function() {
const oldPassword = $('#currentPassword').val();
const newPassword = $('#newPassword').val();
const confirmPassword = $('#confirmPassword').val();
if (!oldPassword || !newPassword) {
alert('密码不能为空');
return;
}
if (newPassword !== confirmPassword) {
alert('两次输入的新密码不一致');
return;
}
if (newPassword.length < 6) {
alert('新密码长度不能少于6位');
return;
}
$.ajax({
url: '{{ url_for("update_password") }}',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
old_password: oldPassword,
new_password: newPassword
}),
success: function(response) {
if (response.success) {
$('#changePasswordModal').modal('hide');
alert(response.message);
window.location.href = '{{ url_for("logout") }}';
} else {
alert('修改失败: ' + response.error);
}
},
error: function() {
alert('修改失败,请重试');
}
});
});
// 删除帖子
$('.delete-post').click(function() {
const postId = $(this).data('post-id');
if (confirm('确定要删除这个帖子吗?此操作不可恢复。')) {
$.ajax({
url: '/forum/posts/' + postId,
type: 'DELETE',
success: function(response) {
if (response.success) {
alert('帖子已删除');
window.location.reload();
} else {
alert('删除失败: ' + response.error);
}
},
error: function() {
alert('删除失败,请重试');
}
});
}
});
// 取消收藏
$('.unfavorite-post').click(function() {
const postId = $(this).data('post-id');
$.ajax({
url: '/api/favorite/' + postId,
type: 'DELETE',
success: function(response) {
if (response.success) {
alert('已取消收藏');
window.location.reload();
} else {
alert('操作失败: ' + response.error);
}
},
error: function() {
alert('操作失败,请重试');
}
});
});
});
</script>
{% endblock %}
API路由详解
- 点赞与收藏功能
python
# 收藏帖子
@app.route('/api/favorite/<int:post_id>', methods=['POST', 'DELETE'])
def toggle_favorite(post_id):
try:
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
post = Post.query.get(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
user = User.query.get(session['user_id'])
favorite = Favorite.query.filter_by(user_id=user.id, post_id=post_id).first()
is_favorite = False
if request.method == 'POST' and not favorite:
new_favorite = Favorite(user_id=user.id, post_id=post_id)
db.session.add(new_favorite)
is_favorite = True
elif request.method == 'DELETE' and favorite:
db.session.delete(favorite)
is_favorite = False
db.session.commit()
favorite_count = Favorite.query.filter_by(post_id=post_id).count()
return jsonify({
'success': True,
'is_favorite': is_favorite,
'count': favorite_count
}), 200
except Exception as e:
db.session.rollback()
app.logger.error(f"收藏操作失败: {str(e)}")
return jsonify({'error': '操作失败,请重试'}), 500
# 点赞帖子
@app.route('/api/like/<int:post_id>', methods=['POST', 'DELETE'])
def toggle_like(post_id):
# 必须登录才能点赞
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
user = User.query.get(session['user_id'])
if not user:
return jsonify({'error': '用户不存在'}), 404
post = Post.query.get(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
# 查找是否已经点过赞
like = Like.query.filter_by(user_id=user.id, post_id=post_id).first()
is_liked = False
if request.method == 'POST' and not like:
# 点赞:新增记录
new_like = Like(user_id=user.id, post_id=post_id)
db.session.add(new_like)
is_liked = True
elif request.method == 'DELETE' and like:
# 取消点赞:删除记录
db.session.delete(like)
is_liked = False
db.session.commit()
# 获取该帖子的总点赞数
like_count = Like.query.filter_by(post_id=post_id).count()
return jsonify({
'success': True,
'is_liked': is_liked,
'like_count': like_count
})
- 评论功能
python
# 发布评论
@app.route('/forum/posts/<int:post_id>/comments', methods=['POST'])
def create_comment(post_id):
try:
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
data = request.get_json() or request.form.to_dict()
content = data.get('content', '').strip()
if not content:
return jsonify({'error': '评论内容不能为空'}), 400
post = Post.query.get(post_id)
if not post:
return jsonify({'error': '帖子不存在'}), 404
user = User.query.get(session['user_id'])
new_comment = Comment(
content=content,
author_id=user.id,
post_id=post_id
)
db.session.add(new_comment)
db.session.commit()
return jsonify({
'success': True,
'message': '评论发布成功',
'comment': {
'id': new_comment.id,
'content': new_comment.content,
'user_id': user.id,
'user_name': user.username,
'user_avatar': user.avatar_url,
'create_time': new_comment.time.strftime('%Y-%m-%d %H:%M:%S')
}
}), 201
except Exception as e:
db.session.rollback()
app.logger.error(f"发布评论失败: {str(e)}")
return jsonify({'error': '评论失败,请重试'}), 500
# 删除评论
@app.route('/forum/comments/<int:comment_id>', methods=['DELETE'])
def delete_comment(comment_id):
try:
if 'user_id' not in session:
return jsonify({'error': '请先登录'}), 401
comment = Comment.query.get(comment_id)
if not comment:
return jsonify({'error': '评论不存在'}), 404
current_user = User.query.get(session['user_id'])
# 管理员可删除任意评论
if comment.author_id != current_user.id and not current_user.is_admin:
return jsonify({'error': '没有权限删除此评论'}), 403
db.session.delete(comment)
db.session.commit()
return jsonify({
'success': True,
'message': '评论已删除'
}), 200
except Exception as e:
db.session.rollback()
app.logger.error(f"删除评论失败: {str(e)}")
return jsonify({'error': '删除评论失败,请重试'}), 500
部署与运行
- 安装依赖
创建 `requirements.txt` 文件:
bash
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Migrate==4.0.5
Flask-SocketIO==5.3.6
Flask-WTF==1.1.1
Flask-CORS==4.0.0
eventlet==0.33.3
Werkzeug==2.3.7
安装依赖:
bash
pip install -r requirements.txt
- 初始化数据库
bash
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
- 运行应用
bash
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
或者使用Gunicorn部署:
bash
gunicorn -k eventlet -w 1 app:app
总结
本教程详细介绍了基于您提供的Flask代码实现论坛与个人中心功能的完整过程。您的代码已经具备了强大的基础功能,包括:
-
用户认证系统(注册、登录、登出)
-
论坛功能(发帖、评论、点赞、收藏)
-
个人中心(资料管理、头像上传、密码修改)
-
管理员功能(用户管理、内容管理)