初始化技术博客项目

功能特性:
- 前端文章展示 (Markdown渲染、代码高亮)
- 后台管理系统 (文章、分类、标签、作者管理)
- 图片上传 (点击、拖拽、粘贴)
- 网站设置 (Logo、底部宣传图片上传)
- 访问统计 (IP记录、趋势图表)
- 参考来源、附件支持
- 单端口部署 (16012)

技术栈:
- Flask 3.0 + SQLAlchemy
- SQLite数据库
- marked.js + highlight.js (Markdown渲染)
- Chart.js (统计图表)
This commit is contained in:
2026-06-04 23:12:34 +08:00
commit e689fed71e
53 changed files with 6506 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
"""文章扩展服务"""
import os
import uuid
from datetime import datetime
from werkzeug.utils import secure_filename
from app import db
from app.models import ArticleAuthor, Reference, Attachment, UploadedImage, Article
class UploadService:
"""文件上传服务"""
UPLOAD_FOLDER = 'uploads'
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webpo', 'svg'}
ALLOWED_FILE_EXTENSIONS = {'pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip', 'rar', 'tar', 'gz', 'txt', 'md'}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
@staticmethod
def get_upload_folder():
"""获取上传目录路径"""
from flask import current_app
upload_folder = os.path.join(current_app.root_path, '..', UploadService.UPLOAD_FOLDER)
os.makedirs(upload_folder, exist_ok=True)
return upload_folder
@staticmethod
def allowed_image(filename):
"""检查是否是允许的图片格式"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in UploadService.ALLOWED_IMAGE_EXTENSIONS
@staticmethod
def allowed_file(filename):
"""检查是否是允许的文件格式"""
ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
return ext in UploadService.ALLOWED_IMAGE_EXTENSIONS or \
ext in UploadService.ALLOWED_FILE_EXTENSIONS
@staticmethod
def upload_image(file, article_id=None, uploader='admin'):
"""上传图片"""
if not file or not UploadService.allowed_image(file.filename):
return None, '不支持的图片格式'
# 检查文件大小
file.seek(0, os.SEEK_END)
size = file.tell()
file.seek(0)
if size > UploadService.MAX_FILE_SIZE:
return None, '文件大小超过限制最大10MB'
# 生成唯一文件名
ext = file.filename.rsplit('.', 1)[1].lower()
filename = f'{datetime.now().strftime("%Y%m%d")}_{uuid.uuid4().hex[:8]}.{ext}'
# 保存文件
filepath = os.path.join(UploadService.get_upload_folder(), filename)
file.save(filepath)
# 记录到数据库
image = UploadedImage(
filename=filename,
filepath=filepath,
article_id=article_id,
uploader=uploader
)
db.session.add(image)
db.session.commit()
return image, None
@staticmethod
def upload_attachment(file, article_id, description=None):
"""上传附件"""
if not file or not UploadService.allowed_file(file.filename):
return None, '不支持的文件格式'
# 检查文件大小
file.seek(0, os.SEEK_END)
size = file.tell()
file.seek(0)
if size > UploadService.MAX_FILE_SIZE:
return None, '文件大小超过限制最大10MB'
# 生成唯一文件名
original_name = secure_filename(file.filename)
ext = original_name.rsplit('.', 1)[1].lower() if '.' in original_name else ''
filename = f'{datetime.now().strftime("%Y%m%d")}_{uuid.uuid4().hex[:8]}_{original_name}'
# 保存文件
filepath = os.path.join(UploadService.get_upload_folder(), filename)
file.save(filepath)
# 记录到数据库
attachment = Attachment(
article_id=article_id,
filename=original_name,
filepath=filepath,
filesize=size,
filetype=ext,
description=description
)
db.session.add(attachment)
db.session.commit()
return attachment, None
@staticmethod
def delete_image(image_id):
"""删除图片"""
image = UploadedImage.query.get(image_id)
if image:
# 删除文件
if os.path.exists(image.filepath):
os.remove(image.filepath)
# 删除记录
db.session.delete(image)
db.session.commit()
return True
return False
@staticmethod
def delete_attachment(attachment_id):
"""删除附件"""
attachment = Attachment.query.get(attachment_id)
if attachment:
# 删除文件
if os.path.exists(attachment.filepath):
os.remove(attachment.filepath)
# 删除记录
db.session.delete(attachment)
db.session.commit()
return True
return False
@staticmethod
def get_image_url(filename):
"""获取图片URL相对路径"""
return f'/uploads/{filename}'
@staticmethod
def get_image_url(filename):
"""获取图片URL"""
return f'/uploads/{filename}'
class AuthorService:
"""作者服务"""
@staticmethod
def get_all():
"""获取所有作者"""
return ArticleAuthor.query.order_by(ArticleAuthor.name).all()
@staticmethod
def get_by_id(author_id):
"""通过ID获取作者"""
return ArticleAuthor.query.get(author_id)
@staticmethod
def create(name, avatar=None, bio=None, website=None):
"""创建作者"""
author = ArticleAuthor(name=name, avatar=avatar, bio=bio, website=website)
db.session.add(author)
db.session.commit()
return author
@staticmethod
def update(author_id, **kwargs):
"""更新作者"""
author = ArticleAuthor.query.get(author_id)
if author:
for key, value in kwargs.items():
if hasattr(author, key):
setattr(author, key, value)
db.session.commit()
return author
@staticmethod
def delete(author_id):
"""删除作者"""
author = ArticleAuthor.query.get(author_id)
if author:
db.session.delete(author)
db.session.commit()
return True
return False
class ReferenceService:
"""参考来源服务"""
@staticmethod
def get_by_article(article_id):
"""获取文章的参考来源"""
return Reference.query.filter_by(article_id=article_id).order_by(Reference.created_at).all()
@staticmethod
def add(article_id, title, url=None, description=None):
"""添加参考来源"""
ref = Reference(article_id=article_id, title=title, url=url, description=description)
db.session.add(ref)
db.session.commit()
return ref
@staticmethod
def update(reference_id, **kwargs):
"""更新参考来源"""
ref = Reference.query.get(reference_id)
if ref:
for key, value in kwargs.items():
if hasattr(ref, key):
setattr(ref, key, value)
db.session.commit()
return ref
@staticmethod
def delete(reference_id):
"""删除参考来源"""
ref = Reference.query.get(reference_id)
if ref:
db.session.delete(ref)
db.session.commit()
return True
return False
@staticmethod
def update_article_references(article_id, references_data):
"""批量更新文章参考来源"""
# 删除旧的参考来源
Reference.query.filter_by(article_id=article_id).delete()
# 添加新的参考来源
for ref_data in references_data:
if ref_data.get('title'): # 只添加有标题的
ReferenceService.add(
article_id=article_id,
title=ref_data.get('title'),
url=ref_data.get('url'),
description=ref_data.get('description')
)
class AttachmentService:
"""附件服务"""
@staticmethod
def get_by_article(article_id):
"""获取文章附件"""
return Attachment.query.filter_by(article_id=article_id).order_by(Attachment.upload_time).all()
@staticmethod
def delete_attachment(attachment_id):
"""删除附件"""
return UploadService.delete_attachment(attachment_id)