Files
zjpb.net/models.py
Jowe 731f0d3822 feat: v4.0 - 添加 Skills 管理功能
- 新增 Skill 模型,支持手动添加和 GitHub 抓取
- 后台添加 Skills 管理界面
- 前台添加 /skills 列表页和 /skills/<slug> 详情页
- 首页增加 Skills Tab 入口
- 添加数据库迁移脚本

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 22:39:50 +08:00

362 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy()
# 网站和标签的多对多关系表
site_tags = db.Table('site_tags',
db.Column('site_id', db.Integer, db.ForeignKey('sites.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'), primary_key=True)
)
class Site(db.Model):
"""网站模型"""
__tablename__ = 'sites'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(8), unique=True, nullable=False, comment='8位数字编码')
name = db.Column(db.String(100), nullable=False, comment='网站名称')
url = db.Column(db.String(500), nullable=False, comment='网站URL')
slug = db.Column(db.String(100), unique=True, nullable=True, comment='URL别名(SEO用)')
logo = db.Column(db.String(500), comment='Logo图片路径')
short_desc = db.Column(db.String(200), comment='简短描述')
description = db.Column(db.Text, comment='详细介绍')
features = db.Column(db.Text, comment='主要功能')
news_keywords = db.Column(db.String(200), comment='新闻获取关键词(用于精准匹配相关新闻)')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
is_recommended = db.Column(db.Boolean, default=False, nullable=False, comment='是否推荐')
view_count = db.Column(db.Integer, default=0, comment='浏览次数')
sort_order = db.Column(db.Integer, default=0, comment='排序权重')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
# 关联标签
tags = db.relationship('Tag', secondary=site_tags, lazy='subquery',
backref=db.backref('sites', lazy=True))
def __repr__(self):
return f'<Site {self.name}>'
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'code': self.code,
'name': self.name,
'url': self.url,
'slug': self.slug,
'logo': self.logo,
'short_desc': self.short_desc,
'description': self.description,
'features': self.features,
'news_keywords': self.news_keywords,
'is_active': self.is_active,
'is_recommended': self.is_recommended,
'view_count': self.view_count,
'tags': [tag.name for tag in self.tags],
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
}
class Tag(db.Model):
"""标签模型"""
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False, comment='标签名称')
slug = db.Column(db.String(50), unique=True, nullable=False, comment='URL别名')
description = db.Column(db.String(200), comment='标签描述')
seo_title = db.Column(db.String(100), comment='SEO标题(v2.4新增)')
seo_description = db.Column(db.String(300), comment='SEO页面描述(v2.4新增)')
seo_keywords = db.Column(db.String(200), comment='SEO关键词(v2.4新增)')
icon = db.Column(db.String(100), comment='图标')
sort_order = db.Column(db.Integer, default=0, comment='排序权重')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
def __repr__(self):
return f'<Tag {self.name}>'
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'name': self.name,
'slug': self.slug,
'description': self.description,
'seo_title': self.seo_title,
'seo_description': self.seo_description,
'seo_keywords': self.seo_keywords,
'icon': self.icon
}
class News(db.Model):
"""新闻模型"""
__tablename__ = 'news'
id = db.Column(db.Integer, primary_key=True)
site_id = db.Column(db.Integer, db.ForeignKey('sites.id'), nullable=False, comment='关联网站ID')
title = db.Column(db.String(200), nullable=False, comment='新闻标题')
content = db.Column(db.Text, comment='新闻内容')
news_type = db.Column(db.String(50), default='Industry News', comment='新闻类型')
url = db.Column(db.String(500), comment='新闻链接')
source_name = db.Column(db.String(100), comment='新闻来源网站名称')
source_icon = db.Column(db.String(500), comment='新闻来源网站图标URL')
published_at = db.Column(db.DateTime, default=datetime.now, comment='发布时间')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
# 关联网站
site = db.relationship('Site', backref=db.backref('news', lazy='dynamic', order_by='News.published_at.desc()'))
def __repr__(self):
return f'<News {self.title}>'
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'site_id': self.site_id,
'title': self.title,
'content': self.content,
'news_type': self.news_type,
'url': self.url,
'source_name': self.source_name,
'source_icon': self.source_icon,
'published_at': self.published_at.strftime('%Y-%m-%d') if self.published_at else None,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
}
class Admin(UserMixin, db.Model):
"""管理员模型"""
__tablename__ = 'admins'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False, comment='用户名')
password_hash = db.Column(db.String(255), nullable=False, comment='密码哈希')
email = db.Column(db.String(100), comment='邮箱')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
last_login = db.Column(db.DateTime, comment='最后登录时间')
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 get_id(self):
"""返回唯一标识区分Admin和User"""
return f"admin:{self.id}"
def __repr__(self):
return f'<Admin {self.username}>'
class ApiKey(db.Model):
"""API密钥模型"""
__tablename__ = 'api_keys'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(64), unique=True, nullable=False, comment='API密钥')
name = db.Column(db.String(50), nullable=False, comment='密钥名称')
permissions = db.Column(db.String(200), default='site:read,site:write', comment='权限列表')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
last_used = db.Column(db.DateTime, comment='最后使用时间')
usage_count = db.Column(db.Integer, default=0, comment='使用次数')
def __repr__(self):
return f'<ApiKey {self.name}>'
def has_permission(self, permission):
"""检查是否拥有指定权限"""
if not self.is_active:
return False
perms = self.permissions.split(',') if self.permissions else []
return permission.strip() in [p.strip() for p in perms]
def to_dict(self, include_key=False):
"""转换为字典"""
result = {
'id': self.id,
'name': self.name,
'permissions': self.permissions,
'is_active': self.is_active,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'last_used': self.last_used.strftime('%Y-%m-%d %H:%M:%S') if self.last_used else None,
'usage_count': self.usage_count
}
if include_key:
result['key'] = self.key
return result
class PromptTemplate(db.Model):
"""AI提示词模板模型"""
__tablename__ = 'prompt_templates'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(50), unique=True, nullable=False, comment='唯一标识(tags/features/description)')
name = db.Column(db.String(100), nullable=False, comment='模板名称')
system_prompt = db.Column(db.Text, nullable=False, comment='系统提示词')
user_prompt_template = db.Column(db.Text, nullable=False, comment='用户提示词模板(支持变量)')
description = db.Column(db.String(200), comment='模板说明')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
def __repr__(self):
return f'<PromptTemplate {self.name}>'
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'key': self.key,
'name': self.name,
'system_prompt': self.system_prompt,
'user_prompt_template': self.user_prompt_template,
'description': self.description,
'is_active': self.is_active,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
}
class User(UserMixin, db.Model):
"""普通用户模型"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False, index=True, comment='用户名')
password_hash = db.Column(db.String(255), nullable=False, comment='密码哈希')
email = db.Column(db.String(100), unique=True, nullable=True, index=True, comment='邮箱')
email_verified = db.Column(db.Boolean, default=False, comment='邮箱是否已验证')
email_verified_at = db.Column(db.DateTime, comment='邮箱验证时间')
email_verify_token = db.Column(db.String(100), comment='邮箱验证令牌')
email_verify_token_expires = db.Column(db.DateTime, comment='验证令牌过期时间')
avatar = db.Column(db.String(500), comment='头像URL')
bio = db.Column(db.String(200), comment='个人简介')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
is_public_profile = db.Column(db.Boolean, default=False, comment='是否公开个人资料')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
last_login = db.Column(db.DateTime, comment='最后登录时间')
# 关联
collections = db.relationship('Collection', backref='user', lazy='dynamic', cascade='all, delete-orphan')
folders = db.relationship('Folder', backref='user', lazy='dynamic', cascade='all, delete-orphan')
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 get_id(self):
"""返回唯一标识区分Admin和User"""
return f"user:{self.id}"
def __repr__(self):
return f'<User {self.username}>'
class Folder(db.Model):
"""收藏分组模型"""
__tablename__ = 'folders'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True, comment='用户ID')
name = db.Column(db.String(50), nullable=False, comment='文件夹名称')
description = db.Column(db.String(200), comment='文件夹描述')
icon = db.Column(db.String(50), default='📁', comment='图标emoji')
sort_order = db.Column(db.Integer, default=0, comment='排序权重')
is_public = db.Column(db.Boolean, default=False, comment='是否公开')
public_slug = db.Column(db.String(100), unique=True, nullable=True, index=True, comment='公开链接标识')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
# 关联
collections = db.relationship('Collection', backref='folder', lazy='dynamic', cascade='all, delete-orphan')
__table_args__ = (
db.UniqueConstraint('user_id', 'name', name='unique_user_folder'),
)
def __repr__(self):
return f'<Folder {self.name}>'
class Collection(db.Model):
"""收藏记录模型"""
__tablename__ = 'collections'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True, comment='用户ID')
site_id = db.Column(db.Integer, db.ForeignKey('sites.id'), nullable=False, index=True, comment='网站ID')
folder_id = db.Column(db.Integer, db.ForeignKey('folders.id'), nullable=True, index=True, comment='文件夹ID')
note = db.Column(db.Text, comment='备注')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
# 关联
site = db.relationship('Site', backref='collections')
__table_args__ = (
db.UniqueConstraint('user_id', 'site_id', name='unique_user_site'),
db.Index('idx_user_folder', 'user_id', 'folder_id'),
)
def __repr__(self):
return f'<Collection user={self.user_id} site={self.site_id}>'
class Skill(db.Model):
"""Skills 模型"""
__tablename__ = 'skills'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, comment='Skill名称')
slug = db.Column(db.String(100), unique=True, nullable=False, comment='URL别名')
description = db.Column(db.Text, comment='详细介绍')
short_desc = db.Column(db.String(200), comment='简短描述')
logo = db.Column(db.String(500), comment='Logo图片路径')
github_url = db.Column(db.String(500), comment='GitHub 链接')
source_type = db.Column(db.String(20), default='manual', comment='来源类型: manual/github')
source_repo = db.Column(db.String(200), comment='来源仓库')
usage = db.Column(db.Text, comment='使用方法')
examples = db.Column(db.Text, comment='使用示例')
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
is_featured = db.Column(db.Boolean, default=False, comment='是否推荐')
view_count = db.Column(db.Integer, default=0, comment='浏览次数')
sort_order = db.Column(db.Integer, default=0, comment='排序权重')
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
def __repr__(self):
return f'<Skill {self.name}>'
def to_dict(self):
"""转换为字典"""
return {
'id': self.id,
'name': self.name,
'slug': self.slug,
'description': self.description,
'short_desc': self.short_desc,
'logo': self.logo,
'github_url': self.github_url,
'source_type': self.source_type,
'source_repo': self.source_repo,
'usage': self.usage,
'examples': self.examples,
'is_active': self.is_active,
'is_featured': self.is_featured,
'view_count': self.view_count,
'sort_order': self.sort_order,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None
}