爆炸級的整體優化

This commit is contained in:
Wensheng 2024-10-28 20:53:12 +08:00
parent 9f20c8dbba
commit 4bb0b1cfa8
16 changed files with 1605 additions and 611 deletions

@ -4,19 +4,22 @@ from datetime import datetime
class Comment(db.Model):
"""留言模型"""
__tablename__ = 'comment'
# 基本欄位
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
content = db.Column(db.Text, nullable=False, comment='留言內容')
# 時間相關欄位
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='創建時間')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新時間')
# 外鍵關聯
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, comment='留言者ID')
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False, comment='文章ID')
parent_id = db.Column(db.Integer, db.ForeignKey('comment.id'), comment='父留言ID用於回覆功能')
# 關聯關係
author = db.relationship('User', backref=db.backref('comments', lazy=True))
post = db.relationship('Post', backref=db.backref('comments', lazy=True, cascade='all, delete-orphan'))
replies = db.relationship(
'Comment',
backref=db.backref('parent', remote_side=[id]),
@ -24,5 +27,23 @@ class Comment(db.Model):
cascade='all, delete-orphan'
)
@property
def reply_count(self):
"""獲取回覆數量"""
return self.replies.count()
def __repr__(self):
return f'<Comment {self.id}>'
def to_dict(self):
"""轉換為字典格式用於API回應"""
return {
'id': self.id,
'content': self.content,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'user_id': self.user_id,
'post_id': self.post_id,
'parent_id': self.parent_id,
'reply_count': self.reply_count
}

@ -1,16 +1,42 @@
# app/models/like.py
from app import db
from datetime import datetime
class Like(db.Model):
"""按讚模型"""
__tablename__ = 'likes'
# 基本欄位
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)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='按讚時間')
__table_args__ = (db.UniqueConstraint('user_id', 'post_id', name='_user_post_uc'),)
# 外鍵關聯
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, comment='用戶ID')
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False, comment='文章ID')
# 確保每個用戶只能對同一篇文章按讚一次
__table_args__ = (
db.UniqueConstraint('user_id', 'post_id', name='unique_user_post_like'),
)
def __repr__(self):
return f'<Like {self.id}>'
return f'<Like {self.id}: User {self.user_id} -> Post {self.post_id}>'
def to_dict(self):
"""轉換為字典格式用於API回應"""
return {
'id': self.id,
'created_at': self.created_at.isoformat(),
'user_id': self.user_id,
'post_id': self.post_id
}
@classmethod
def get_user_likes(cls, user_id):
"""獲取用戶的所有按讚記錄"""
return cls.query.filter_by(user_id=user_id).all()
@classmethod
def get_post_likes(cls, post_id):
"""獲取文章的所有按讚記錄"""
return cls.query.filter_by(post_id=post_id).all()

@ -3,15 +3,23 @@ from datetime import datetime
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)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
"""文章模型"""
__tablename__ = 'post'
# 關聯
author = db.relationship('User', backref=db.backref('posts', lazy=True))
# 基本欄位
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False, comment='標題')
content = db.Column(db.Text, nullable=False, comment='內容')
# 時間相關欄位
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='創建時間')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新時間')
# 外鍵
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, comment='作者ID')
# 關聯關係
comments = db.relationship('Comment', backref='post', lazy='dynamic', cascade='all, delete-orphan')
likes = db.relationship('Like', backref='post', lazy='dynamic', cascade='all, delete-orphan')
@property
@ -19,8 +27,13 @@ class Post(db.Model):
"""獲取按讚數量"""
return self.likes.count()
@property
def comments_count(self) -> int:
"""獲取留言數量"""
return self.comments.count()
def is_liked_by(self, user):
"""檢查用戶是否已按讚"""
"""檢查用戶是否已按讚此文章"""
if not user:
return False
return bool(self.likes.filter_by(user_id=user.id).first())

@ -2,52 +2,108 @@ from app import db, login_manager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from .like import Like
@login_manager.user_loader
def load_user(id):
"""載入用戶,用於 Flask-Login"""
return User.query.get(int(id))
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True, nullable=False)
email = db.Column(db.String(120), unique=True, index=True, nullable=False)
password_hash = db.Column(db.String(128))
avatar_path = db.Column(db.String(200))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
is_admin = db.Column(db.Boolean, default=False)
"""用戶模型"""
__tablename__ = 'user'
def like_post(self, post):
"""按讚文章"""
if not self.is_liking(post):
# 基本欄位
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True, nullable=False, comment='用戶名')
email = db.Column(db.String(120), unique=True, index=True, nullable=False, comment='電子郵件')
password_hash = db.Column(db.String(128), comment='密碼雜湊')
avatar_path = db.Column(db.String(200), nullable=True, comment='頭像路徑')
# 時間相關欄位
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='創建時間')
last_login = db.Column(db.DateTime, default=datetime.utcnow, comment='最後登入時間')
# 狀態欄位
is_active = db.Column(db.Boolean, default=True, comment='是否啟用')
is_admin = db.Column(db.Boolean, default=False, comment='是否為管理員')
# 關聯關係
posts = db.relationship('Post', backref='author', lazy='dynamic', cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic', cascade='all, delete-orphan')
likes = db.relationship('Like', backref='user', lazy='dynamic', cascade='all, delete-orphan')
def set_password(self, password: str) -> None:
"""
設置密碼
Args:
password: 原始密碼
"""
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
"""
驗證密碼
Args:
password: 待驗證的密碼
Returns:
bool: 密碼是否正確
"""
return check_password_hash(self.password_hash, password)
def like_post(self, post) -> None:
"""
對文章按讚
Args:
post: 文章實例
"""
if not self.has_liked_post(post):
from app.models.like import Like
like = Like(user_id=self.id, post_id=post.id)
db.session.add(like)
def unlike_post(self, post):
"""取消按讚"""
like = Like.query.filter_by(
user_id=self.id,
post_id=post.id
).first()
def unlike_post(self, post) -> None:
"""
取消文章按讚
Args:
post: 文章實例
"""
from app.models.like import Like
like = Like.query.filter_by(user_id=self.id, post_id=post.id).first()
if like:
db.session.delete(like)
def is_liking(self, post):
"""檢查是否已按讚"""
return Like.query.filter_by(
user_id=self.id,
post_id=post.id
).first() is not None
def has_liked_post(self, post) -> bool:
"""
檢查是否已對文章按讚
def set_password(self, password):
self.password_hash = generate_password_hash(password)
Args:
post: 文章實例
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Returns:
bool: 是否已按讚
"""
from app.models.like import Like
return Like.query.filter_by(user_id=self.id, post_id=post.id).first() is not None
def __repr__(self):
@property
def posts_count(self) -> int:
"""獲取發文總數"""
return self.posts.count()
@property
def received_likes_count(self) -> int:
"""獲取收到的總讚數"""
from app.models.like import Like
from app.models.post import Post
return Like.query.join(Post).filter(Post.user_id == self.id).count()
def __repr__(self) -> str:
"""模型的字符串表示"""
return f'<User {self.username}>'

@ -3,35 +3,47 @@ from flask_login import login_user, logout_user, login_required, current_user
from app.services import UserService
from urllib.parse import urlparse
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""處理用戶登入"""
"""
用戶登入處理
GET: 顯示登入頁面
POST: 處理登入請求
"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
remember = request.form.get('remember', False)
# 獲取表單數據
credentials = {
'email': request.form.get('email'),
'password': request.form.get('password'),
'remember': bool(request.form.get('remember'))
}
user = UserService.get_user_by_email(email)
if user is None or not user.check_password(password):
# 驗證用戶
user = UserService.get_user_by_email(credentials['email'])
if not user or not user.check_password(credentials['password']):
flash('電子郵件或密碼錯誤', 'danger')
return redirect(url_for('auth.login'))
# 更新最後登入時間
UserService.update_last_login(user.id)
login_user(user, remember=remember)
# 更新登入時間
success, error = UserService.update_last_login(user.id)
if not success:
current_app.logger.error(f"Failed to update last login time: {error}")
# 登入用戶
login_user(user, remember=credentials['remember'])
flash('登入成功!', 'success')
# 處理重定向
next_page = request.args.get('next')
if not next_page or urlparse(next_page).netloc != '':
next_page = url_for('main.index')
flash('登入成功!', 'success')
return redirect(next_page)
return render_template('auth/login.html', title='登入')
@ -39,21 +51,35 @@ def login():
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""處理用戶註冊"""
"""
用戶註冊處理
GET: 顯示註冊頁面
POST: 處理註冊請求
"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
email = request.form.get('email')
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# 獲取註冊資料
user_data = {
'email': request.form.get('email'),
'username': request.form.get('username'),
'password': request.form.get('password'),
'confirm_password': request.form.get('confirm_password')
}
if password != confirm_password:
# 驗證密碼
if user_data['password'] != user_data['confirm_password']:
flash('密碼不一致', 'danger')
return redirect(url_for('auth.register'))
user, error = UserService.create_user(username, email, password)
# 創建用戶
user, error = UserService.create_user(
username=user_data['username'],
email=user_data['email'],
password=user_data['password']
)
if error:
flash(error, 'danger')
return redirect(url_for('auth.register'))
@ -76,48 +102,59 @@ def logout():
@auth_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
"""處理用戶個人資料更新"""
"""
用戶個人資料管理
GET: 顯示個人資料頁面
POST: 處理個人資料更新
"""
if request.method == 'POST':
action = request.form.get('action')
if action == 'update_profile':
# 處理基本資料更新
# 處理頭像更新
if 'avatar' in request.files:
file = request.files['avatar']
if file.filename:
success, error = UserService.update_avatar(
user_id=current_user.id,
file=file,
app=current_app
)
file=file
) # 移除 app 參數
if not success:
flash(f'頭像更新失敗: {error}', 'danger')
return redirect(url_for('auth.profile'))
# 更新其他資料
# 處理基本資料更新
profile_data = {
'username': request.form.get('username'),
'email': request.form.get('email')
}
success, error = UserService.update_profile(
user_id=current_user.id,
username=request.form.get('username'),
email=request.form.get('email')
**profile_data
)
if success:
flash('個人資料已更新', 'success')
else:
flash(f'更新失敗: {error}', 'danger')
flash('個人資料已更新' if success else f'更新失敗: {error}',
'success' if success else 'danger')
elif action == 'update_password':
# 處理密碼更新
password_data = {
'current_password': request.form.get('current_password'),
'new_password': request.form.get('new_password')
}
# 驗證輸入
if not all(password_data.values()):
flash('請填寫所有密碼欄位', 'danger')
return redirect(url_for('auth.profile'))
success, error = UserService.update_password(
user_id=current_user.id,
current_password=request.form.get('current_password'),
new_password=request.form.get('new_password')
current_password=password_data['current_password'],
new_password=password_data['new_password']
)
if success:
flash('密碼已更新', 'success')
else:
flash(f'更新失敗: {error}', 'danger')
flash('密碼已更新' if success else f'更新失敗: {error}',
'success' if success else 'danger')
return redirect(url_for('auth.profile'))
return render_template('auth/profile.html', title='個人資料')

