当前位置: 首页 > news >正文

Flask论坛与个人中心页面开发教程完整详细版

目录
1. [项目概述](#项目概述)
2. [项目结构](#项目结构)
3. [数据库模型](#数据库模型)
4. [论坛功能实现](#论坛功能实现)
5. [个人中心实现](#个人中心实现)
6. [前端模板示例](#前端模板示例)
7. [API路由详解](#API路由详解)
8. [部署与运行](#部署与运行)

项目概述

本教程将基于您提供的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        # 管理员页面

数据库模型

# 用户模型
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')

论坛功能实现

1. 论坛首页路由

@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        # 分页对象,用于前端生成分页导航)

2. 帖子详情页

@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) + 1db.session.commit()comments = Comment.query.filter_by(post_id=post_id).order_by(Comment.time.desc()).all()author = post.authorauthor_posts_count = author.posts.count() if author else 0author_comments_count = author.comments.count() if author else 0is_favorite = Falseif current_user:is_favorite = Favorite.query.filter_by(user_id=current_user.id, post_id=post_id).first() is not Noneis_liked = Falseif 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)

3. 发布帖子API

@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': '请先登录'}), 401data = request.get_json() or request.form.to_dict()if not data.get('title') or not data.get('content'):return jsonify({'error': '标题和内容不能为空'}), 400user = 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}), 201except Exception as e:db.session.rollback()app.logger.error(f"发布帖子失败: {str(e)}", exc_info=True)return jsonify({'error': '发布失败,请重试'}), 500

个人中心实现

1. 个人资料页路由

@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 0comment_count = user.comments.count() if user.comments else 0favorite_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     )

 2. 查看其他用户资料

@app.route('/user_profile.html')
def user_profile():# 从 URL 参数中获取 user_iduser_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"), 404user = 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)

3. 更新个人资料API

@app.route('/api/update_profile', methods=['POST'])
def update_profile():if 'user_id' not in session:return jsonify({'error': '请先登录'}), 401user = User.query.get(session['user_id'])if not user:return jsonify({'error': '用户不存在'}), 404data = request.get_json()if not data or 'signature' not in data:return jsonify({'error': '签名内容不能为空'}), 400new_signature = data['signature'].strip()if not new_signature:return jsonify({'error': '签名内容不能为空'}), 400user.signature = new_signaturedb.session.commit()return jsonify({'success': True,'user': {'id': user.id,'username': user.username,'signature': user.signature}})

4. 上传头像API

@app.route('/api/upload_avatar', methods=['POST'])
def upload_avatar():if 'user_id' not in session:return jsonify({'error': '请先登录'}), 401user = User.query.get(session['user_id'])if not user:return jsonify({'error': '用户不存在'}), 404if 'avatar' not in request.files:return jsonify({'error': '未选择文件'}), 400file = request.files['avatar']if file.filename == '':return jsonify({'error': '未选择文件'}), 400if not allowed_file(file.filename):return jsonify({'error': '不支持的文件格式,请上传 PNG、JPG、JPEG 或 GIF'}), 400filename = secure_filename(file.filename)# 防止文件名冲突,使用 uuidunique_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# 更新用户的头像URLuser.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})

前端模板示例

1. 论坛页面 (forum.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">&laquo;</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">&raquo;</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 %}

2. 个人中心页面 (profile.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路由详解

1. 点赞与收藏功能

# 收藏帖子
@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': '请先登录'}), 401post = Post.query.get(post_id)if not post:return jsonify({'error': '帖子不存在'}), 404user = User.query.get(session['user_id'])favorite = Favorite.query.filter_by(user_id=user.id, post_id=post_id).first()is_favorite = Falseif request.method == 'POST' and not favorite:new_favorite = Favorite(user_id=user.id, post_id=post_id)db.session.add(new_favorite)is_favorite = Trueelif request.method == 'DELETE' and favorite:db.session.delete(favorite)is_favorite = Falsedb.session.commit()favorite_count = Favorite.query.filter_by(post_id=post_id).count()return jsonify({'success': True,'is_favorite': is_favorite,'count': favorite_count}), 200except 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': '请先登录'}), 401user = User.query.get(session['user_id'])if not user:return jsonify({'error': '用户不存在'}), 404post = 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 = Falseif request.method == 'POST' and not like:# 点赞:新增记录new_like = Like(user_id=user.id, post_id=post_id)db.session.add(new_like)is_liked = Trueelif request.method == 'DELETE' and like:# 取消点赞:删除记录db.session.delete(like)is_liked = Falsedb.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})

2. 评论功能

# 发布评论
@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': '请先登录'}), 401data = request.get_json() or request.form.to_dict()content = data.get('content', '').strip()if not content:return jsonify({'error': '评论内容不能为空'}), 400post = Post.query.get(post_id)if not post:return jsonify({'error': '帖子不存在'}), 404user = 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')}}), 201except 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': '请先登录'}), 401comment = Comment.query.get(comment_id)if not comment:return jsonify({'error': '评论不存在'}), 404current_user = User.query.get(session['user_id'])# 管理员可删除任意评论if comment.author_id != current_user.id and not current_user.is_admin:return jsonify({'error': '没有权限删除此评论'}), 403db.session.delete(comment)db.session.commit()return jsonify({'success': True,'message': '评论已删除'}), 200except Exception as e:db.session.rollback()app.logger.error(f"删除评论失败: {str(e)}")return jsonify({'error': '删除评论失败,请重试'}), 500

