核心功能: - 用户注册/登录系统(用户名+密码) - 工具收藏功能(一键收藏/取消收藏) - 收藏分组管理(文件夹) - 用户中心(个人资料、收藏列表) 数据库变更: - 新增 users 表(用户信息) - 新增 folders 表(收藏分组) - 新增 collections 表(收藏记录) 安全增强: - Admin 和 User 完全隔离 - 修复14个管理员路由的权限漏洞 - 所有管理功能添加用户类型检查 新增文件: - templates/auth/register.html - 注册页面 - templates/auth/login.html - 登录页面 - templates/user/profile.html - 用户中心 - templates/user/collections.html - 收藏列表 - create_user_tables.py - 数据库迁移脚本 - USER_SYSTEM_README.md - 用户系统文档 - CHANGELOG_v3.0.md - 版本更新日志 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
270 lines
12 KiB
Python
270 lines
12 KiB
Python
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 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='邮箱')
|
||
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', 'folder_id', name='unique_user_site_folder'),
|
||
db.Index('idx_user_folder', 'user_id', 'folder_id'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f'<Collection user={self.user_id} site={self.site_id}>'
|
||
|