功能特性: - 前端文章展示 (Markdown渲染、代码高亮) - 后台管理系统 (文章、分类、标签、作者管理) - 图片上传 (点击、拖拽、粘贴) - 网站设置 (Logo、底部宣传图片上传) - 访问统计 (IP记录、趋势图表) - 参考来源、附件支持 - 单端口部署 (16012) 技术栈: - Flask 3.0 + SQLAlchemy - SQLite数据库 - marked.js + highlight.js (Markdown渲染) - Chart.js (统计图表)
255 lines
8.0 KiB
Python
255 lines
8.0 KiB
Python
"""文章扩展服务"""
|
||
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) |