初始化技术博客项目
功能特性: - 前端文章展示 (Markdown渲染、代码高亮) - 后台管理系统 (文章、分类、标签、作者管理) - 图片上传 (点击、拖拽、粘贴) - 网站设置 (Logo、底部宣传图片上传) - 访问统计 (IP记录、趋势图表) - 参考来源、附件支持 - 单端口部署 (16012) 技术栈: - Flask 3.0 + SQLAlchemy - SQLite数据库 - marked.js + highlight.js (Markdown渲染) - Chart.js (统计图表)
This commit is contained in:
10
app/models/__init__.py
Normal file
10
app/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""数据模型模块"""
|
||||
from app.models.article import Article, article_tags
|
||||
from app.models.admin import Admin
|
||||
from app.models.category import Category
|
||||
from app.models.tag import Tag
|
||||
from app.models.site_config import SiteConfig
|
||||
from app.models.visit_log import VisitLog, DailyStats
|
||||
from app.models.article_ext import ArticleAuthor, Reference, Attachment, UploadedImage
|
||||
|
||||
__all__ = ['Article', 'article_tags', 'Admin', 'Category', 'Tag', 'SiteConfig', 'VisitLog', 'DailyStats', 'ArticleAuthor', 'Reference', 'Attachment', 'UploadedImage']
|
||||
41
app/models/admin.py
Normal file
41
app/models/admin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""管理员模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
class Admin(db.Model):
|
||||
"""管理员账户表"""
|
||||
__tablename__ = 'admins'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
email = db.Column(db.String(100), unique=True)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
last_login = db.Column(db.DateTime)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Admin {self.username}>'
|
||||
|
||||
def set_password(self, password):
|
||||
"""设置密码"""
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
"""验证密码"""
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典(不包含密码)"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'is_active': self.is_active,
|
||||
'last_login': self.last_login.isoformat() if self.last_login else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
66
app/models/article.py
Normal file
66
app/models/article.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""文章模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class Article(db.Model):
|
||||
"""文章表"""
|
||||
__tablename__ = 'articles'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
slug = db.Column(db.String(200), unique=True, nullable=False)
|
||||
summary = db.Column(db.Text)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
cover_image = db.Column(db.String(500))
|
||||
is_published = db.Column(db.Boolean, default=False)
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
|
||||
# 作者关联
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('article_authors.id'))
|
||||
author = db.relationship('ArticleAuthor', backref='articles')
|
||||
|
||||
# 分类关联
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
|
||||
category = db.relationship('Category', back_populates='articles')
|
||||
|
||||
# 标签关联(多对多)
|
||||
tags = db.relationship('Tag', secondary='article_tags', back_populates='articles')
|
||||
|
||||
# 参考来源和附件(在article_ext.py中定义)
|
||||
# references = db.relationship('Reference', backref='article')
|
||||
# attachments = db.relationship('Attachment', backref='article')
|
||||
|
||||
# 时间戳
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
published_at = db.Column(db.DateTime)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Article {self.title}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'title': self.title,
|
||||
'slug': self.slug,
|
||||
'summary': self.summary,
|
||||
'content': self.content,
|
||||
'cover_image': self.cover_image,
|
||||
'is_published': self.is_published,
|
||||
'view_count': self.view_count,
|
||||
'author': self.author.name if self.author else None,
|
||||
'category': self.category.name if self.category else None,
|
||||
'tags': [tag.name for tag in self.tags],
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||
'published_at': self.published_at.isoformat() if self.published_at else None
|
||||
}
|
||||
|
||||
|
||||
# 文章-标签关联表
|
||||
article_tags = db.Table('article_tags',
|
||||
db.Column('article_id', db.Integer, db.ForeignKey('articles.id'), primary_key=True),
|
||||
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
|
||||
)
|
||||
89
app/models/article_ext.py
Normal file
89
app/models/article_ext.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""文章扩展模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class ArticleAuthor(db.Model):
|
||||
"""文章作者表"""
|
||||
__tablename__ = 'article_authors'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
avatar = db.Column(db.String(500)) # 作者头像URL
|
||||
bio = db.Column(db.String(200)) # 作者简介
|
||||
website = db.Column(db.String(500)) # 作者网站/主页
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ArticleAuthor {self.name}>'
|
||||
|
||||
|
||||
class Reference(db.Model):
|
||||
"""参考来源表"""
|
||||
__tablename__ = 'references'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
|
||||
title = db.Column(db.String(200), nullable=False) # 来源标题
|
||||
url = db.Column(db.String(500)) # 来源链接
|
||||
description = db.Column(db.String(300)) # 来源描述
|
||||
|
||||
article = db.relationship('Article', backref=db.backref('references', lazy='dynamic'))
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Reference {self.title}>'
|
||||
|
||||
|
||||
class Attachment(db.Model):
|
||||
"""附件表"""
|
||||
__tablename__ = 'attachments'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'), nullable=False)
|
||||
filename = db.Column(db.String(200), nullable=False) # 文件名
|
||||
filepath = db.Column(db.String(500), nullable=False) # 文件路径
|
||||
filesize = db.Column(db.Integer) # 文件大小(字节)
|
||||
filetype = db.Column(db.String(50)) # 文件类型
|
||||
description = db.Column(db.String(200)) # 文件描述
|
||||
|
||||
article = db.relationship('Article', backref=db.backref('attachments', lazy='dynamic'))
|
||||
|
||||
upload_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Attachment {self.filename}>'
|
||||
|
||||
def get_filesize_display(self):
|
||||
"""显示友好的文件大小"""
|
||||
size = self.filesize or 0
|
||||
if size < 1024:
|
||||
return f'{size} B'
|
||||
elif size < 1024 * 1024:
|
||||
return f'{size / 1024:.1f} KB'
|
||||
else:
|
||||
return f'{size / 1024 / 1024:.1f} MB'
|
||||
|
||||
|
||||
class UploadedImage(db.Model):
|
||||
"""上传图片表"""
|
||||
__tablename__ = 'uploaded_images'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
filename = db.Column(db.String(200), nullable=False)
|
||||
filepath = db.Column(db.String(500), nullable=False)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id')) # 关联文章(可选)
|
||||
uploader = db.Column(db.String(50)) # 上传者
|
||||
|
||||
upload_time = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
article = db.relationship('Article', backref=db.backref('uploaded_images', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f'<UploadedImage {self.filename}>'
|
||||
|
||||
def get_url(self):
|
||||
"""获取图片访问URL"""
|
||||
return f'/uploads/{self.filename}'
|
||||
33
app/models/category.py
Normal file
33
app/models/category.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""分类模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class Category(db.Model):
|
||||
"""文章分类表"""
|
||||
__tablename__ = 'categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
slug = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.String(200))
|
||||
sort_order = db.Column(db.Integer, default=0)
|
||||
|
||||
# 关联文章
|
||||
articles = db.relationship('Article', back_populates='category')
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'slug': self.slug,
|
||||
'description': self.description,
|
||||
'sort_order': self.sort_order,
|
||||
'article_count': len(self.articles)
|
||||
}
|
||||
59
app/models/site_config.py
Normal file
59
app/models/site_config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""网站配置模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class SiteConfig(db.Model):
|
||||
"""网站配置表"""
|
||||
__tablename__ = 'site_configs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 基本信息
|
||||
site_name = db.Column(db.String(100), default='技术博客')
|
||||
site_subtitle = db.Column(db.String(200), default='分享技术,记录成长')
|
||||
site_logo = db.Column(db.String(500)) # Logo图片URL
|
||||
|
||||
# SEO设置
|
||||
site_keywords = db.Column(db.String(500))
|
||||
site_description = db.Column(db.Text)
|
||||
|
||||
# 底部信息
|
||||
footer_text = db.Column(db.String(200), default='© 2024 技术博客')
|
||||
footer_copyright = db.Column(db.String(200))
|
||||
footer_technology = db.Column(db.String(200), default='Powered by Flask')
|
||||
|
||||
# 底部宣传图片
|
||||
promo_image_1 = db.Column(db.String(500)) # 宣传图片1 URL
|
||||
promo_image_1_title = db.Column(db.String(100)) # 图片1标题/说明
|
||||
promo_image_1_link = db.Column(db.String(500)) # 图片1点击链接
|
||||
promo_image_2 = db.Column(db.String(500)) # 宣传图片2 URL
|
||||
promo_image_2_title = db.Column(db.String(100)) # 图片2标题/说明
|
||||
promo_image_2_link = db.Column(db.String(500)) # 图片2点击链接
|
||||
promo_image_3 = db.Column(db.String(500)) # 宣传图片3 URL(如二维码)
|
||||
promo_image_3_title = db.Column(db.String(100)) # 图片3标题/说明
|
||||
|
||||
# 联系方式
|
||||
contact_email = db.Column(db.String(100))
|
||||
contact_github = db.Column(db.String(100))
|
||||
|
||||
# 其他设置
|
||||
articles_per_page = db.Column(db.Integer, default=10)
|
||||
enable_comments = db.Column(db.Boolean, default=False)
|
||||
|
||||
# 时间戳
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SiteConfig {self.site_name}>'
|
||||
|
||||
@staticmethod
|
||||
def get_config():
|
||||
"""获取网站配置(单例)"""
|
||||
config = SiteConfig.query.first()
|
||||
if not config:
|
||||
config = SiteConfig()
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return config
|
||||
32
app/models/tag.py
Normal file
32
app/models/tag.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""标签模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models.article import article_tags
|
||||
|
||||
|
||||
class Tag(db.Model):
|
||||
"""标签表"""
|
||||
__tablename__ = 'tags'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(30), unique=True, nullable=False)
|
||||
slug = db.Column(db.String(30), unique=True, nullable=False)
|
||||
color = db.Column(db.String(7), default='#007bff') # 标签颜色
|
||||
|
||||
# 关联文章
|
||||
articles = db.relationship('Article', secondary=article_tags, back_populates='tags')
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Tag {self.name}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'slug': self.slug,
|
||||
'color': self.color,
|
||||
'article_count': len(self.articles)
|
||||
}
|
||||
83
app/models/visit_log.py
Normal file
83
app/models/visit_log.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""访问统计模型"""
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
|
||||
|
||||
class VisitLog(db.Model):
|
||||
"""访问日志表"""
|
||||
__tablename__ = 'visit_logs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 访问信息
|
||||
ip_address = db.Column(db.String(50), nullable=False)
|
||||
user_agent = db.Column(db.String(500))
|
||||
referer = db.Column(db.String(500))
|
||||
|
||||
# 访问路径
|
||||
path = db.Column(db.String(500), nullable=False)
|
||||
method = db.Column(db.String(10), default='GET')
|
||||
|
||||
# 访问者信息(可从IP解析)
|
||||
country = db.Column(db.String(50))
|
||||
city = db.Column(db.String(50))
|
||||
|
||||
# 文章关联(如果访问的是文章详情页)
|
||||
article_id = db.Column(db.Integer, db.ForeignKey('articles.id'))
|
||||
article = db.relationship('Article', backref='visit_logs')
|
||||
|
||||
# 时间戳
|
||||
visited_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<VisitLog {self.ip_address} - {self.path}>'
|
||||
|
||||
@staticmethod
|
||||
def get_today_count():
|
||||
"""获取今日访问量"""
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
return VisitLog.query.filter(
|
||||
VisitLog.visited_at >= datetime.combine(today, datetime.min.time())
|
||||
).count()
|
||||
|
||||
@staticmethod
|
||||
def get_total_count():
|
||||
"""获取总访问量"""
|
||||
return VisitLog.query.count()
|
||||
|
||||
@staticmethod
|
||||
def get_unique_ip_count():
|
||||
"""获取独立IP数量"""
|
||||
return db.session.query(db.func.count(db.func.distinct(VisitLog.ip_address))).scalar()
|
||||
|
||||
|
||||
class DailyStats(db.Model):
|
||||
"""每日统计汇总表"""
|
||||
__tablename__ = 'daily_stats'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.Date, unique=True, nullable=False, index=True)
|
||||
|
||||
# 统计数据
|
||||
total_visits = db.Column(db.Integer, default=0)
|
||||
unique_ips = db.Column(db.Integer, default=0)
|
||||
article_views = db.Column(db.Integer, default=0)
|
||||
|
||||
# 热门文章
|
||||
top_article_id = db.Column(db.Integer, db.ForeignKey('articles.id'))
|
||||
top_article_views = db.Column(db.Integer, default=0)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<DailyStats {self.date}>'
|
||||
|
||||
@staticmethod
|
||||
def get_stats_list(days=30):
|
||||
"""获取最近N天的统计数据"""
|
||||
from datetime import date, timedelta
|
||||
start_date = date.today() - timedelta(days=days)
|
||||
return DailyStats.query.filter(
|
||||
DailyStats.date >= start_date
|
||||
).order_by(DailyStats.date.desc()).all()
|
||||
Reference in New Issue
Block a user