Files
zjpb.net/models.py
Jowe 7a6fd0c388 fix: v3.0.1 - 修复5个代码问题
修复内容:
1. Collection 模型唯一约束逻辑错误
   - 修改约束从 (user_id, site_id, folder_id) 到 (user_id, site_id)
   - 防止用户多次收藏同一网站

2. 用户注册重复提交数据库
   - 优化为只提交一次数据库操作
   - 提升注册性能

3. JavaScript 未使用的变量
   - 删除 updateCollectButton() 中未使用的 icon 变量

4. 文件夹计数逻辑缺失
   - 为每个文件夹添加收藏数量计算
   - 修复收藏列表页面显示

5. JavaScript 错误处理不完善
   - 所有 fetch 调用添加 HTTP 状态码检查
   - 改进网络错误提示

新增文件:
- fix_collection_constraint.py - 数据库约束修复脚本
- BUGFIX_v3.0.1.md - 详细修复记录

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 19:27:12 +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', 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}>'