Complete project files including setup.sh
This commit is contained in:
162
routes/auth.py
Normal file
162
routes/auth.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
from models import db, User
|
||||
from config import Config
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
"""检查文件扩展名是否允许"""
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
"""用户注册"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
student_id_photo = request.files.get('student_id_photo')
|
||||
|
||||
# 验证输入
|
||||
if not all([username, email, password, confirm_password, student_id_photo]):
|
||||
flash('请填写所有字段并上传学生证照片', 'error')
|
||||
return render_template('register.html')
|
||||
|
||||
if password != confirm_password:
|
||||
flash('两次输入的密码不一致', 'error')
|
||||
return render_template('register.html')
|
||||
|
||||
if len(password) < 6:
|
||||
flash('密码长度至少为6位', 'error')
|
||||
return render_template('register.html')
|
||||
|
||||
# 检查用户名和邮箱是否已存在
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('用户名已被注册', 'error')
|
||||
return render_template('register.html')
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('邮箱已被注册', 'error')
|
||||
return render_template('register.html')
|
||||
|
||||
# 保存学生证照片
|
||||
if student_id_photo and allowed_file(student_id_photo.filename):
|
||||
filename = secure_filename(f"{username}_{student_id_photo.filename}")
|
||||
filepath = os.path.join(Config.UPLOAD_FOLDER, 'student_ids', filename)
|
||||
student_id_photo.save(filepath)
|
||||
|
||||
# 创建用户
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
student_id_photo=f'student_ids/{filename}',
|
||||
is_approved=False
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
flash('注册成功!请等待管理员审核您的学生身份', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
else:
|
||||
flash('请上传有效的图片文件(支持 PNG, JPG, JPEG, GIF, WEBP)', 'error')
|
||||
|
||||
return render_template('register.html')
|
||||
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""用户登录"""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
remember = request.form.get('remember', False)
|
||||
|
||||
if not all([username, password]):
|
||||
flash('请填写用户名和密码', 'error')
|
||||
return render_template('login.html')
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user is None or not user.check_password(password):
|
||||
flash('用户名或密码错误', 'error')
|
||||
return render_template('login.html')
|
||||
|
||||
if not user.is_approved:
|
||||
flash('您的账号正在等待管理员审核', 'warning')
|
||||
return render_template('login.html')
|
||||
|
||||
login_user(user, remember=remember)
|
||||
|
||||
# 检查管理员是否需要修改密码
|
||||
if user.is_admin and not user.password_changed:
|
||||
flash('首次登录,请修改密码', 'warning')
|
||||
return redirect(url_for('auth.change_password'))
|
||||
|
||||
flash(f'欢迎回来,{user.username}!', 'success')
|
||||
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page) if next_page else redirect(url_for('posts.index'))
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
"""用户登出"""
|
||||
logout_user()
|
||||
flash('您已成功退出登录', 'info')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
|
||||
@auth_bp.route('/change-password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
"""修改密码"""
|
||||
if request.method == 'POST':
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
if not all([current_password, new_password, confirm_password]):
|
||||
flash('请填写所有字段', 'error')
|
||||
return render_template('change_password.html')
|
||||
|
||||
# 验证当前密码
|
||||
if not current_user.check_password(current_password):
|
||||
flash('当前密码错误', 'error')
|
||||
return render_template('change_password.html')
|
||||
|
||||
# 验证新密码
|
||||
if new_password != confirm_password:
|
||||
flash('两次输入的新密码不一致', 'error')
|
||||
return render_template('change_password.html')
|
||||
|
||||
if len(new_password) < 6:
|
||||
flash('密码长度至少为6位', 'error')
|
||||
return render_template('change_password.html')
|
||||
|
||||
# 更新密码
|
||||
current_user.set_password(new_password)
|
||||
current_user.password_changed = True
|
||||
db.session.commit()
|
||||
|
||||
flash('密码修改成功', 'success')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
return render_template('change_password.html')
|
||||
|
||||
178
routes/new_admin.py
Normal file
178
routes/new_admin.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import current_user
|
||||
from functools import wraps
|
||||
from models import db, User, Post
|
||||
|
||||
# 使用 new_admin 以避免任何命名冲突或缓存问题
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
"""管理员权限装饰器 - 直接定义在此文件中以避免导入问题"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for('auth.login', next=request.url))
|
||||
|
||||
if not current_user.is_admin:
|
||||
flash('需要管理员权限', 'error')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@admin_bp.route('/')
|
||||
@admin_required
|
||||
def dashboard():
|
||||
"""管理员仪表板"""
|
||||
pending_users = User.query.filter_by(is_approved=False).count()
|
||||
pending_posts = Post.query.filter_by(is_approved=False).count()
|
||||
|
||||
return render_template('admin/dashboard.html',
|
||||
pending_users=pending_users,
|
||||
pending_posts=pending_posts)
|
||||
|
||||
|
||||
@admin_bp.route('/users')
|
||||
@admin_required
|
||||
def users():
|
||||
"""待审核用户列表"""
|
||||
pending_users = User.query.filter_by(is_approved=False).order_by(User.created_at.desc()).all()
|
||||
approved_users = User.query.filter_by(is_approved=True).order_by(User.created_at.desc()).limit(20).all()
|
||||
|
||||
return render_template('admin/users.html',
|
||||
pending_users=pending_users,
|
||||
approved_users=approved_users)
|
||||
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/approve', methods=['POST'])
|
||||
@admin_required
|
||||
def approve_user(user_id):
|
||||
"""批准用户注册"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.is_approved:
|
||||
flash('该用户已经通过审核', 'info')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
user.is_approved = True
|
||||
db.session.commit()
|
||||
|
||||
flash(f'已批准用户 {user.username} 的注册', 'success')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/reject', methods=['POST'])
|
||||
@admin_required
|
||||
def reject_user(user_id):
|
||||
"""拒绝用户注册"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if user.is_approved:
|
||||
flash('该用户已经通过审核,无法拒绝', 'error')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
# 删除用户及其相关数据
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'已拒绝用户 {user.username} 的注册', 'success')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
|
||||
@admin_bp.route('/posts')
|
||||
@admin_required
|
||||
def posts():
|
||||
"""待审核帖子列表"""
|
||||
pending_posts = Post.query.filter_by(is_approved=False).order_by(Post.created_at.desc()).all()
|
||||
approved_posts = Post.query.filter_by(is_approved=True).order_by(Post.created_at.desc()).limit(20).all()
|
||||
|
||||
return render_template('admin/posts.html',
|
||||
pending_posts=pending_posts,
|
||||
approved_posts=approved_posts)
|
||||
|
||||
|
||||
@admin_bp.route('/posts/<int:post_id>/approve', methods=['POST'])
|
||||
@admin_required
|
||||
def approve_post(post_id):
|
||||
"""批准帖子发布"""
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
if post.is_approved:
|
||||
flash('该帖子已经通过审核', 'info')
|
||||
return redirect(url_for('admin.posts'))
|
||||
|
||||
post.is_approved = True
|
||||
db.session.commit()
|
||||
|
||||
flash('已批准该帖子发布', 'success')
|
||||
return redirect(url_for('admin.posts'))
|
||||
|
||||
|
||||
@admin_bp.route('/posts/<int:post_id>/reject', methods=['POST'])
|
||||
@admin_required
|
||||
def reject_post(post_id):
|
||||
"""拒绝帖子发布"""
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
if post.is_approved:
|
||||
flash('该帖子已经通过审核,无法拒绝', 'error')
|
||||
return redirect(url_for('admin.posts'))
|
||||
|
||||
# 删除帖子
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
|
||||
flash('已拒绝该帖子发布', 'success')
|
||||
return redirect(url_for('admin.posts'))
|
||||
|
||||
|
||||
@admin_bp.route('/create-admin', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def create_admin():
|
||||
"""创建新管理员"""
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
if not all([username, email, password, confirm_password]):
|
||||
flash('请填写所有字段', 'error')
|
||||
return render_template('admin/create_admin.html')
|
||||
|
||||
if password != confirm_password:
|
||||
flash('两次输入的密码不一致', 'error')
|
||||
return render_template('admin/create_admin.html')
|
||||
|
||||
if len(password) < 6:
|
||||
flash('密码长度至少为6位', 'error')
|
||||
return render_template('admin/create_admin.html')
|
||||
|
||||
# 检查用户名和邮箱是否已存在
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('用户名已被使用', 'error')
|
||||
return render_template('admin/create_admin.html')
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('邮箱已被使用', 'error')
|
||||
return render_template('admin/create_admin.html')
|
||||
|
||||
# 创建新管理员
|
||||
new_admin = User(
|
||||
username=username,
|
||||
email=email,
|
||||
is_approved=True,
|
||||
is_admin=True,
|
||||
password_changed=True # 新建管理员默认认为已知晓密码,或者后续再改
|
||||
)
|
||||
new_admin.set_password(password)
|
||||
|
||||
db.session.add(new_admin)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'成功创建管理员账号: {username}', 'success')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
return render_template('admin/create_admin.html')
|
||||
126
routes/posts.py
Normal file
126
routes/posts.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from models import db, Post, Comment
|
||||
from config import Config
|
||||
|
||||
posts_bp = Blueprint('posts', __name__)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
"""检查文件扩展名是否允许"""
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
@posts_bp.route('/')
|
||||
def index():
|
||||
"""首页 - 显示已审核的帖子流"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
# 只显示已审核的帖子
|
||||
pagination = Post.query.filter_by(is_approved=True)\
|
||||
.order_by(Post.created_at.desc())\
|
||||
.paginate(page=page, per_page=current_app.config['POSTS_PER_PAGE'], error_out=False)
|
||||
|
||||
posts = pagination.items
|
||||
|
||||
return render_template('index.html', posts=posts, pagination=pagination)
|
||||
|
||||
|
||||
@posts_bp.route('/post/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_post():
|
||||
"""创建帖子"""
|
||||
if not current_user.is_approved:
|
||||
flash('您的账号尚未通过审核,无法发帖', 'error')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
description = request.form.get('description')
|
||||
image = request.files.get('image')
|
||||
|
||||
if not image:
|
||||
flash('请上传照片', 'error')
|
||||
return render_template('create_post.html')
|
||||
|
||||
if not allowed_file(image.filename):
|
||||
flash('请上传有效的图片文件(支持 PNG, JPG, JPEG, GIF, WEBP)', 'error')
|
||||
return render_template('create_post.html')
|
||||
|
||||
# 保存图片
|
||||
filename = secure_filename(f"{current_user.username}_{image.filename}")
|
||||
# 添加时间戳避免重名
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
filename = f"{timestamp}_{filename}"
|
||||
filepath = os.path.join(Config.UPLOAD_FOLDER, 'posts', filename)
|
||||
image.save(filepath)
|
||||
|
||||
# 创建帖子
|
||||
post = Post(
|
||||
user_id=current_user.id,
|
||||
image_path=f'posts/{filename}',
|
||||
description=description,
|
||||
is_approved=False # 需要审核
|
||||
)
|
||||
|
||||
db.session.add(post)
|
||||
db.session.commit()
|
||||
|
||||
flash('帖子已提交,等待管理员审核', 'success')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
return render_template('create_post.html')
|
||||
|
||||
|
||||
@posts_bp.route('/post/<int:post_id>')
|
||||
def post_detail(post_id):
|
||||
"""帖子详情页"""
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
# 如果帖子未审核,只有作者和管理员可以查看
|
||||
if not post.is_approved:
|
||||
if not current_user.is_authenticated or \
|
||||
(current_user.id != post.user_id and not current_user.is_admin):
|
||||
flash('该帖子正在审核中', 'warning')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
# 获取评论
|
||||
comments = post.comments.order_by(Comment.created_at.desc()).all()
|
||||
|
||||
return render_template('post_detail.html', post=post, comments=comments)
|
||||
|
||||
|
||||
@posts_bp.route('/post/<int:post_id>/comment', methods=['POST'])
|
||||
@login_required
|
||||
def add_comment(post_id):
|
||||
"""添加评论"""
|
||||
if not current_user.is_approved:
|
||||
flash('您的账号尚未通过审核,无法评论', 'error')
|
||||
return redirect(url_for('posts.post_detail', post_id=post_id))
|
||||
|
||||
post = Post.query.get_or_404(post_id)
|
||||
|
||||
if not post.is_approved:
|
||||
flash('该帖子正在审核中,无法评论', 'error')
|
||||
return redirect(url_for('posts.index'))
|
||||
|
||||
content = request.form.get('content')
|
||||
|
||||
if not content or not content.strip():
|
||||
flash('评论内容不能为空', 'error')
|
||||
return redirect(url_for('posts.post_detail', post_id=post_id))
|
||||
|
||||
comment = Comment(
|
||||
post_id=post_id,
|
||||
user_id=current_user.id,
|
||||
content=content.strip()
|
||||
)
|
||||
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
flash('评论发表成功', 'success')
|
||||
return redirect(url_for('posts.post_detail', post_id=post_id))
|
||||
93
routes/users.py
Normal file
93
routes/users.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from models import db, User, Follow, Post
|
||||
|
||||
users_bp = Blueprint('users', __name__)
|
||||
|
||||
|
||||
@users_bp.route('/user/<username>')
|
||||
def profile(username):
|
||||
"""用户个人主页"""
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
|
||||
# 只显示已审核的帖子(除非是自己或管理员)
|
||||
if current_user.is_authenticated and (current_user.id == user.id or current_user.is_admin):
|
||||
posts = user.posts.order_by(Post.created_at.desc()).all()
|
||||
else:
|
||||
posts = user.posts.filter_by(is_approved=True).order_by(Post.created_at.desc()).all()
|
||||
|
||||
follower_count = user.get_follower_count()
|
||||
following_count = user.get_following_count()
|
||||
|
||||
is_following = False
|
||||
if current_user.is_authenticated:
|
||||
is_following = current_user.is_following(user)
|
||||
|
||||
return render_template('profile.html',
|
||||
user=user,
|
||||
posts=posts,
|
||||
follower_count=follower_count,
|
||||
following_count=following_count,
|
||||
is_following=is_following)
|
||||
|
||||
|
||||
@users_bp.route('/user/<username>/follow', methods=['POST'])
|
||||
@login_required
|
||||
def follow(username):
|
||||
"""关注用户"""
|
||||
if not current_user.is_approved:
|
||||
flash('您的账号尚未通过审核,无法关注', 'error')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('不能关注自己', 'error')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
if current_user.is_following(user):
|
||||
flash('您已经关注了该用户', 'info')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
current_user.follow(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'成功关注 {user.username}', 'success')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
|
||||
@users_bp.route('/user/<username>/unfollow', methods=['POST'])
|
||||
@login_required
|
||||
def unfollow(username):
|
||||
"""取消关注"""
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash('不能取消关注自己', 'error')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
if not current_user.is_following(user):
|
||||
flash('您还没有关注该用户', 'info')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
current_user.unfollow(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'已取消关注 {user.username}', 'success')
|
||||
return redirect(url_for('users.profile', username=username))
|
||||
|
||||
|
||||
@users_bp.route('/user/<username>/followers')
|
||||
def followers(username):
|
||||
"""粉丝列表"""
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
followers = [follow.follower for follow in user.followers.all()]
|
||||
return render_template('followers.html', user=user, followers=followers)
|
||||
|
||||
|
||||
@users_bp.route('/user/<username>/following')
|
||||
def following(username):
|
||||
"""关注列表"""
|
||||
user = User.query.filter_by(username=username).first_or_404()
|
||||
following = [follow.following for follow in user.following.all()]
|
||||
return render_template('following.html', user=user, following=following)
|
||||
Reference in New Issue
Block a user