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

目录

  1. 项目概述\](#项目概述)

  2. 数据库模型\](#数据库模型)

  3. 个人中心实现\](#个人中心实现)

  4. 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')

论坛功能实现

  1. 论坛首页路由
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        # 分页对象,用于前端生成分页导航
    )
  1. 帖子详情页
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
    )
  1. 发布帖子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

个人中心实现

  1. 个人资料页路由
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     
    )
  1. 查看其他用户资料
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)
  1. 更新个人资料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
        }
    })
  1. 上传头像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
    })

前端模板示例

  1. 论坛页面 (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 %}
  1. 个人中心页面 (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路由详解

  1. 点赞与收藏功能
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
    })
  1. 评论功能
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

部署与运行

  1. 安装依赖

创建 `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
  1. 初始化数据库
bash 复制代码
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
  1. 运行应用
bash 复制代码
flask db init
flask db migrate -m "Initial migration"
flask db upgrade

或者使用Gunicorn部署:

bash 复制代码
gunicorn -k eventlet -w 1 app:app

总结

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

  1. 用户认证系统(注册、登录、登出)

  2. 论坛功能(发帖、评论、点赞、收藏)

  3. 个人中心(资料管理、头像上传、密码修改)

  4. 管理员功能(用户管理、内容管理)

相关推荐
Rhys..4 小时前
python + Flask模块学习 2 接收用户请求并返回json数据
python·学习·flask
Enougme4 小时前
python-虚拟试衣
python
nightunderblackcat5 小时前
新手向:Python制作贪吃蛇游戏(Pygame)
python·游戏·pygame
迷知悟道5 小时前
java面向对象四大核心特征之抽象---超详细(保姆级)
java·后端
flysh055 小时前
pyAutoGUI 模块主要功能介绍-(1)鼠标功能
python·计算机外设·鼠标控制
Aurora_NeAr6 小时前
对比Java学习Go——程序结构与变量
后端
AntBlack6 小时前
每周学点 AI:ComfyUI + Modal 的一键部署脚本
人工智能·后端·aigc
5大大大大雄7 小时前
docker容器日志处理
后端
励志不掉头发的内向程序员7 小时前
从零开始的python学习——文件
开发语言·python·学习