504 lines
20 KiB
Python
504 lines
20 KiB
Python
from app import db
|
||
from datetime import datetime
|
||
import pytz
|
||
import json
|
||
|
||
def beijing_now():
|
||
"""获取北京时间"""
|
||
tz = pytz.timezone('Asia/Shanghai')
|
||
return datetime.now(tz)
|
||
|
||
class Post(db.Model):
|
||
"""帖子模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||
|
||
# 内容
|
||
title = db.Column(db.String(200)) # 标题(可选)
|
||
content = db.Column(db.Text, nullable=False) # 内容
|
||
content_type = db.Column(db.String(20), default='text') # 内容类型:text/rich_text/link
|
||
|
||
# 时间
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||
|
||
# 统计
|
||
likes_count = db.Column(db.Integer, default=0)
|
||
comments_count = db.Column(db.Integer, default=0)
|
||
view_count = db.Column(db.Integer, default=0)
|
||
|
||
# 状态
|
||
status = db.Column(db.String(20), default='active') # active/hidden/deleted
|
||
is_top = db.Column(db.Boolean, default=False) # 是否置顶
|
||
|
||
# 关系
|
||
user = db.relationship('User', backref='posts')
|
||
likes = db.relationship('PostLike', backref='post', lazy='dynamic')
|
||
comments = db.relationship('Comment', backref='post', lazy='dynamic')
|
||
|
||
class User(db.Model):
|
||
"""用户模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
|
||
# 基础账号信息(注册时必填)
|
||
username = db.Column(db.String(80), unique=True, nullable=False) # 用户名
|
||
email = db.Column(db.String(120), unique=True, nullable=False) # 邮箱
|
||
password_hash = db.Column(db.String(128), nullable=False) # 密码哈希
|
||
email_confirmed = db.Column(db.Boolean, default=False) # 邮箱是否验证
|
||
|
||
# 账号状态
|
||
created_at = db.Column(db.DateTime, default=beijing_now) # 注册时间
|
||
last_seen = db.Column(db.DateTime, default=beijing_now) # 最后活跃时间
|
||
status = db.Column(db.String(20), default='active') # 账号状态 active/banned/deleted
|
||
|
||
# 个人资料(可选,后续在个人中心完善)
|
||
nickname = db.Column(db.String(30)) # 社区昵称
|
||
avatar_url = db.Column(db.String(200)) # 头像URL
|
||
banner_url = db.Column(db.String(200)) # 个人主页背景图
|
||
bio = db.Column(db.String(200)) # 个人简介
|
||
gender = db.Column(db.String(10)) # 性别
|
||
birth_date = db.Column(db.Date) # 生日
|
||
location = db.Column(db.String(100)) # 所在地
|
||
|
||
# 联系方式(可选)
|
||
phone = db.Column(db.String(20)) # 手机号
|
||
wechat_id = db.Column(db.String(80)) # 微信号
|
||
|
||
# 实名认证信息(可选)
|
||
real_name = db.Column(db.String(30)) # 真实姓名
|
||
id_number = db.Column(db.String(18)) # 身份证号(加密存储)
|
||
is_verified = db.Column(db.Boolean, default=False) # 是否实名认证
|
||
verify_time = db.Column(db.DateTime) # 实名认证时间
|
||
|
||
# 投资相关信息(可选)
|
||
trading_experience = db.Column(db.Integer) # 炒股年限
|
||
investment_style = db.Column(db.String(50)) # 投资风格
|
||
risk_preference = db.Column(db.String(20)) # 风险偏好
|
||
investment_amount = db.Column(db.String(20)) # 投资规模
|
||
preferred_markets = db.Column(db.String(200), default='[]') # 偏好市场 JSON
|
||
|
||
# 社区信息(系统自动更新)
|
||
user_level = db.Column(db.Integer, default=1) # 用户等级
|
||
reputation_score = db.Column(db.Integer, default=0) # 信用积分
|
||
contribution_point = db.Column(db.Integer, default=0) # 贡献点数
|
||
post_count = db.Column(db.Integer, default=0) # 发帖数
|
||
comment_count = db.Column(db.Integer, default=0) # 评论数
|
||
follower_count = db.Column(db.Integer, default=0) # 粉丝数
|
||
following_count = db.Column(db.Integer, default=0) # 关注数
|
||
|
||
# 创作者信息(可选)
|
||
is_creator = db.Column(db.Boolean, default=False) # 是否创作者
|
||
creator_type = db.Column(db.String(20)) # 创作者类型
|
||
creator_tags = db.Column(db.String(200), default='[]') # 创作者标签 JSON
|
||
|
||
# 系统设置
|
||
email_notifications = db.Column(db.Boolean, default=True) # 邮件通知
|
||
sms_notifications = db.Column(db.Boolean, default=False) # 短信通知
|
||
wechat_notifications = db.Column(db.Boolean, default=False) # 微信通知
|
||
notification_preferences = db.Column(db.String(500), default='{}') # 通知偏好 JSON
|
||
privacy_level = db.Column(db.String(20), default='public') # 隐私级别
|
||
theme_preference = db.Column(db.String(20), default='light') # 主题偏好
|
||
blocked_keywords = db.Column(db.String(500), default='[]') # 屏蔽关键词 JSON
|
||
# 手机号验证
|
||
phone_confirmed = db.Column(db.Boolean, default=False) # 手机是否验证
|
||
phone_confirm_time = db.Column(db.DateTime) # 手机验证时间
|
||
|
||
def __init__(self, username, email=None, password=None, phone=None):
|
||
self.username = username
|
||
if email:
|
||
self.email = email
|
||
if password:
|
||
self.set_password(password)
|
||
if phone:
|
||
self.phone = phone
|
||
|
||
def set_password(self, password):
|
||
from werkzeug.security import generate_password_hash
|
||
self.password_hash = generate_password_hash(password)
|
||
|
||
def check_password(self, password):
|
||
from werkzeug.security import check_password_hash
|
||
return check_password_hash(self.password_hash, password)
|
||
|
||
def update_last_seen(self):
|
||
self.last_seen = beijing_now()
|
||
db.session.commit()
|
||
|
||
def get_preferred_markets(self):
|
||
try:
|
||
return json.loads(self.preferred_markets)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
|
||
def get_blocked_keywords(self):
|
||
try:
|
||
return json.loads(self.blocked_keywords)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
|
||
def get_notification_preferences(self):
|
||
try:
|
||
return json.loads(self.notification_preferences)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return {}
|
||
|
||
def get_creator_tags(self):
|
||
try:
|
||
return json.loads(self.creator_tags)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
|
||
def set_preferred_markets(self, markets):
|
||
self.preferred_markets = json.dumps(markets)
|
||
|
||
def set_blocked_keywords(self, keywords):
|
||
self.blocked_keywords = json.dumps(keywords)
|
||
|
||
def set_notification_preferences(self, preferences):
|
||
self.notification_preferences = json.dumps(preferences)
|
||
|
||
def set_creator_tags(self, tags):
|
||
self.creator_tags = json.dumps(tags)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
'id': self.id,
|
||
'username': self.username,
|
||
'email': self.email,
|
||
'nickname': self.nickname,
|
||
'avatar_url': self.avatar_url,
|
||
'bio': self.bio,
|
||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||
'last_seen': self.last_seen.isoformat() if self.last_seen else None,
|
||
'status': self.status,
|
||
'user_level': self.user_level,
|
||
'reputation_score': self.reputation_score,
|
||
'post_count': self.post_count,
|
||
'follower_count': self.follower_count,
|
||
'following_count': self.following_count
|
||
}
|
||
|
||
def __repr__(self):
|
||
return f'<User {self.username}>'
|
||
|
||
class Comment(db.Model):
|
||
"""评论"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||
content = db.Column(db.Text, nullable=False)
|
||
parent_id = db.Column(db.Integer, db.ForeignKey('comment.id')) # 父评论ID,用于回复
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
status = db.Column(db.String(20), default='active')
|
||
|
||
user = db.relationship('User', backref='comments')
|
||
replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]))
|
||
|
||
|
||
class CommentLike(db.Model):
|
||
"""评论点赞记录(基于session_id以兼容匿名点赞)"""
|
||
__tablename__ = 'comment_like'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
|
||
session_id = db.Column(db.String(100), nullable=False)
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
__table_args__ = (db.UniqueConstraint('comment_id', 'session_id'),)
|
||
|
||
class EventFollow(db.Model):
|
||
"""事件关注"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
user = db.relationship('User', backref='event_follows')
|
||
|
||
__table_args__ = (db.UniqueConstraint('user_id', 'event_id'),)
|
||
|
||
class PostLike(db.Model):
|
||
"""帖子点赞"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
user = db.relationship('User', backref='post_likes')
|
||
|
||
__table_args__ = (db.UniqueConstraint('user_id', 'post_id'),)
|
||
|
||
class Event(db.Model):
|
||
"""事件模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
title = db.Column(db.String(200), nullable=False)
|
||
description = db.Column(db.Text)
|
||
|
||
# 事件类型与状态
|
||
event_type = db.Column(db.String(50))
|
||
status = db.Column(db.String(20), default='active')
|
||
|
||
# 时间相关
|
||
start_time = db.Column(db.DateTime, default=beijing_now)
|
||
end_time = db.Column(db.DateTime)
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
# 热度与统计
|
||
hot_score = db.Column(db.Float, default=0)
|
||
view_count = db.Column(db.Integer, default=0)
|
||
trending_score = db.Column(db.Float, default=0)
|
||
post_count = db.Column(db.Integer, default=0)
|
||
follower_count = db.Column(db.Integer, default=0)
|
||
|
||
# 关联信息
|
||
related_industries = db.Column(db.JSON)
|
||
keywords = db.Column(db.JSON)
|
||
files = db.Column(db.JSON)
|
||
importance = db.Column(db.String(20))
|
||
related_avg_chg = db.Column(db.Float, default=0)
|
||
related_max_chg = db.Column(db.Float, default=0)
|
||
related_week_chg = db.Column(db.Float, default=0)
|
||
|
||
# 新增字段
|
||
invest_score = db.Column(db.Integer) # 超预期得分
|
||
expectation_surprise_score = db.Column(db.Integer)
|
||
# 创建者信息
|
||
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||
creator = db.relationship('User', backref='created_events')
|
||
|
||
# 关系
|
||
posts = db.relationship('Post', backref='event', lazy='dynamic')
|
||
followers = db.relationship('EventFollow', backref='event', lazy='dynamic')
|
||
related_stocks = db.relationship('RelatedStock', backref='event', lazy='dynamic')
|
||
historical_events = db.relationship('HistoricalEvent', backref='event', lazy='dynamic')
|
||
related_data = db.relationship('RelatedData', backref='event', lazy='dynamic')
|
||
related_concepts = db.relationship('RelatedConcepts', backref='event', lazy='dynamic')
|
||
|
||
@property
|
||
def keywords_list(self):
|
||
if isinstance(self.keywords, list):
|
||
return self.keywords
|
||
elif isinstance(self.keywords, str):
|
||
try:
|
||
return json.loads(self.keywords)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
return []
|
||
|
||
def set_keywords(self, keywords):
|
||
if isinstance(keywords, list):
|
||
self.keywords = keywords
|
||
elif isinstance(keywords, str):
|
||
try:
|
||
self.keywords = json.loads(keywords)
|
||
except json.JSONDecodeError:
|
||
self.keywords = [keywords]
|
||
else:
|
||
self.keywords = []
|
||
|
||
class RelatedStock(db.Model):
|
||
"""相关标的模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||
stock_code = db.Column(db.String(20)) # 股票代码
|
||
stock_name = db.Column(db.String(100)) # 股票名称
|
||
sector = db.Column(db.String(100)) # 关联类型
|
||
relation_desc = db.Column(db.String(1024)) # 关联原因描述
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||
correlation = db.Column(db.Float())
|
||
momentum = db.Column(db.String(1024)) #动量
|
||
|
||
class RelatedData(db.Model):
|
||
"""关联数据模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||
title = db.Column(db.String(200)) # 数据标题
|
||
data_type = db.Column(db.String(50)) # 数据类型
|
||
data_content = db.Column(db.JSON) # 数据内容(JSON格式)
|
||
description = db.Column(db.Text) # 数据描述
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
class RelatedConcepts(db.Model):
|
||
"""关联数据模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||
concept_code = db.Column(db.String(20)) # 数据标题
|
||
concept = db.Column(db.String(100)) # 数据类型
|
||
reason = db.Column(db.Text) # 数据描述
|
||
image_paths = db.Column(db.JSON) # 数据内容(JSON格式)
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
@property
|
||
def image_paths_list(self):
|
||
if isinstance(self.image_paths, list):
|
||
return self.image_paths
|
||
elif isinstance(self.image_paths, str):
|
||
try:
|
||
return json.loads(self.image_paths)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
return []
|
||
|
||
def set_image_paths(self, image_paths):
|
||
if isinstance(image_paths, list):
|
||
self.image_paths = image_paths
|
||
elif isinstance(image_paths, str):
|
||
try:
|
||
self.image_paths = json.loads(image_paths)
|
||
except json.JSONDecodeError:
|
||
self.image_paths = [image_paths]
|
||
else:
|
||
self.image_paths = []
|
||
|
||
def get_first_image_path(self):
|
||
paths = self.image_paths_list
|
||
return paths[0] if paths else None
|
||
|
||
class EventHotHistory(db.Model):
|
||
"""事件热度历史记录"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||
score = db.Column(db.Float) # 总分
|
||
interaction_score = db.Column(db.Float) # 互动分数
|
||
follow_score = db.Column(db.Float) # 关注度分数
|
||
view_score = db.Column(db.Float) # 浏览量分数
|
||
recent_activity_score = db.Column(db.Float) # 最近活跃度分数
|
||
time_decay = db.Column(db.Float) # 时间衰减因子
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
event = db.relationship('Event', backref='hot_history')
|
||
|
||
class EventTransmissionNode(db.Model):
|
||
"""事件传导节点模型"""
|
||
__tablename__ = 'event_transmission_nodes'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||
node_type = db.Column(db.Enum('company', 'industry', 'policy', 'technology',
|
||
'market', 'event', 'other'), nullable=False)
|
||
node_name = db.Column(db.String(200), nullable=False)
|
||
node_description = db.Column(db.Text)
|
||
importance_score = db.Column(db.Integer, default=50)
|
||
stock_code = db.Column(db.String(20))
|
||
is_main_event = db.Column(db.Boolean, default=False)
|
||
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||
|
||
# Relationships
|
||
event = db.relationship('Event', backref='transmission_nodes')
|
||
outgoing_edges = db.relationship('EventTransmissionEdge',
|
||
foreign_keys='EventTransmissionEdge.from_node_id',
|
||
backref='from_node', cascade='all, delete-orphan')
|
||
incoming_edges = db.relationship('EventTransmissionEdge',
|
||
foreign_keys='EventTransmissionEdge.to_node_id',
|
||
backref='to_node', cascade='all, delete-orphan')
|
||
|
||
__table_args__ = (
|
||
db.Index('idx_event_node_type', 'event_id', 'node_type'),
|
||
db.Index('idx_node_name', 'node_name'),
|
||
)
|
||
|
||
class EventTransmissionEdge(db.Model):
|
||
"""事件传导边模型"""
|
||
__tablename__ = 'event_transmission_edges'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||
from_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
|
||
to_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
|
||
|
||
transmission_type = db.Column(db.Enum('supply_chain', 'competition', 'policy',
|
||
'technology', 'capital_flow', 'expectation',
|
||
'cyclic_effect', 'other'), nullable=False)
|
||
transmission_mechanism = db.Column(db.Text)
|
||
direction = db.Column(db.Enum('positive', 'negative', 'neutral', 'mixed'), default='neutral')
|
||
strength = db.Column(db.Integer, default=50)
|
||
impact = db.Column(db.Text)
|
||
is_circular = db.Column(db.Boolean, default=False)
|
||
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||
|
||
# Relationship
|
||
event = db.relationship('Event', backref='transmission_edges')
|
||
|
||
__table_args__ = (
|
||
db.Index('idx_event_edge_type', 'event_id', 'transmission_type'),
|
||
db.Index('idx_from_to_nodes', 'from_node_id', 'to_node_id'),
|
||
)
|
||
|
||
class EventSankeyFlow(db.Model):
|
||
"""事件桑基流模型"""
|
||
__tablename__ = 'event_sankey_flows'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
|
||
|
||
# 流的基本信息
|
||
source_node = db.Column(db.String(200), nullable=False)
|
||
source_type = db.Column(db.Enum('event', 'policy', 'technology', 'industry',
|
||
'company', 'product'), nullable=False)
|
||
source_level = db.Column(db.Integer, nullable=False, default=0)
|
||
|
||
target_node = db.Column(db.String(200), nullable=False)
|
||
target_type = db.Column(db.Enum('policy', 'technology', 'industry',
|
||
'company', 'product'), nullable=False)
|
||
target_level = db.Column(db.Integer, nullable=False, default=1)
|
||
|
||
# 流量信息
|
||
flow_value = db.Column(db.Numeric(10, 2), nullable=False)
|
||
flow_ratio = db.Column(db.Numeric(5, 4), nullable=False)
|
||
|
||
# 传导机制
|
||
transmission_path = db.Column(db.String(500))
|
||
impact_description = db.Column(db.Text)
|
||
evidence_strength = db.Column(db.Integer, default=50)
|
||
|
||
# 时间戳
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
|
||
|
||
# 关系
|
||
event = db.relationship('Event', backref='sankey_flows')
|
||
|
||
__table_args__ = (
|
||
db.Index('idx_event_flow', 'event_id'),
|
||
db.Index('idx_source_target', 'source_node', 'target_node'),
|
||
)
|
||
|
||
class HistoricalEvent(db.Model):
|
||
"""历史事件模型"""
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||
title = db.Column(db.String(200))
|
||
content = db.Column(db.Text)
|
||
event_date = db.Column(db.DateTime)
|
||
relevance = db.Column(db.Integer) # 相关性
|
||
importance = db.Column(db.Integer) # 重要程度
|
||
related_stock = db.Column(db.JSON) # 保留JSON字段
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
# 新增关系
|
||
stocks = db.relationship('HistoricalEventStock', backref='historical_event', lazy='dynamic',
|
||
cascade='all, delete-orphan')
|
||
|
||
class HistoricalEventStock(db.Model):
|
||
"""历史事件相关股票模型"""
|
||
__tablename__ = 'historical_event_stocks'
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
historical_event_id = db.Column(db.Integer, db.ForeignKey('historical_event.id'), nullable=False)
|
||
stock_code = db.Column(db.String(20), nullable=False)
|
||
stock_name = db.Column(db.String(50))
|
||
relation_desc = db.Column(db.Text)
|
||
correlation = db.Column(db.Float, default=0.5)
|
||
sector = db.Column(db.String(100))
|
||
created_at = db.Column(db.DateTime, default=beijing_now)
|
||
|
||
__table_args__ = (
|
||
db.Index('idx_historical_event_stock', 'historical_event_id', 'stock_code'),
|
||
) |