Files
zjpb.net/models.py
Jowe 2067fb1712 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>
2026-02-06 19:19:05 +08:00

270 lines
12 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 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}>'