@ -1,60 +1,44 @@
# 標準庫導入
from datetime import datetime
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request
from flask_login import current_user
from sqlalchemy.orm import joinedload
from app import db
from app.models import User, Post
from app.services import UserService
# 第三方套件導入
from flask import render_template, Blueprint, request
# 本地應用導入
from app.models.user import User
from app.models.post import Post
# 常數配置
POSTS_PER_PAGE = 5
MEMBERS_PER_PAGE = 16
DATE_FORMAT = '%Y-%m-%d %H:%M'
# 創建主要藍圖
main_bp = Blueprint('main', __name__, url_prefix='/')
def get_latest_posts(limit=POSTS_PER_PAGE):
def get_active_users(limit: int = 12) -> list:
"""
獲取最新的文章
:param limit: 限制返回的文章數量
:return: 文章列表
獲取活躍用戶列表
Args:
limit: 返回用戶數量限制預設12人
Returns:
活躍用戶列表
"""
return Post.query.order_by(Post.created_at.desc()).limit(limit).all()
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
return User.query.filter(
User.last_login >= thirty_days_ago
).order_by(
User.last_login.desc()
).limit(limit).all()
def get_user_statistics():
def get_site_statistics() -> dict:
"""
獲取用戶統計資料
:return: dict 包含總用戶數和本月新增用戶數
"""
total_users = User.query.count()
# 計算本月新增用戶
first_day_of_month = datetime.utcnow().replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
new_users_this_month = User.query.filter(
User.created_at >= first_day_of_month
).count()
return {
'total': total_users,
'new_this_month': new_users_this_month
}
def get_last_update_time():
"""
獲取最後更新時間
:return: str 格式化的最後更新時間
獲取網站統計資訊
Returns:
包含網站統計資訊的字典
"""
# 獲取最後更新時間
latest_user = User.query.order_by(User.created_at.desc()).first()
latest_post = Post.query.order_by(Post.created_at.desc()).first()
# 計算最後更新時間
last_update = None
if latest_user and latest_post:
last_update = max(latest_user.created_at, latest_post.created_at)
@ -63,53 +47,64 @@ def get_last_update_time():
elif latest_post:
last_update = latest_post.created_at
return last_update.strftime(DATE_FORMAT) if last_update else '無資料'
return last_update.strftime('%Y-%m-%d %H:%M') if last_update else '無資料'
# 路由定義
@main_bp.route('/')
def index():
"""首頁視圖函數"""
# 獲取統計資料
user_stats = get_user_statistics()
"""首頁視圖"""
# 獲取用戶統計
user_stats = UserService.get_user_statistics()
# 獲取最新文章
latest_posts = Post.query.options(
joinedload(Post.author)
).order_by(Post.created_at.desc()).limit(10).all()
# 準備模板數據
template_data = {
'title': '首頁',
'latest_posts': get_latest_posts(),
'latest_posts': latest_posts,
'total_users': user_stats['total'],
'total_posts': Post.query.count(),
'new_users_this_month': user_stats['new_this_month'],
'last_update': get_last_update_time()
'last_update': get_site_statistics(),
'active_users': get_active_users()
}
# 如果用戶已登入,添加用戶統計資訊
if current_user.is_authenticated:
user_activity_stats = UserService.get_user_stats(current_user.id)
template_data['user_stats'] = {
'posts_count': user_activity_stats['posts_count'],
'received_likes': user_activity_stats['received_likes']
}
return render_template('main/index.html', **template_data)
@main_bp.route('/about')
def about():
"""關於頁面視圖函數"""
return render_template('main/about.html', title='關於我們')
@main_bp.route('/members')
def members():
"""會員列表視圖函數"""
# 獲取當前頁碼
"""會員列表視圖"""
page = request.args.get('page', 1, type=int)
per_page = 16 # 每頁顯示的會員數量
# 查詢會員資料
pagination = User.query.order_by(User.created_at.desc()).paginate(
pagination = User.query.order_by(
User.created_at.desc()
).paginate(
page=page,
per_page=MEMBERS_PER_PAGE,
per_page=per_page,
error_out=False
)
# 準備模板數據
template_data = {
'title': '會員列表',
'users': pagination.items,
'pagination': pagination
}
return render_template('main/members.html',
title='會員列表',
users=pagination.items,
pagination=pagination)
return render_template('main/members.html', **template_data)
@main_bp.route('/about')
def about():
"""關於頁面視圖"""
return render_template('main/about.html',
title='關於我們')

@ -1,21 +1,37 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask import (
Blueprint, render_template, redirect, url_for,
flash, request, jsonify, current_app
)
from flask_login import login_required, current_user
from app.services import PostService, CommentService, LikeService
post_bp = Blueprint('post', __name__, url_prefix='/posts')
# 配置常量
POSTS_PER_PAGE = 10
@post_bp.route('/')
def index():
"""文章列表"""
"""
文章列表頁面
支援搜索和分頁功能
"""
page = request.args.get('page', 1, type=int)
search_query = request.args.get('q')
# 根據是否有搜索關鍵字決定查詢方式
if search_query:
pagination = PostService.search_posts(search_query, page=page)
pagination = PostService.search_posts(
query=search_query,
page=page,
per_page=POSTS_PER_PAGE
)
else:
pagination = PostService.get_posts_page(page=page)
pagination = PostService.get_posts_page(
page=page,
per_page=POSTS_PER_PAGE
)
return render_template('posts/index.html',
title='文章列表',
@ -26,15 +42,20 @@ def index():
@post_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
"""創建文章"""
"""
創建新文章
GET: 顯示創建文章表單
POST: 處理文章創建請求
"""
if request.method == 'POST':
title = request.form.get('title', '').strip()
content = request.form.get('content', '').strip()
post_data = {
'title': request.form.get('title', '').strip(),
'content': request.form.get('content', '').strip()
}
post, error = PostService.create_post(
user_id=current_user.id,
title=title,
content=content
**post_data
)
if error:
@ -49,7 +70,12 @@ def create():
@post_bp.route('/<int:id>')
def show(id):
"""顯示文章詳情"""
"""
顯示文章詳情
Args:
id: 文章ID
"""
post = PostService.get_post_by_id(id)
if not post:
flash('文章不存在', 'danger')
@ -63,20 +89,28 @@ def show(id):
@post_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
"""編輯文章"""
"""
編輯文章
GET: 顯示編輯表單
POST: 處理文章更新請求
Args:
id: 文章ID
"""
post = PostService.get_post_by_id(id)
if not post or post.user_id != current_user.id:
flash('無權限編輯此文章', 'danger')
return redirect(url_for('post.index'))
if request.method == 'POST':
title = request.form.get('title', '').strip()
content = request.form.get('content', '').strip()
post_data = {
'title': request.form.get('title', '').strip(),
'content': request.form.get('content', '').strip()
}
success, error = PostService.update_post(
post_id=id,
title=title,
content=content
**post_data
)
if not success:
@ -94,7 +128,12 @@ def edit(id):
@post_bp.route('/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
"""刪除文章"""
"""
刪除文章
Args:
id: 文章ID
"""
post = PostService.get_post_by_id(id)
if not post or post.user_id != current_user.id:
flash('無權限刪除此文章', 'danger')
@ -112,7 +151,15 @@ def delete(id):
@post_bp.route('/<int:post_id>/like', methods=['POST'])
@login_required
def toggle_like(post_id):
"""切換按讚狀態"""
"""
切換文章按讚狀態
Args:
post_id: 文章ID
Returns:
JSON響應包含操作結果和最新按讚數
"""
success, is_liked, count = LikeService.toggle_like(
user_id=current_user.id,
post_id=post_id
@ -124,27 +171,35 @@ def toggle_like(post_id):
'liked': is_liked,
'count': count
})
return jsonify({'success': False, 'message': '操作失敗'}), 500
return jsonify({
'success': False,
'message': '操作失敗'
}), 500
@post_bp.route('/<int:post_id>/comments', methods=['POST'])
@login_required
def create_comment(post_id):
"""創建留言"""
content = request.form.get('content', '').strip()
parent_id = request.form.get('parent_id', type=int)
"""
創建文章評論
Args:
post_id: 文章ID
"""
comment_data = {
'content': request.form.get('content', '').strip(),
'parent_id': request.form.get('parent_id', type=int)
}
success, error = CommentService.create_comment(
user_id=current_user.id,
post_id=post_id,
content=content,
parent_id=parent_id
**comment_data
)
if success:
flash('留言成功', 'success')
else:
flash(f'留言失敗: {error}', 'danger')
flash('留言成功' if success else f'留言失敗: {error}',
'success' if success else 'danger')
return redirect(url_for('post.show', id=post_id))
@ -152,15 +207,34 @@ def create_comment(post_id):
@post_bp.route('/comments/<int:comment_id>', methods=['DELETE'])
@login_required
def delete_comment(comment_id):
"""刪除留言"""
"""
刪除評論
Args:
comment_id: 評論ID
Returns:
JSON響應包含操作結果
"""
comment = CommentService.get_comment_by_id(comment_id)
if not comment:
return jsonify({'success': False, 'message': '留言不存在'}), 404
return jsonify({
'success': False,
'message': '留言不存在'
}), 404
if comment.user_id != current_user.id:
return jsonify({'success': False, 'message': '無權限刪除此留言'}), 403
return jsonify({
'success': False,
'message': '無權限刪除此留言'
}), 403
success, error = CommentService.delete_comment(comment_id)
if success:
return jsonify({'success': True})
return jsonify({'success': False, 'message': error}), 500
return jsonify({
'success': False,
'message': error
}), 500

@ -1,148 +1,181 @@
# 標準庫導入
from datetime import datetime, timedelta
# 第三方套件導入
from flask import render_template, Blueprint, flash, redirect, url_for, request
from flask import Blueprint, render_template, flash, redirect, url_for, request, current_app
from flask_login import login_required, current_user
# 本地應用導入
from app import db
from app.models.user import User
from app.models import User
# 系統配置常量
class SystemConfig:
"""系統配置常量"""
VERSION = '1.0.0'
LAST_UPDATE = '2024-10-27'
ACTIVE_DAYS_THRESHOLD = 30
DEFAULT_PAGE_SIZE = 20
# 常數配置
SYSTEM_VERSION = '1.0.0'
LAST_UPDATE = '2024-10-27'
ACTIVE_DAYS_THRESHOLD = 30
# 創建設定藍圖
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
# 工具函數
def get_user_counts():
"""
獲取用戶統計數據
:return: tuple(int, int) 總用戶數和活躍用戶數
"""
try:
# 計算總用戶數
total_users = User.query.count()
# 計算活躍用戶數最近30天內有登入的用戶
thirty_days_ago = datetime.utcnow() - timedelta(days=ACTIVE_DAYS_THRESHOLD)
active_users = User.query.filter(User.last_login >= thirty_days_ago).count()
return total_users, active_users
except Exception as e:
return 0, 0
def get_system_stats():
def get_system_statistics() -> dict:
"""
獲取系統統計數據
:return: dict 系統統計資訊
"""
total_users, active_users = get_user_counts()
return {
'total_users': total_users,
'active_users': active_users,
'system_version': SYSTEM_VERSION,
'last_update': LAST_UPDATE
}
def handle_profile_update(form_data):
"""
處理個人資料更新
:param form_data: 表單數據
:return: tuple(bool, str) 操作是否成功及訊息
Returns:
dict: 包含系統統計資訊的字典
"""
try:
current_user.username = form_data.get('username', current_user.username)
current_user.email = form_data.get('email', current_user.email)
db.session.commit()
return True, '個人資料已更新'
# 計算時間閾值
active_threshold = datetime.utcnow() - timedelta(days=SystemConfig.ACTIVE_DAYS_THRESHOLD)
# 獲取用戶統計
total_users = User.query.count()
active_users = User.query.filter(User.last_login >= active_threshold).count()
return {
'total_users': total_users,
'active_users': active_users,
'system_version': SystemConfig.VERSION,
'last_update': SystemConfig.LAST_UPDATE,
}
except Exception as e:
db.session.rollback()
return False, f'更新失敗: {str(e)}'
current_app.logger.error(f"Error getting system statistics: {str(e)}")
return {
'total_users': 0,
'active_users': 0,
'system_version': SystemConfig.VERSION,
'last_update': SystemConfig.LAST_UPDATE,
}
def handle_password_update(form_data):
"""
處理密碼更新
:param form_data: 表單數據
:return: tuple(bool, str) 操作是否成功及訊息
"""
current_password = form_data.get('current_password')
new_password = form_data.get('new_password')
confirm_password = form_data.get('confirm_password')
class ProfileManager:
"""個人資料管理類"""
# 驗證密碼
if not current_user.check_password(current_password):
return False, '目前密碼不正確'
@staticmethod
def update_profile(form_data: dict) -> tuple:
"""
更新個人資料
if new_password != confirm_password:
return False, '新密碼與確認密碼不符'
Args:
form_data: 表單數據
try:
current_user.set_password(new_password)
db.session.commit()
return True, '密碼已更新'
except Exception as e:
db.session.rollback()
return False, f'更新失敗: {str(e)}'
Returns:
tuple: (是否成功, 提示訊息)
"""
try:
current_user.username = form_data.get('username', current_user.username)
current_user.email = form_data.get('email', current_user.email)
db.session.commit()
return True, '個人資料已更新'
except Exception as e:
db.session.rollback()
return False, f'更新失敗: {str(e)}'
@staticmethod
def update_password(form_data: dict) -> tuple:
"""
更新密碼
Args:
form_data: 表單數據
Returns:
tuple: (是否成功, 提示訊息)
"""
current_password = form_data.get('current_password')
new_password = form_data.get('new_password')
confirm_password = form_data.get('confirm_password')
# 驗證密碼
if not current_user.check_password(current_password):
return False, '目前密碼不正確'
if new_password != confirm_password:
return False, '新密碼與確認密碼不符'
try:
current_user.set_password(new_password)
db.session.commit()
return True, '密碼已更新'
except Exception as e:
db.session.rollback()
return False, f'更新失敗: {str(e)}'
def handle_settings_action(action, form_data):
"""
處理設定頁面的不同操作
:param action: 操作類型
:param form_data: 表單數據
:return: bool 操作是否成功
"""
handlers = {
'update_profile': handle_profile_update,
'update_password': handle_password_update
}
handler = handlers.get(action)
if not handler:
flash('無效的操作', 'danger')
return False
success, message = handler(form_data)
flash(message, 'success' if success else 'danger')
return success
# 視圖函數
@settings_bp.route('/', methods=['GET', 'POST'])
@login_required
def index():
"""設定頁面視圖函數"""
"""
設定頁面視圖
GET: 顯示設定頁面
POST: 處理設定更新請求
"""
if request.method == 'POST':
action = request.form.get('action')
handle_settings_action(action, request.form)
# 根據操作類型處理不同的設定更新
if action == 'update_profile':
success, message = ProfileManager.update_profile(request.form)
elif action == 'update_password':
success, message = ProfileManager.update_password(request.form)
else:
success, message = False, '無效的操作'
# 設置提示訊息
flash(message, 'success' if success else 'danger')
return redirect(url_for('settings.index'))
# 準備頁面數據
template_data = {
'title': '設定',
'stats': get_system_stats(),
'stats': get_system_statistics(),
}
return render_template('pages/settings.html', **template_data)
# 錯誤處理
@settings_bp.errorhandler(Exception)
def handle_error(error):
"""
統一錯誤處理
:param error: 錯誤實例
:return: redirect
Args:
error: 錯誤實例
Returns:
重定向到設定頁面
"""
db.session.rollback()
current_app.logger.error(f"Settings error: {str(error)}")
flash(f'發生錯誤: {str(error)}', 'danger')
return redirect(url_for('settings.index'))
# 系統維護相關路由
@settings_bp.route('/maintenance', methods=['POST'])
@login_required
def maintenance():
"""系統維護操作(需要管理員權限)"""
if not current_user.is_admin:
flash('無權限執行此操作', 'danger')
return redirect(url_for('settings.index'))
action = request.form.get('action')
try:
if action == 'clear_inactive_users':
# 清理不活躍用戶
threshold = datetime.utcnow() - timedelta(days=180) # 半年未登入
inactive_users = User.query.filter(User.last_login < threshold).all()
for user in inactive_users:
db.session.delete(user)
db.session.commit()
flash(f'已清理 {len(inactive_users)} 個不活躍用戶', 'success')
else:
flash('無效的維護操作', 'danger')
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Maintenance error: {str(e)}")
flash(f'維護操作失敗: {str(e)}', 'danger')
return redirect(url_for('settings.index'))

@ -1,12 +1,58 @@
from typing import Tuple, Optional
from app import db
class BaseService:
"""
基礎服務類
提供共用的資料庫操作方法
"""
@staticmethod
def commit():
def commit() -> Tuple[bool, Optional[str]]:
"""
提交資料庫變更
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
db.session.commit()
return True, None
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def save_to_db(model: db.Model) -> Tuple[bool, Optional[str]]:
"""
保存模型實例到資料庫
Args:
model: 資料庫模型實例
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
db.session.add(model)
return BaseService.commit()
except Exception as e:
return False, str(e)
@staticmethod
def delete_from_db(model: db.Model) -> Tuple[bool, Optional[str]]:
"""
從資料庫刪除模型實例
Args:
model: 資料庫模型實例
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
db.session.delete(model)
return BaseService.commit()
except Exception as e:
return False, str(e)

@ -1,55 +1,241 @@
from typing import Tuple, Optional, List, Dict
from datetime import datetime
from flask import current_app
from app import db
from app.models import Comment
from .base_service import BaseService
from typing import Tuple, Optional, List
class CommentService(BaseService):
"""留言服務類"""
# 配置常量
DEFAULT_PAGE_SIZE = 20
MAX_REPLY_DEPTH = 3 # 最大回覆深度
@staticmethod
def create_comment(user_id: int, post_id: int, content: str, parent_id: Optional[int] = None) -> Tuple[bool, Optional[str]]:
def create_comment(user_id: int, post_id: int, content: str,
parent_id: Optional[int] = None) -> Tuple[bool, Optional[str]]:
"""
創建新留言
Args:
user_id: 用戶ID
post_id: 文章ID
content: 留言內容
parent_id: 父留言ID用於回覆
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
# 驗證輸入
if not content.strip():
return False, "留言內容不能為空"
# 檢查回覆深度
if parent_id:
depth = CommentService.get_comment_depth(parent_id)
if depth >= CommentService.MAX_REPLY_DEPTH:
return False, f"最多只能回覆 {CommentService.MAX_REPLY_DEPTH}"
# 創建留言
comment = Comment(
user_id=user_id,
post_id=post_id,
content=content,
parent_id=parent_id
)
db.session.add(comment)
return CommentService.commit()
return CommentService.save_to_db(comment)
except Exception as e:
current_app.logger.error(f"Error creating comment: {str(e)}")
return False, str(e)
@staticmethod
def get_comment_by_id(comment_id: int) -> Optional[Comment]:
"""
通過ID獲取留言
Args:
comment_id: 留言ID
Returns:
Optional[Comment]: 留言實例或None
"""
return Comment.query.get(comment_id)
try:
return Comment.query.get(comment_id)
except Exception as e:
current_app.logger.error(f"Error getting comment: {str(e)}")
return None
@staticmethod
def delete_comment(comment_id: int) -> Tuple[bool, Optional[str]]:
"""
刪除留言
Args:
comment_id: 留言ID
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
comment = Comment.query.get(comment_id)
if not comment:
return False, "留言不存在"
db.session.delete(comment)
return CommentService.commit()
return CommentService.delete_from_db(comment)
except Exception as e:
current_app.logger.error(f"Error deleting comment: {str(e)}")
return False, str(e)
@classmethod
def get_post_comments(cls, post_id: int, page: int = 1,
per_page: int = None) -> Tuple[List[Comment], int]:
"""
獲取文章的留言列表
Args:
post_id: 文章ID
page: 頁碼
per_page: 每頁數量
Returns:
Tuple[List[Comment], int]: (留言列表, 總頁數)
"""
try:
per_page = per_page or cls.DEFAULT_PAGE_SIZE
pagination = Comment.query.filter_by(
post_id=post_id,
parent_id=None # 只獲取頂層留言
).order_by(
Comment.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
return pagination.items, pagination.pages
except Exception as e:
current_app.logger.error(f"Error getting post comments: {str(e)}")
return [], 0
@staticmethod
def get_comment_depth(comment_id: int) -> int:
"""
獲取留言的深度
Args:
comment_id: 留言ID
Returns:
int: 留言深度0表示頂層留言
"""
try:
depth = 0
comment = Comment.query.get(comment_id)
while comment and comment.parent_id:
depth += 1
comment = comment.parent
return depth
except Exception as e:
current_app.logger.error(f"Error getting comment depth: {str(e)}")
return 0
@staticmethod
def get_user_comments(user_id: int, page: int = 1,
per_page: int = 20) -> Tuple[List[Comment], int]:
"""
獲取用戶的所有留言
Args:
user_id: 用戶ID
page: 頁碼
per_page: 每頁數量
Returns:
Tuple[List[Comment], int]: (留言列表, 總頁數)
"""
try:
pagination = Comment.query.filter_by(
user_id=user_id
).order_by(
Comment.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
return pagination.items, pagination.pages
except Exception as e:
current_app.logger.error(f"Error getting user comments: {str(e)}")
return [], 0
@staticmethod
def update_comment(comment_id: int, content: str) -> Tuple[bool, Optional[str]]:
"""
更新留言內容
Args:
comment_id: 留言ID
content: 新的留言內容
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
if not content.strip():
return False, "留言內容不能為空"
comment = Comment.query.get(comment_id)
if not comment:
return False, "留言不存在"
comment.content = content
comment.updated_at = datetime.utcnow()
return CommentService.commit()
except Exception as e:
current_app.logger.error(f"Error updating comment: {str(e)}")
return False, str(e)
@staticmethod
def get_post_comments(post_id: int, page: int = 1, per_page: int = 20) -> Tuple[List[Comment], int]:
def get_comment_statistics(post_id: int) -> Dict:
"""
獲取文章的留言列表
獲取文章留言統計資訊
Args:
post_id: 文章ID
Returns:
Dict: 統計資訊字典
"""
pagination = Comment.query.filter_by(post_id=post_id)\
.order_by(Comment.created_at.desc())\
.paginate(page=page, per_page=per_page, error_out=False)
return pagination.items, pagination.pages
try:
total_comments = Comment.query.filter_by(post_id=post_id).count()
total_replies = Comment.query.filter(
Comment.post_id == post_id,
Comment.parent_id.isnot(None)
).count()
return {
'total_comments': total_comments,
'total_replies': total_replies,
'root_comments': total_comments - total_replies
}
except Exception as e:
current_app.logger.error(f"Error getting comment statistics: {str(e)}")
return {
'total_comments': 0,
'total_replies': 0,
'root_comments': 0
}

@ -1,94 +1,162 @@
from typing import Tuple, List, Dict, Optional
from datetime import datetime, timedelta
from flask import current_app
from sqlalchemy import func
from app import db
from app.models import Like, Post
from .base_service import BaseService
from typing import Tuple, Optional
class LikeService(BaseService):
"""按讚服務類"""
@staticmethod
def toggle_like(user_id: int, post_id: int) -> Tuple[bool, bool, int]:
"""
切換文章的按讚狀態
:param user_id: 用戶ID
:param post_id: 文章ID
:return: Tuple[是否成功, 當前是否為按讚狀態, 當前按讚數]
Args:
user_id: 用戶ID
post_id: 文章ID
Returns:
Tuple[bool, bool, int]: (是否成功, 當前是否為按讚狀態, 當前按讚數)
"""
try:
# 檢查文章是否存在
post = Post.query.get(post_id)
if not post:
return False, False, 0
# 檢查是否已經按讚
existing_like = Like.query.filter_by(
post_id=post_id,
user_id=user_id
).first()
post = Post.query.get(post_id)
if not post:
return False, False, 0
if existing_like:
# 如果已按讚,則取消
# 取消按讚
db.session.delete(existing_like)
db.session.commit()
return True, False, post.like_count
else:
# 如果未按讚,則添加
# 新增按讚
new_like = Like(post_id=post_id, user_id=user_id)
db.session.add(new_like)
db.session.commit()
return True, True, post.like_count
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error toggling like: {str(e)}")
return False, False, 0
@staticmethod
def is_post_liked_by_user(post_id: int, user_id: int) -> bool:
"""
檢查用戶是否已對文章按讚
:param post_id: 文章ID
:param user_id: 用戶ID
:return: 是否已按讚
Args:
post_id: 文章ID
user_id: 用戶ID
Returns:
bool: 是否已按讚
"""
return Like.query.filter_by(
post_id=post_id,
user_id=user_id
).first() is not None
try:
return Like.query.filter_by(
post_id=post_id,
user_id=user_id
).first() is not None
except Exception as e:
current_app.logger.error(f"Error checking like status: {str(e)}")
return False
@staticmethod
def get_post_likes_count(post_id: int) -> int:
"""
獲取文章的按讚數量
:param post_id: 文章ID
:return: 按讚數量
"""
return Like.query.filter_by(post_id=post_id).count()
@staticmethod
def get_user_liked_posts(user_id: int, page: int = 1, per_page: int = 10) -> Tuple[list, int]:
def get_user_liked_posts(user_id: int, page: int = 1,
per_page: int = 10) -> Tuple[List[Post], int]:
"""
獲取用戶按讚的文章列表
:param user_id: 用戶ID
:param page: 頁碼
:param per_page: 每頁數量
:return: Tuple[文章列表, 總頁數]
Args:
user_id: 用戶ID
page: 頁碼
per_page: 每頁數量
Returns:
Tuple[List[Post], int]: (文章列表, 總頁數)
"""
pagination = Post.query.join(Like).filter(
Like.user_id == user_id
).order_by(Like.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return pagination.items, pagination.pages
try:
pagination = Post.query.join(
Like
).filter(
Like.user_id == user_id
).order_by(
Like.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
return pagination.items, pagination.pages
except Exception as e:
current_app.logger.error(f"Error getting liked posts: {str(e)}")
return [], 0
@staticmethod
def get_post_likers(post_id: int, page: int = 1, per_page: int = 20) -> list:
def get_post_likes_statistics(post_id: int) -> Dict:
"""
獲取文章的按讚用戶列表
:param post_id: 文章ID
:param page: 頁碼
:param per_page: 每頁數量
:return: 用戶列表
獲取文章按讚統計資訊
Args:
post_id: 文章ID
Returns:
Dict: 統計資訊字典
"""
from app.models import User
return User.query.join(Like).filter(
Like.post_id == post_id
).order_by(Like.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
try:
total_likes = Like.query.filter_by(post_id=post_id).count()
recent_likes = Like.query.filter(
Like.post_id == post_id,
Like.created_at >= datetime.utcnow() - timedelta(days=7)
).count()
return {
'total_likes': total_likes,
'recent_likes': recent_likes,
'trend': recent_likes / total_likes if total_likes > 0 else 0
}
except Exception as e:
current_app.logger.error(f"Error getting like statistics: {str(e)}")
return {
'total_likes': 0,
'recent_likes': 0,
'trend': 0
}
@staticmethod
def get_trending_posts(days: int = 7, limit: int = 10) -> List[Post]:
"""
獲取趨勢文章根據最近按讚數
Args:
days: 統計天數
limit: 返回數量
Returns:
List[Post]: 趨勢文章列表
"""
try:
since = datetime.utcnow() - timedelta(days=days)
return Post.query.join(
Like
).filter(
Like.created_at >= since
).group_by(
Post.id
).order_by(
func.count(Like.id).desc()
).limit(limit).all()
except Exception as e:
current_app.logger.error(f"Error getting trending posts: {str(e)}")
return []

@ -1,72 +1,110 @@
from typing import Tuple, Optional, Any, List
from datetime import datetime, timedelta
from sqlalchemy import or_, func
from flask import current_app
from app import db
from app.models import Post
from .base_service import BaseService
from typing import Tuple, Optional, Any
from datetime import datetime
from sqlalchemy import or_
class PostService(BaseService):
"""文章服務類"""
# 配置常量
DEFAULT_PAGE_SIZE = 10
EXCERPT_LENGTH = 200
@staticmethod
def create_post(user_id: int, title: str, content: str) -> Tuple[Optional[Post], Optional[str]]:
"""
創建新文章
:param user_id: 用戶ID
:param title: 文章標題
:param content: 文章內容
:return: (Post object, error message)
Args:
user_id: 用戶ID
title: 文章標題
content: 文章內容
Returns:
Tuple[Optional[Post], Optional[str]]: (文章實例, 錯誤訊息)
"""
try:
# 驗證輸入
if not title.strip():
return None, "標題不能為空"
if not content.strip():
return None, "內容不能為空"
# 創建文章
post = Post(
title=title,
content=content,
user_id=user_id
)
db.session.add(post)
success, error = PostService.commit()
if success:
return post, None
return None, error
success, error = PostService.save_to_db(post)
return (post, None) if success else (None, error)
except Exception as e:
current_app.logger.error(f"Error creating post: {str(e)}")
return None, str(e)
@staticmethod
def get_post_by_id(post_id: int) -> Optional[Post]:
"""
通過ID獲取文章
:param post_id: 文章ID
:return: Post object or None
"""
return Post.query.get(post_id)
@staticmethod
def get_posts_page(page: int = 1, per_page: int = 10) -> Any:
Args:
post_id: 文章ID
Returns:
Optional[Post]: 文章實例或None
"""
try:
return Post.query.get(post_id)
except Exception as e:
current_app.logger.error(f"Error getting post: {str(e)}")
return None
@classmethod
def get_posts_page(cls, page: int = 1, per_page: int = None) -> Any:
"""
獲取分頁的文章列表
:param page: 頁碼
:param per_page: 每頁數量
:return: Pagination object
Args:
page: 頁碼
per_page: 每頁數量
Returns:
分頁對象
"""
return Post.query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
try:
per_page = per_page or cls.DEFAULT_PAGE_SIZE
return Post.query.order_by(
Post.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
except Exception as e:
current_app.logger.error(f"Error getting posts page: {str(e)}")
return None
@staticmethod
def update_post(post_id: int, title: str, content: str) -> Tuple[bool, Optional[str]]:
"""
更新文章
:param post_id: 文章ID
:param title: 新標題
:param content: 新內容
:return: (success boolean, error message)
Args:
post_id: 文章ID
title: 新標題
content: 新內容
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
# 驗證輸入
if not title.strip():
return False, "標題不能為空"
if not content.strip():
@ -76,57 +114,142 @@ class PostService(BaseService):
if not post:
return False, "文章不存在"
# 更新文章
post.title = title
post.content = content
post.updated_at = datetime.utcnow()
return PostService.commit()
except Exception as e:
current_app.logger.error(f"Error updating post: {str(e)}")
return False, str(e)
@staticmethod
def delete_post(post_id: int) -> Tuple[bool, Optional[str]]:
"""
刪除文章
:param post_id: 文章ID
:return: (success boolean, error message)
Args:
post_id: 文章ID
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
post = Post.query.get(post_id)
if not post:
return False, "文章不存在"
db.session.delete(post)
return PostService.commit()
return PostService.delete_from_db(post)
except Exception as e:
current_app.logger.error(f"Error deleting post: {str(e)}")
return False, str(e)
@staticmethod
def get_user_posts(user_id: int, page: int = 1, per_page: int = 10) -> Any:
@classmethod
def get_user_posts(cls, user_id: int, page: int = 1, per_page: int = None) -> Any:
"""
獲取指定用戶的文章列表
:param user_id: 用戶ID
:param page: 頁碼
:param per_page: 每頁數量
:return: Pagination object
"""
return Post.query.filter_by(user_id=user_id) \
.order_by(Post.created_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
@staticmethod
def search_posts(query: str, page: int = 1, per_page: int = 10) -> Any:
Args:
user_id: 用戶ID
page: 頁碼
per_page: 每頁數量
Returns:
分頁對象
"""
try:
per_page = per_page or cls.DEFAULT_PAGE_SIZE
return Post.query.filter_by(
user_id=user_id
).order_by(
Post.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
except Exception as e:
current_app.logger.error(f"Error getting user posts: {str(e)}")
return None
@classmethod
def search_posts(cls, query: str, page: int = 1, per_page: int = None) -> Any:
"""
搜索文章
:param query: 搜索關鍵字
:param page: 頁碼
:param per_page: 每頁數量
:return: Pagination object
Args:
query: 搜索關鍵字
page: 頁碼
per_page: 每頁數量
Returns:
分頁對象
"""
return Post.query.filter(
or_(
Post.title.ilike(f'%{query}%'),
Post.content.ilike(f'%{query}%')
try:
per_page = per_page or cls.DEFAULT_PAGE_SIZE
return Post.query.filter(
or_(
Post.title.ilike(f'%{query}%'),
Post.content.ilike(f'%{query}%')
)
).order_by(
Post.created_at.desc()
).paginate(
page=page,
per_page=per_page,
error_out=False
)
).order_by(Post.created_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
except Exception as e:
current_app.logger.error(f"Error searching posts: {str(e)}")
return None
@staticmethod
def get_latest_posts(limit: int = 5) -> List[Post]:
"""
獲取最新文章
Args:
limit: 限制數量
Returns:
List[Post]: 文章列表
"""
try:
return Post.query.order_by(
Post.created_at.desc()
).limit(limit).all()
except Exception as e:
current_app.logger.error(f"Error getting latest posts: {str(e)}")
return []
@staticmethod
def get_trending_posts(days: int = 7, limit: int = 10) -> List[Post]:
"""
獲取趨勢文章根據最近按讚數
Args:
days: 統計天數
limit: 返回數量
Returns:
List[Post]: 趨勢文章列表
"""
try:
from app.models import Like # 僅在需要時導入 Like
since = datetime.utcnow() - timedelta(days=days)
return Post.query.join(
Like
).filter(
Post.created_at >= since
).group_by(
Post.id
).order_by(
func.count(Like.id).desc()
).limit(limit).all()
except Exception as e:
current_app.logger.error(f"Error getting trending posts: {str(e)}")
return []

@ -1,125 +1,174 @@
import os
from typing import Tuple, Optional, Dict, List
from datetime import datetime, timedelta
from werkzeug.utils import secure_filename
from PIL import Image
from datetime import datetime, timedelta
from app import db
from flask import current_app
from app.models import User
from .base_service import BaseService
from typing import Tuple, Optional, Any
class UserService(BaseService):
"""用戶服務類"""
# 配置常量
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
AVATAR_SIZE = (300, 300)
AVATAR_SIZE = (300, 300) # 頭像尺寸
AVATAR_QUALITY = 85 # 圖片品質
@staticmethod
def create_user(username: str, email: str, password: str) -> Tuple[Optional[User], Optional[str]]:
"""
創建新用戶
:param username: 用戶名
:param email: 電子郵件
:param password: 密碼
:return: (User object, error message)
Args:
username: 用戶名
email: 電子郵件
password: 密碼
Returns:
Tuple[Optional[User], Optional[str]]: (用戶實例, 錯誤訊息)
"""
try:
# 檢查用戶名是否已存在
# 檢查用戶名和郵箱是否已存在
if User.query.filter_by(username=username).first():
return None, "用戶名已被使用"
# 檢查郵箱是否已存在
if User.query.filter_by(email=email).first():
return None, "郵箱已被註冊"
# 創建新用戶
user = User(
username=username,
email=email
)
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
success, error = UserService.commit()
success, error = UserService.save_to_db(user)
return (user, None) if success else (None, error)
if success:
return user, None
return None, error
except Exception as e:
current_app.logger.error(f"Error creating user: {str(e)}")
return None, str(e)
@staticmethod
def get_user_by_id(user_id: int) -> Optional[User]:
"""
通過ID獲取用戶
:param user_id: 用戶ID
:return: User object or None
"""
"""根據ID獲取用戶"""
return User.query.get(user_id)
@staticmethod
def get_user_by_email(email: str) -> Optional[User]:
"""
通過郵箱獲取用戶
:param email: 郵箱
:return: User object or None
"""
"""根據郵箱獲取用戶"""
return User.query.filter_by(email=email).first()
@staticmethod
def update_profile(user_id: int, username: str = None, email: str = None) -> Tuple[bool, Optional[str]]:
def update_profile(user_id: int, username: Optional[str] = None,
email: Optional[str] = None) -> Tuple[bool, Optional[str]]:
"""
更新用戶資料
:param user_id: 用戶ID
:param username: 新用戶名
:param email: 新郵箱
:return: (success, error message)
Args:
user_id: 用戶ID
username: 新用戶名
email: 新郵箱
Returns:
Tuple[bool, str]: (是否成功, 錯誤訊息)
"""
try:
user = User.query.get(user_id)
user = UserService.get_user_by_id(user_id)
if not user:
return False, "用戶不存在"
if username:
# 檢查新用戶名是否已被使用
existing_user = User.query.filter_by(username=username).first()
if existing_user and existing_user.id != user_id:
return False, "用戶名已被使用"
user.username = username
if email:
# 檢查新郵箱是否已被註冊
existing_user = User.query.filter_by(email=email).first()
if existing_user and existing_user.id != user_id:
return False, "郵箱已被註冊"
user.email = email
return UserService.commit()
except Exception as e:
current_app.logger.error(f"Error updating profile: {str(e)}")
return False, str(e)
@staticmethod
def allowed_file(filename: str) -> bool:
def update_password(user_id: int, current_password: str, new_password: str) -> Tuple[bool, Optional[str]]:
"""
檢查文件是否允許上傳
:param filename: 文件名
:return: bool
更新用戶密碼
Args:
user_id: 用戶ID
current_password: 當前密碼
new_password: 新密碼
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in UserService.ALLOWED_EXTENSIONS
try:
# 獲取用戶
user = UserService.get_user_by_id(user_id)
if not user:
return False, "用戶不存在"
# 驗證當前密碼
if not user.check_password(current_password):
return False, "當前密碼不正確"
# 驗證新密碼
if len(new_password) < 6:
return False, "新密碼長度不能小於6位"
# 更新密碼
user.set_password(new_password)
return UserService.commit()
except Exception as e:
current_app.logger.error(f"Error updating password: {str(e)}")
return False, str(e)
@staticmethod
def process_avatar(file, user_id: int, app) -> Tuple[bool, Optional[str], Optional[str]]:
def update_last_login(user_id: int) -> Tuple[bool, Optional[str]]:
"""
更新用戶最後登入時間
Args:
user_id: 用戶ID
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
user = UserService.get_user_by_id(user_id)
if not user:
return False, "用戶不存在"
user.last_login = datetime.utcnow()
return UserService.commit()
except Exception as e:
current_app.logger.error(f"Error updating last login: {str(e)}")
return False, str(e)
@classmethod
def process_avatar(cls, file, user_id: int) -> Tuple[bool, Optional[str], Optional[str]]:
"""
處理頭像上傳
:param file: 上傳的文件
:param user_id: 用戶ID
:param app: Flask app對象
:return: (success, file_path, error_message)
Args:
file: 上傳的文件
user_id: 用戶ID
Returns:
Tuple[bool, Optional[str], Optional[str]]: (是否成功, 文件路徑, 錯誤訊息)
"""
try:
if not file:
return False, None, "未選擇文件"
if not UserService.allowed_file(file.filename):
if not cls.allowed_file(file.filename):
return False, None, "不支持的文件格式"
# 生成安全的文件名
@ -127,15 +176,13 @@ class UserService(BaseService):
filename = secure_filename(f"avatar_{user_id}_{timestamp}.jpg")
# 確保上傳目錄存在
upload_dir = os.path.join(app.static_folder, 'uploads', 'avatars')
upload_dir = os.path.join(current_app.static_folder, 'uploads', 'avatars')
os.makedirs(upload_dir, exist_ok=True)
filepath = os.path.join(upload_dir, filename)
# 處理圖片
image = Image.open(file)
# 將圖片轉換為RGB模式處理PNG等格式
if image.mode in ('RGBA', 'P'):
image = image.convert('RGB')
@ -144,123 +191,162 @@ class UserService(BaseService):
size = min(width, height)
left = (width - size) // 2
top = (height - size) // 2
right = left + size
bottom = top + size
image = image.crop((left, top, right, bottom))
image = image.crop((left, top, left + size, top + size))
# 調整大小
image = image.resize(UserService.AVATAR_SIZE, Image.Resampling.LANCZOS)
image = image.resize(cls.AVATAR_SIZE, Image.Resampling.LANCZOS)
# 保存圖片
image.save(filepath, 'JPEG', quality=85)
image.save(filepath, 'JPEG', quality=cls.AVATAR_QUALITY)
# 返回相對路徑
return True, f"uploads/avatars/{filename}", None
except Exception as e:
current_app.logger.error(f"Error processing avatar: {str(e)}")
return False, None, str(e)
@classmethod
def allowed_file(cls, filename: str) -> bool:
"""檢查文件是否為允許的格式"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in cls.ALLOWED_EXTENSIONS
@staticmethod
def update_avatar(user_id: int, file, app) -> Tuple[bool, Optional[str]]:
def update_avatar(user_id: int, file) -> Tuple[bool, Optional[str]]:
"""
更新用戶頭像
:param user_id: 用戶ID
:param file: 上傳的文件
:param app: Flask app對象
:return: (success, error_message)
Args:
user_id: 用戶ID
file: 上傳的文件
Returns:
Tuple[bool, Optional[str]]: (是否成功, 錯誤訊息)
"""
try:
# 處理新頭像
success, file_path, error = UserService.process_avatar(file, user_id, app)
if not success:
return False, error
# 更新數據庫
user = User.query.get(user_id)
user = UserService.get_user_by_id(user_id)
if not user:
return False, "用戶不存在"
# 處理新頭像
success, file_path, error = UserService.process_avatar(file, user_id)
if not success:
return False, error
# 刪除舊頭像
if user.avatar_path:
old_avatar = os.path.join(app.static_folder, user.avatar_path)
old_avatar = os.path.join(current_app.static_folder, user.avatar_path)
if os.path.exists(old_avatar):
os.remove(old_avatar)
# 更新頭像路徑
user.avatar_path = file_path
db.session.commit()
return True, None
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def update_password(user_id: int, current_password: str, new_password: str) -> Tuple[bool, Optional[str]]:
"""
更新密碼
:param user_id: 用戶ID
:param current_password: 當前密碼
:param new_password: 新密碼
:return: (success, error_message)
"""
try:
user = User.query.get(user_id)
if not user:
return False, "用戶不存在"
if not user.check_password(current_password):
return False, "當前密碼不正確"
if len(new_password) < 6:
return False, "新密碼長度不能小於6位"
user.set_password(new_password)
return UserService.commit()
except Exception as e:
current_app.logger.error(f"Error updating avatar: {str(e)}")
return False, str(e)
@staticmethod
def update_last_login(user_id: int) -> None:
@classmethod
def process_avatar(cls, file, user_id: int) -> Tuple[bool, Optional[str], Optional[str]]:
"""
更新最後登入時間
:param user_id: 用戶ID
處理頭像上傳
Args:
file: 上傳的文件
user_id: 用戶ID
Returns:
Tuple[bool, Optional[str], Optional[str]]: (是否成功, 文件路徑, 錯誤訊息)
"""
try:
user = User.query.get(user_id)
if user:
user.last_login = datetime.utcnow()
db.session.commit()
except Exception:
db.session.rollback()
if not file:
return False, None, "未選擇文件"
if not cls.allowed_file(file.filename):
return False, None, "不支持的文件格式"
# 生成安全的文件名
timestamp = int(datetime.utcnow().timestamp())
filename = secure_filename(f"avatar_{user_id}_{timestamp}.jpg")
# 確保上傳目錄存在
upload_dir = os.path.join(current_app.static_folder, 'uploads', 'avatars')
os.makedirs(upload_dir, exist_ok=True)
filepath = os.path.join(upload_dir, filename)
# 處理圖片
image = Image.open(file)
if image.mode in ('RGBA', 'P'):
image = image.convert('RGB')
# 裁剪為正方形
width, height = image.size
size = min(width, height)
left = (width - size) // 2
top = (height - size) // 2
image = image.crop((left, top, left + size, top + size))
# 調整大小
image = image.resize(cls.AVATAR_SIZE, Image.Resampling.LANCZOS)
# 保存圖片
image.save(filepath, 'JPEG', quality=cls.AVATAR_QUALITY)
return True, f"uploads/avatars/{filename}", None
except Exception as e:
current_app.logger.error(f"Error processing avatar: {str(e)}")
return False, None, str(e)
@staticmethod
def get_users_page(page: int = 1, per_page: int = 16) -> Any:
def get_user_stats(user_id: int) -> Dict:
"""
獲取分頁的用戶列表
:param page: 頁碼
:param per_page: 每頁數量
:return: Pagination object
獲取指定用戶的統計資訊
Args:
user_id: 用戶ID
Returns:
Dict: 用戶統計資訊
"""
return User.query.order_by(User.created_at.desc()) \
.paginate(page=page, per_page=per_page, error_out=False)
try:
user = UserService.get_user_by_id(user_id)
if not user:
return {'posts_count': 0, 'received_likes': 0}
return {
'posts_count': user.posts_count,
'received_likes': user.received_likes_count
}
except Exception as e:
current_app.logger.error(f"Error getting user stats: {str(e)}")
return {'posts_count': 0, 'received_likes': 0}
@staticmethod
def get_active_users_count(days: int = 30) -> int:
def get_user_statistics() -> Dict:
"""
獲取活躍用戶數量
:param days: 天數
:return: 活躍用戶數量
"""
since = datetime.utcnow() - timedelta(days=days)
return User.query.filter(User.last_login >= since).count()
獲取用戶統計資料
@staticmethod
def get_new_users_count(days: int = 30) -> int:
Returns:
Dict: 統計數據字典
"""
獲取新增用戶數量
:param days: 天數
:return: 新增用戶數量
"""
since = datetime.utcnow() - timedelta(days=days)
return User.query.filter(User.created_at >= since).count()
try:
# 計算本月新增用戶
first_day_of_month = datetime.utcnow().replace(
day=1, hour=0, minute=0, second=0, microsecond=0
)
return {
'total': User.query.count(),
'new_this_month': User.query.filter(
User.created_at >= first_day_of_month
).count(),
'active': User.query.filter(
User.last_login >= datetime.utcnow() - timedelta(days=30)
).count()
}
except Exception as e:
current_app.logger.error(f"Error getting user statistics: {str(e)}")
return {'total': 0, 'new_this_month': 0, 'active': 0}

@ -56,7 +56,7 @@ main {
border: none;
border-radius: var(--card-border-radius);
box-shadow: 0 2px 4px rgba(0,0,0,.05);
transition: transform var(--transition-speed) ease,
transition: transform var(--transition-speed) ease,
box-shadow var(--transition-speed) ease;
}
@ -235,16 +235,16 @@ main {
.display-4 {
font-size: 2.5rem;
}
.container {
padding-left: 1rem;
padding-right: 1rem;
}
.card-body {
padding: 1rem;
}
.btn {
padding: 0.375rem 1rem;
}
@ -254,7 +254,7 @@ main {
main {
padding: 1rem 0;
}
.display-4 {
font-size: 2rem;
}
@ -271,4 +271,4 @@ main {
.border-primary {
border-color: var(--bs-primary) !important;
}
}

@ -1,9 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-4">
{% if current_user.is_authenticated %}
<!-- 歡迎區塊 - 僅登入用戶可見 -->
<!-- 用戶歡迎卡片 -->
<div class="card bg-primary text-white shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
@ -11,8 +10,7 @@
{% if current_user.avatar_path %}
<img src="{{ url_for('static', filename=current_user.avatar_path) }}"
class="rounded-circle border border-2 border-white"
style="width: 80px; height: 80px; object-fit: cover;"
alt="{{ current_user.username }}的頭像">
style="width: 80px; height: 80px; object-fit: cover;">
{% else %}
<div class="avatar-circle bg-white text-primary d-flex align-items-center justify-content-center"
style="width: 80px; height: 80px; border-radius: 50%; font-size: 2rem;">
@ -22,27 +20,143 @@
</div>
<div class="col">
<h4 class="mb-1">歡迎回來,{{ current_user.username }}</h4>
<p class="mb-0 opacity-75">
<i class="bi bi-clock"></i>
上次登入時間:{{
<div class="small">
<i class="bi bi-clock"></i> 上次登入時間:{{
current_user.last_login.strftime('%Y-%m-%d %H:%M') if current_user.last_login else '首次登入'
}}
</p>
</div>
<div class="mt-2">
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-light btn-sm">
<i class="bi bi-person-gear"></i> 管理個人資料
</a>
<a href="{{ url_for('post.create') }}" class="btn btn-outline-light btn-sm ms-2">
<i class="bi bi-plus-lg"></i> 發布文章
</a>
</div>
</div>
<div class="col-auto">
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-light">
<i class="bi bi-person-gear"></i> 管理個人資料
</a>
<div class="col-md-3 text-end">
<div class="border-start border-white-50 ps-3">
<div class="mb-2">
<div class="small text-white-50">我的文章</div>
<div class="h4 mb-0">{{ user_stats.posts_count }}</div>
</div>
<div>
<div class="small text-white-50">收到的讚</div>
<div class="h4 mb-0">{{ user_stats.received_likes }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- 主要內容區 -->
<div class="row g-4">
<!-- 左側主要內容 -->
<!-- 左側主要內容 -->
<div class="col-lg-8">
<!-- 熱門文章輪播 -->
{% if latest_posts %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-star-fill text-warning"></i> 精選文章
</h5>
<div>
<button class="btn btn-sm btn-outline-secondary me-1" data-bs-target="#featuredPosts"
data-bs-slide="prev">
<i class="bi bi-chevron-left"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" data-bs-target="#featuredPosts"
data-bs-slide="next">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<div id="featuredPosts" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner">
{% for post in latest_posts[:3] %}
<div class="carousel-item {% if loop.first %}active{% endif %}">
<div class="p-4" style="height: 400px; overflow: hidden;">
<div class="d-flex flex-column h-100">
<!-- 作者資訊 -->
<div class="d-flex align-items-center mb-3">
{% if post.author.avatar_path %}
<img src="{{ url_for('static', filename=post.author.avatar_path) }}"
class="rounded-circle me-2"
style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center me-2"
style="width: 40px; height: 40px; border-radius: 50%; font-size: 1.2rem;">
{{ post.author.username[0].upper() }}
</div>
{% endif %}
<div>
<div class="fw-medium">{{ post.author.username }}</div>
<div class="small text-muted">
{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}
</div>
</div>
</div>
<!-- 文章內容 -->
<div class="flex-grow-1 overflow-hidden">
<h4 class="card-title mb-3">
<a href="{{ url_for('post.show', id=post.id) }}"
class="text-decoration-none text-dark">
{{ post.title }}
</a>
</h4>
<p class="card-text text-muted"
style="display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden;">
{{ post.content }}
</p>
</div>
<!-- 文章統計 -->
<div class="d-flex justify-content-between align-items-center mt-3 pt-3 border-top">
<div class="d-flex align-items-center text-muted">
<div class="me-3">
<i class="bi bi-heart-fill text-danger"></i>
<span class="ms-1">{{ post.like_count }}</span>
</div>
<div class="me-3">
<i class="bi bi-chat-fill"></i>
<span class="ms-1">{{ post.comments_count }}</span>
</div>
<div>
<i class="bi bi-eye-fill"></i>
<span class="ms-1">{{
post.view_count if post.view_count else 0
}}</span>
</div>
</div>
<a href="{{ url_for('post.show', id=post.id) }}"
class="btn btn-primary btn-sm">
閱讀更多 <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="carousel-indicators" style="bottom: 0;">
{% for post in latest_posts[:3] %}
<button type="button"
data-bs-target="#featuredPosts"
data-bs-slide-to="{{ loop.index0 }}"
class="{% if loop.first %}active{% endif %}"
style="background-color: #0d6efd;"></button>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- 最新文章列表 -->
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">最新文章</h5>
@ -50,53 +164,84 @@
查看全部 <i class="bi bi-arrow-right"></i>
</a>
</div>
<div class="card-body">
{% if latest_posts %}
{% for post in latest_posts %}
<div class="card mb-3 border-0">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
{% if post.author.avatar_path %}
<img src="{{ url_for('static', filename=post.author.avatar_path) }}"
class="rounded-circle me-2"
style="width: 32px; height: 32px; object-fit: cover;"
alt="{{ post.author.username }}的頭像">
{% else %}
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; border-radius: 50%; font-size: 1rem;">
{{ post.author.username[0].upper() }}
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
{% if post.author.avatar_path %}
<img src="{{ url_for('static', filename=post.author.avatar_path) }}"
class="rounded-circle me-2"
style="width: 32px; height: 32px; object-fit: cover;">
{% else %}
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; border-radius: 50%; font-size: 1rem;">
{{ post.author.username[0].upper() }}
</div>
{% endif %}
<div>
<div class="fw-medium">{{ post.author.username }}</div>
<small class="text-muted">{{
post.created_at.strftime('%Y-%m-%d %H:%M')
}}</small>
</div>
</div>
{% endif %}
<div class="small">
<span class="fw-medium">{{ post.author.username }}</span>
<span class="text-muted">• {{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<div class="d-flex align-items-center text-muted small">
<div class="me-3">
<i class="bi bi-heart-fill text-danger"></i>
{{ post.like_count }}
</div>
<div>
<i class="bi bi-chat-fill"></i>
{{ post.comments_count }}
</div>
</div>
</div>
<h6 class="card-title mb-2">
<a href="{{ url_for('post.show', id=post.id) }}"
class="text-decoration-none text-dark">
<h6 class="card-title">
<a href="{{ url_for('post.show', id=post.id) }}" class="text-decoration-none text-dark">
{{ post.title }}
</a>
</h6>
<p class="card-text small text-muted mb-0">
{{ post.content[:100] }}{% if post.content|length > 100 %}...{% endif %}
</p>
</div>
{% if not loop.last %}
{% endif %}
<p class="card-text small text-muted">
{{ post.content[:150] }}{% if post.content|length > 150 %}...{% endif %}
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('post.show', id=post.id) }}" class="btn btn-outline-primary btn-sm">
閱讀更多
</a>
{% if current_user == post.author %}
<div class="btn-group">
<a href="{{ url_for('post.edit', id=post.id) }}"
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-pencil"></i> 編輯
</a>
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal"
data-bs-target="#deleteModal" data-post-id="{{ post.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% if not loop.last %}
<hr class="my-3">
{% endif %}
{% endfor %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-file-text" style="font-size: 2rem;"></i>
<p class="text-muted mt-2 mb-0">目前還沒有文章</p>
<i class="bi bi-journal-text display-4 text-muted"></i>
<p class="mt-3 mb-0">目前還沒有文章</p>
{% if current_user.is_authenticated %}
<div class="mt-3">
<a href="{{ url_for('post.create') }}" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> 發布第一篇文章
</a>
</div>
<a href="{{ url_for('post.create') }}" class="btn btn-primary mt-3">
<i class="bi bi-plus-lg"></i> 發布第一篇文章
</a>
{% endif %}
</div>
{% endif %}
@ -109,42 +254,81 @@
<!-- 系統資訊卡片 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="card-title mb-0">系統資訊</h5>
<h5 class="card-title mb-0">
<i class="bi bi-info-circle-fill text-primary"></i> 系統資訊
</h5>
</div>
<div class="card-body">
<div class="list-group list-group-flush">
<div class="list-group-item px-0">
<div class="d-flex justify-content-between">
<span><i class="bi bi-people"></i> 總會員數</span>
<span class="fw-bold">{{ total_users }}</span>
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-people"></i> 總會員數
</div>
<span class="badge bg-primary rounded-pill">{{ total_users }}</span>
</div>
</div>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between">
<span><i class="bi bi-person-add"></i> 本月新增</span>
<span class="fw-bold">{{ new_users_this_month }}</span>
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-person-plus"></i> 本月新增
</div>
<span class="badge bg-success rounded-pill">{{ new_users_this_month }}</span>
</div>
</div>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between">
<span><i class="bi bi-file-text"></i> 總文章數</span>
<span class="fw-bold">{{ total_posts }}</span>
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-file-text"></i> 總文章數
</div>
<span class="badge bg-info rounded-pill">{{ total_posts }}</span>
</div>
</div>
<div class="list-group-item px-0">
<div class="d-flex justify-content-between">
<span><i class="bi bi-clock-history"></i> 最後更新</span>
<span>{{ last_update }}</span>
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-clock-history"></i> 最後更新
</div>
<small class="text-muted">{{ last_update }}</small>
</div>
</div>
</div>
</div>
</div>
<!-- 活躍用戶卡片 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="card-title mb-0">
<i class="bi bi-star-fill text-warning"></i> 活躍用戶
</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
{% for user in active_users[:12] %}
<a href="#" class="text-decoration-none" data-bs-toggle="tooltip" title="{{ user.username }}">
{% if user.avatar_path %}
<img src="{{ url_for('static', filename=user.avatar_path) }}"
class="rounded-circle border"
style="width: 40px; height: 40px; object-fit: cover;">
{% else %}
<div class="avatar-circle bg-primary text-white d-flex align-items-center justify-content-center"
style="width: 40px; height: 40px; border-radius: 50%; font-size: 1.2rem;">
{{ user.username[0].upper() }}
</div>
{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
<!-- 快速連結卡片 -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="card-title mb-0">快速連結</h5>
<h5 class="card-title mb-0">
<i class="bi bi-link-45deg text-primary"></i> 快速連結
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
@ -154,7 +338,7 @@
</a>
{% endif %}
<a href="{{ url_for('post.index') }}" class="btn btn-outline-primary">
<i class="bi bi-file-text"></i> 所有文章
<i class="bi bi-journal-text"></i> 所有文章
</a>
<a href="{{ url_for('main.members') }}" class="btn btn-outline-primary">
<i class="bi bi-people"></i> 會員列表
@ -173,4 +357,50 @@
</div>
</div>
</div>
<!-- 刪除文章確認對話框 -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">確認刪除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-0">確定要刪除這篇文章嗎?此操作無法復原。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<form id="deleteForm" method="post" style="display: inline;">
<button type="submit" class="btn btn-danger">確定刪除</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 初始化工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// 處理刪除文章
document.getElementById('deleteModal').addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const postId = button.getAttribute('data-post-id');
const form = document.getElementById('deleteForm');
form.action = `/posts/${postId}/delete`;
});
// 初始化輪播
var carousel = new bootstrap.Carousel(document.getElementById('featuredPosts'), {
interval: 5000,
wrap: true,
touch: true
});
</script>
{% endblock %}

@ -75,7 +75,7 @@
<div class="card-header bg-white">
<h5 class="card-title mb-0">
留言區
<span class="badge bg-secondary">{{ post.comment_count }}</span>
<span class="badge bg-secondary">{{ post.comments_count }}</span>
</h5>
</div>
<div class="card-body">