feat: v3.0 - 用户系统和收藏功能

核心功能:
- 用户注册/登录系统(用户名+密码)
- 工具收藏功能(一键收藏/取消收藏)
- 收藏分组管理(文件夹)
- 用户中心(个人资料、收藏列表)

数据库变更:
- 新增 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>
This commit is contained in:
Jowe
2026-02-06 19:19:05 +08:00
parent 34cd05b01c
commit 2067fb1712
11 changed files with 2542 additions and 6 deletions

View File

@@ -148,6 +148,10 @@ class Admin(UserMixin, db.Model):
"""验证密码"""
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}>'
@@ -182,3 +186,84 @@ class PromptTemplate(db.Model):
'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}>'