# -*- coding: utf-8 -*- """数据库模型""" from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from extensions import db class Provider(db.Model): """厂商(一个厂商对应多个配置)""" __tablename__ = "providers" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), nullable=False, unique=True, index=True) official_url = db.Column(db.String(512), nullable=True) # 厂商默认官网 plans = db.relationship("VPSPlan", backref="provider_rel", lazy="dynamic", foreign_keys="VPSPlan.provider_id") class User(db.Model): """前台用户""" __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(32), nullable=False, unique=True, index=True) password_hash = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) last_login_at = db.Column(db.DateTime, nullable=True) is_banned = db.Column(db.Boolean, nullable=False, default=False, index=True) banned_at = db.Column(db.DateTime, nullable=True) banned_reason = db.Column(db.String(255), nullable=True) posts = db.relationship( "ForumPost", backref="author_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumPost.user_id", ) comments = db.relationship( "ForumComment", backref="author_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumComment.user_id", ) reports = db.relationship( "ForumReport", backref="reporter_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumReport.reporter_id", ) notifications = db.relationship( "ForumNotification", backref="recipient_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumNotification.user_id", ) acted_notifications = db.relationship( "ForumNotification", backref="actor_rel", lazy="dynamic", foreign_keys="ForumNotification.actor_id", ) post_likes = db.relationship( "ForumPostLike", backref="user_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumPostLike.user_id", ) post_bookmarks = db.relationship( "ForumPostBookmark", backref="user_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumPostBookmark.user_id", ) def set_password(self, raw_password): self.password_hash = generate_password_hash(raw_password) def check_password(self, raw_password): if not self.password_hash: return False return check_password_hash(self.password_hash, raw_password) class ForumCategory(db.Model): """论坛分类(后台可维护)""" __tablename__ = "forum_categories" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), nullable=False, unique=True, index=True) sort_order = db.Column(db.Integer, nullable=False, default=100, index=True) is_active = db.Column(db.Boolean, nullable=False, default=True, index=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class VPSPlan(db.Model): """云服务器配置/方案(属于某厂商)""" __tablename__ = "vps_plans" id = db.Column(db.Integer, primary_key=True) provider_id = db.Column(db.Integer, db.ForeignKey("providers.id"), nullable=True, index=True) # 关联厂商 provider = db.Column(db.String(64), nullable=True, index=True) # 冗余显示名,与 provider_id 二选一兼容旧数据 region = db.Column(db.String(128), nullable=True, index=True) # 保留兼容,不再在表单中填写 name = db.Column(db.String(128), nullable=True) # 可选,不填则用配置项生成显示 vcpu = db.Column(db.Integer, nullable=True) memory_gb = db.Column(db.Integer, nullable=True) storage_gb = db.Column(db.Integer, nullable=True) bandwidth_mbps = db.Column(db.Integer, nullable=True) traffic = db.Column(db.String(64), nullable=True) price_cny = db.Column(db.Float, nullable=True) price_usd = db.Column(db.Float, nullable=True) currency = db.Column(db.String(8), default="CNY") official_url = db.Column(db.String(512), nullable=True) # 该配置详情页,可覆盖厂商默认 countries = db.Column(db.String(255), nullable=True, index=True) price_histories = db.relationship( "PriceHistory", backref="plan_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="PriceHistory.plan_id", ) @property def provider_name(self): if self.provider_rel: return self.provider_rel.name return self.provider or "" @property def display_name(self): """无规格名称时用配置项生成,如 2核4G 80GB""" if self.name and self.name.strip(): return self.name.strip() parts = [] if self.vcpu is not None: parts.append("{}核".format(self.vcpu)) if self.memory_gb is not None: parts.append("{}G".format(self.memory_gb)) if self.storage_gb is not None: parts.append("{}GB".format(self.storage_gb)) if self.traffic and self.traffic.strip(): parts.append(self.traffic.strip()) return " ".join(parts) if parts else "—" def to_dict(self): return { "id": self.id, "provider": self.provider_name, "region": self.region or "", "name": self.display_name, "vcpu": self.vcpu, "memory_gb": self.memory_gb, "storage_gb": self.storage_gb, "bandwidth_mbps": self.bandwidth_mbps, "traffic": self.traffic or "", "price_cny": float(self.price_cny) if self.price_cny is not None else None, "price_usd": float(self.price_usd) if self.price_usd is not None else None, "currency": self.currency or "CNY", "official_url": self.official_url or (self.provider_rel.official_url if self.provider_rel else "") or "", "countries": self.countries or "", } class PriceHistory(db.Model): """配置价格历史快照""" __tablename__ = "price_histories" id = db.Column(db.Integer, primary_key=True) plan_id = db.Column(db.Integer, db.ForeignKey("vps_plans.id"), nullable=False, index=True) price_cny = db.Column(db.Float, nullable=True) price_usd = db.Column(db.Float, nullable=True) currency = db.Column(db.String(8), nullable=False, default="CNY") source = db.Column(db.String(32), nullable=True) # manual / import / bootstrap captured_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumPost(db.Model): """论坛帖子""" __tablename__ = "forum_posts" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) category = db.Column(db.String(32), nullable=False, default="综合讨论", index=True) title = db.Column(db.String(160), nullable=False, index=True) content = db.Column(db.Text, nullable=False) view_count = db.Column(db.Integer, nullable=False, default=0) is_pinned = db.Column(db.Boolean, nullable=False, default=False, index=True) is_featured = db.Column(db.Boolean, nullable=False, default=False, index=True) is_locked = db.Column(db.Boolean, nullable=False, default=False, index=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) comments = db.relationship( "ForumComment", backref="post_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumComment.post_id", ) likes = db.relationship( "ForumPostLike", backref="post_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumPostLike.post_id", ) bookmarks = db.relationship( "ForumPostBookmark", backref="post_rel", lazy="dynamic", cascade="all, delete-orphan", foreign_keys="ForumPostBookmark.post_id", ) class ForumComment(db.Model): """论坛评论""" __tablename__ = "forum_comments" id = db.Column(db.Integer, primary_key=True) post_id = db.Column(db.Integer, db.ForeignKey("forum_posts.id"), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) content = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow) class ForumPostLike(db.Model): """论坛帖子点赞""" __tablename__ = "forum_post_likes" __table_args__ = ( db.UniqueConstraint("post_id", "user_id", name="uq_forum_post_like_post_user"), ) id = db.Column(db.Integer, primary_key=True) post_id = db.Column(db.Integer, db.ForeignKey("forum_posts.id"), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumPostBookmark(db.Model): """论坛帖子收藏""" __tablename__ = "forum_post_bookmarks" __table_args__ = ( db.UniqueConstraint("post_id", "user_id", name="uq_forum_post_bookmark_post_user"), ) id = db.Column(db.Integer, primary_key=True) post_id = db.Column(db.Integer, db.ForeignKey("forum_posts.id"), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumReport(db.Model): """论坛举报""" __tablename__ = "forum_reports" id = db.Column(db.Integer, primary_key=True) reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) target_type = db.Column(db.String(16), nullable=False, index=True) # post / comment target_id = db.Column(db.Integer, nullable=False, index=True) reason = db.Column(db.String(64), nullable=False) detail = db.Column(db.String(500), nullable=True) snapshot_title = db.Column(db.String(160), nullable=True) snapshot_content = db.Column(db.Text, nullable=True) status = db.Column(db.String(16), nullable=False, default="pending", index=True) # pending/processed/rejected review_note = db.Column(db.String(500), nullable=True) reviewed_by = db.Column(db.String(64), nullable=True) reviewed_at = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumNotification(db.Model): """站内通知""" __tablename__ = "forum_notifications" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) actor_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True, index=True) notif_type = db.Column(db.String(32), nullable=False, index=True) # post_commented/thread_replied/report_processed/content_removed post_id = db.Column(db.Integer, nullable=True, index=True) comment_id = db.Column(db.Integer, nullable=True, index=True) report_id = db.Column(db.Integer, nullable=True, index=True) message = db.Column(db.String(255), nullable=False) is_read = db.Column(db.Boolean, nullable=False, default=False, index=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumTrackEvent(db.Model): """论坛埋点事件(用于漏斗与转化分析)""" __tablename__ = "forum_track_events" id = db.Column(db.Integer, primary_key=True) event_name = db.Column(db.String(64), nullable=False, index=True) label = db.Column(db.String(120), nullable=True) post_id = db.Column(db.Integer, nullable=True, index=True) user_id = db.Column(db.Integer, nullable=True, index=True) visitor_id = db.Column(db.String(64), nullable=True, index=True) cta_variant = db.Column(db.String(16), nullable=True, index=True) device_type = db.Column(db.String(16), nullable=True, index=True) page_path = db.Column(db.String(255), nullable=True) endpoint_path = db.Column(db.String(64), nullable=True) referer = db.Column(db.String(255), nullable=True) ip = db.Column(db.String(120), nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) class ForumTrackDailySummary(db.Model): """论坛埋点按天汇总(用于看板与导出)""" __tablename__ = "forum_track_daily_summary" __table_args__ = ( db.UniqueConstraint("event_day", "cta_variant", "event_name", name="uq_forum_track_daily"), ) id = db.Column(db.Integer, primary_key=True) event_day = db.Column(db.Date, nullable=False, index=True) cta_variant = db.Column(db.String(16), nullable=False, default="unknown", index=True) event_name = db.Column(db.String(64), nullable=False, index=True) total = db.Column(db.Integer, nullable=False, default=0) updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)