f103148ebf
基于 Flask + MySQL + Bootstrap 5 的全栈个人资料库管理系统。 主要功能: - 管理员/普通用户双角色权限体系,全站登录保护 - 资源管理:文本、图片、音频、视频四类资源 - 三种添加方式:本地上传(拖拽)、URL 后台下载、磁力下载(aria2c) - 在线预览:文本、图片、HTML5 音视频播放器 - 安全:bcrypt 加盐密码哈希、CSRF 防护、SQLAlchemy ORM 防注入 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
from datetime import datetime
|
|
from app.extensions import db
|
|
|
|
|
|
class Resource(db.Model):
|
|
__tablename__ = 'resources'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
|
nullable=False, index=True)
|
|
title = db.Column(db.String(255), nullable=False)
|
|
description = db.Column(db.Text, nullable=True)
|
|
|
|
# 资源类型: text / image / audio / video
|
|
resource_type = db.Column(db.Enum('text', 'image', 'audio', 'video'),
|
|
nullable=False, index=True)
|
|
|
|
# 来源类型: upload / url / magnet
|
|
source_type = db.Column(db.Enum('upload', 'url', 'magnet'),
|
|
nullable=False, default='upload')
|
|
|
|
# 文件信息
|
|
filename = db.Column(db.String(255), nullable=True) # 磁盘上的文件名
|
|
original_name = db.Column(db.String(255), nullable=True) # 原始文件名
|
|
file_path = db.Column(db.String(512), nullable=True) # 相对于 static 的路径
|
|
file_size = db.Column(db.BigInteger, nullable=True) # 字节
|
|
mime_type = db.Column(db.String(128), nullable=True)
|
|
|
|
# 来源 URL / 磁力链
|
|
source_url = db.Column(db.Text, nullable=True)
|
|
|
|
# 下载状态: pending / downloading / done / failed (针对 url/magnet)
|
|
download_status = db.Column(
|
|
db.Enum('pending', 'downloading', 'done', 'failed', 'na'),
|
|
nullable=False, default='na'
|
|
)
|
|
download_progress = db.Column(db.Integer, default=0) # 0-100
|
|
download_error = db.Column(db.Text, nullable=True)
|
|
|
|
tags = db.Column(db.String(512), nullable=True) # 逗号分隔
|
|
is_public = db.Column(db.Boolean, default=False)
|
|
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
|
|
onupdate=datetime.utcnow, nullable=False)
|
|
|
|
def __repr__(self):
|
|
return f'<Resource {self.title}>'
|
|
|
|
@property
|
|
def size_human(self):
|
|
"""返回人性化的文件大小字符串"""
|
|
if not self.file_size:
|
|
return '未知'
|
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
if self.file_size < 1024:
|
|
return f'{self.file_size:.1f} {unit}'
|
|
self.file_size /= 1024
|
|
return f'{self.file_size:.1f} PB'
|
|
|
|
@property
|
|
def tag_list(self):
|
|
if not self.tags:
|
|
return []
|
|
return [t.strip() for t in self.tags.split(',') if t.strip()]
|
|
|
|
@property
|
|
def type_icon(self):
|
|
icons = {
|
|
'text': 'bi-file-text',
|
|
'image': 'bi-image',
|
|
'audio': 'bi-music-note-beamed',
|
|
'video': 'bi-camera-video',
|
|
}
|
|
return icons.get(self.resource_type, 'bi-file')
|
|
|
|
@property
|
|
def type_badge_color(self):
|
|
colors = {
|
|
'text': 'secondary',
|
|
'image': 'success',
|
|
'audio': 'warning',
|
|
'video': 'danger',
|
|
}
|
|
return colors.get(self.resource_type, 'secondary')
|