部署与运行

1. 安装依赖

创建 `requirements.txt` 文件:

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

安装依赖:

pip install -r requirements.txt

2. 初始化数据库

flask db init
flask db migrate -m "Initial migration"
flask db upgrade

3. 运行应用

flask db init
flask db migrate -m "Initial migration"
flask db upgrade

或者使用Gunicorn部署:

gunicorn -k eventlet -w 1 app:app

总结

本教程详细介绍了基于您提供的Flask代码实现论坛与个人中心功能的完整过程。您的代码已经具备了强大的基础功能,包括:

1. 用户认证系统(注册、登录、登出)
2. 论坛功能(发帖、评论、点赞、收藏)
3. 个人中心(资料管理、头像上传、密码修改)
4. 管理员功能(用户管理、内容管理)


文章转载自:

http://KO7617PO.mLpch.cn
http://RBqULAWt.mLpch.cn
http://z3zUM7ut.mLpch.cn
http://Y1EsGBaK.mLpch.cn
http://f9iYeLOT.mLpch.cn
http://YAsHR77x.mLpch.cn
http://Ziz7C1mN.mLpch.cn
http://bJyyZHYh.mLpch.cn
http://DZHNMzEh.mLpch.cn
http://QngisF8R.mLpch.cn
http://IrFxwKml.mLpch.cn
http://KWiGsDB8.mLpch.cn
http://BIYysxwG.mLpch.cn
http://QsGjOXoE.mLpch.cn
http://q02ecMeW.mLpch.cn
http://JuoLpCWo.mLpch.cn
http://NKuxML0D.mLpch.cn
http://x9wXCfmo.mLpch.cn
http://dwka1gJ8.mLpch.cn
http://zExbClQr.mLpch.cn
http://yPSfPDMR.mLpch.cn
http://1WT9Fdr1.mLpch.cn
http://mnHwR0Yv.mLpch.cn
http://dZHyX9PU.mLpch.cn
http://ARz3jzlB.mLpch.cn
http://tARwD9ym.mLpch.cn
http://a5O09PSo.mLpch.cn
http://sQlLJSmA.mLpch.cn
http://7pI7N2se.mLpch.cn
http://VGIlbZSI.mLpch.cn
http://www.dtcms.com/a/369917.html

相关文章:

  • 【PostgreSQL】如何实现主从复制?
  • 进程与服务管理:systemd / sysvinit 服务管理、定时服务(cron / at)
  • Java全栈工程师面试实录:从基础到高并发场景的技术探索
  • 2025高教社国赛数学建模A题参考论文35页(含代码和模型)
  • 前缀和、子矩阵的和;差分、差分矩阵
  • 如何在 FastAPI 中巧妙覆盖依赖注入并拦截第三方服务调用?
  • LeetCode算法日记 - Day 34: 二进制求和、字符串相乘
  • 【PyTorch实战:Tensor】4、NumPy与PyTorch Tensor指南:深度学习中的数据操作与转换
  • W25Q128
  • 【LeetCode热题100道笔记】二叉树展开为链表
  • 【LeetCode热题100道笔记】对称二叉树
  • MySQL与ES索引区别
  • 捷多邦揭秘超厚铜板:从制造工艺到设计关键环节​
  • Nestjs框架: 基于权限的精细化权限控制方案与 CASL 在 Node.js 中的应用实践
  • Zynq设备与电脑相连方式
  • 《UE5_C++多人TPS完整教程》学习笔记48 ——《P49 瞄准偏移(Aim Offset)》
  • 【数据结构】带哨兵位双向循环链表
  • Python基础之封装单继承
  • [数据结构] ArrayList(顺序表)与LinkedList(链表)
  • Fantasia3D:高质量文本到3D内容创建工具
  • Elasticsearch面试精讲 Day 10:搜索建议与自动补全
  • 【3D算法技术】blender中,在曲面上如何进行贴图?
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘mkdocs’问题
  • 【牛客刷题-剑指Offer】BM18 二维数组中的查找:一题四解,从暴力到最优
  • 推荐的Java服务环境:JDK17+ZGC(JDK 21的ZGC支持分代回收,性能更高)
  • [光学原理与应用-431]:非线性光学 - 能生成或改变激光波长的物质或元件有哪些?
  • 心路历程- Linux用户组的整理
  • 前端登录鉴权详解
  • CodeSandbox Desktop:零配置项目启动工具,实现项目环境隔离与Github无缝同步
  • Lua > Mac Mini M4安装openresty