# -*- coding = utf-8 -*- # @Time : 2025/2/13 下午12:17 # @File : models.py # @Software : PyCharm # @Author : xingc # @Desc : import enum import uuid from datetime import datetime, date, timedelta from typing import List import ujson from sqlalchemy import ( Integer, String, Text, Date, ForeignKey, DECIMAL, CheckConstraint, UniqueConstraint, Index, Sequence, JSON, Enum, Boolean ) from sqlalchemy.orm import ( DeclarativeBase, Mapped, mapped_column, relationship ) from sqlalchemy.sql import func from sqlalchemy.ext.asyncio import AsyncAttrs from core.base import schema as base_schema class BaseModel(DeclarativeBase, AsyncAttrs): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True, comment='自增id' ) def dict(self, exclude: set = None): data = {} for column in self.__table__.columns: if exclude is not None and column.name in exclude: continue value = getattr(self, column.name) if isinstance(value, datetime): data[column.name] = value.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(value, enum.Enum): data[column.name] = value.value else: data[column.name] = value return data def json(self, exclude: set = None): return ujson.dumps(self.dict(exclude=exclude)) class DateModel: created_at: Mapped[datetime] = mapped_column(server_default=func.current_timestamp(), comment='创建时间') updated_at: Mapped[datetime] = mapped_column(server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment='更新时间') def set_default_vip_expire_date() -> date: expire_date = (datetime.now() - timedelta(days=1)) return expire_date.date() class User(BaseModel, DateModel): __tablename__ = 'system_users' __table_args__ = ( Index('idx_wechat_openid', 'wechat_openid'), UniqueConstraint('user_id', name='uniq_user_id'), {'comment': '用户表'} ) id = None user_id: Mapped[int] = mapped_column( Integer, Sequence('non_primary_id_seq', start=100000, increment=1), primary_key=True, autoincrement=True, comment='用户id' ) nickname: Mapped[str] = mapped_column(String(20), comment='昵称') gender: Mapped[int] = mapped_column(Integer, nullable=True, comment='性别') avatar_url: Mapped[str] = mapped_column(String(500), nullable=True, comment='头像链接') wechat_openid: Mapped[str] = mapped_column(String(100), comment='微信openid') wechat_union_id: Mapped[str] = mapped_column(String(100), nullable=True, comment='微信unionid') phone: Mapped[str] = mapped_column(String(11), nullable=True, comment='手机号码') vip_expire_date: Mapped[date] = mapped_column(Date, default=set_default_vip_expire_date, comment='会员到期时间') points: Mapped[int] = mapped_column(Integer, default=0, comment='会员积分') description: Mapped[str] = mapped_column(String(200), default='', comment='用户简介') fans_count: Mapped[int] = mapped_column(Integer, default=0, comment='粉丝数') followers_count: Mapped[int] = mapped_column(Integer, default=0, comment='关注数') last_active_at: Mapped[datetime] = mapped_column(nullable=True, comment='最后活跃时间') collections: Mapped[list['UserCollection']] = relationship( back_populates='user', cascade='all, delete-orphan' ) # 关注者 following: Mapped[list['UserFollows']] = relationship( 'UserFollows', foreign_keys='UserFollows.follower_id', cascade='all, delete-orphan' ) # 定义被关注关系 followers: Mapped[list['UserFollows']] = relationship( 'UserFollows', foreign_keys='UserFollows.followed_id', cascade='all, delete-orphan' ) chats: Mapped[List['Chat']] = relationship(secondary='system_chat_participant', back_populates='users') messages: Mapped[List['Message']] = relationship(back_populates='user') chat_participants: Mapped[List['ChatParticipant']] = relationship(back_populates='user') def __str__(self): return f'{self.user_id}' def is_vip(self) -> bool: return self.vip_expire_date >= datetime.now().date() class UserFollows(BaseModel, DateModel): __tablename__ = 'system_user_followers' __table_args__ = ( {'comment': '用户关注关系表'} ) follower_id: Mapped[int] = mapped_column( Integer, ForeignKey('system_users.user_id', ondelete='CASCADE'), comment='关注者的用户id' ) followed_id: Mapped[int] = mapped_column( Integer, ForeignKey('system_users.user_id', ondelete='CASCADE'), comment='被关注者的用户id' ) follower = relationship('User', foreign_keys=[follower_id], back_populates='following') followed = relationship('User', foreign_keys=[followed_id], back_populates='followers') def __repr__(self): return f'' class Card(BaseModel, DateModel): __tablename__ = 'system_cards' __table_args__ = ( Index('idx_cert_number_category_source', 'cert_number', 'category', 'source'), {'comment': '卡片表'} ) cert_number: Mapped[str] = mapped_column(String(50), comment='评级卡号') card_year: Mapped[str] = mapped_column(String(50), nullable=True, comment='卡片年份') card_brand: Mapped[str] = mapped_column(String(255), nullable=True, comment='卡片品牌') card_score: Mapped[float] = mapped_column(DECIMAL(10, 1), nullable=True, comment='卡片评分') auto_score: Mapped[float] = mapped_column(DECIMAL(10, 1), nullable=True, comment='签字评分') card_name: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片名称') card_number: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片编号') card_type: Mapped[str] = mapped_column(String(20), nullable=True, comment='卡片类型') card_picture: Mapped[dict] = mapped_column(JSON, default=lambda: dict(), comment='卡片正、反图片(本地)') card_spare_picture: Mapped[dict] = mapped_column(JSON, default=lambda: dict(), comment='卡片正、反图片(远程)') create_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='创建者') perfect_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='完善者') category: Mapped[base_schema.CardCategoryType] = mapped_column( Enum(base_schema.CardCategoryType, inherit_schema=True), nullable=True, server_default=base_schema.CardCategoryType.OTHER.value, comment='卡片分类' ) source: Mapped[base_schema.PlatformSourceType] = mapped_column( Enum(base_schema.PlatformSourceType, inherit_schema=True), comment='来源,PSA、BGS、CGC、GBTC、CCG、BCTC、CIC :采集 、OTHER:自定义' ) diy_source: Mapped[str] = mapped_column(String(50), nullable=True, comment='自定义品牌名') collections: Mapped[list['UserCollection']] = relationship( back_populates='card', cascade='all, delete-orphan' ) class UserCollection(BaseModel, DateModel): __tablename__ = 'system_user_collections' __table_args__ = ( UniqueConstraint('user_id', 'card_id', name='uniq_user_card'), CheckConstraint('purchase_price >= 0', name='check_purchase_price'), CheckConstraint('display_value >= 0', name='check_display_price'), {'comment': '卡片收藏关联表'} ) user_id: Mapped[int] = mapped_column( Integer, ForeignKey('system_users.user_id', ondelete='CASCADE'), nullable=False ) card_id: Mapped[int] = mapped_column( Integer, ForeignKey('system_cards.id', ondelete='CASCADE'), nullable=False ) purchase_price: Mapped[float] = mapped_column( DECIMAL(10, 2), default=0 ) display_price: Mapped[float] = mapped_column( DECIMAL(10, 2), default=0 ) displayed: Mapped[bool] = mapped_column( Boolean, default=False ) user: Mapped['User'] = relationship(back_populates='collections') card: Mapped['Card'] = relationship(back_populates='collections') class SearchCardCache(BaseModel, DateModel): __tablename__ = 'system_cache_cards' __table_args__ = ( Index('idx_cert_and_card_number_source', 'cert_number', 'source'), {'comment': '搜索卡片信息缓存表'} ) cert_number: Mapped[str] = mapped_column(String(50), nullable=False, comment='评级卡号') data: Mapped[dict] = mapped_column(JSON, comment='标准数据') raw_data: Mapped[dict] = mapped_column(JSON, comment='原始数据') source: Mapped[base_schema.SupportPlatformType] = mapped_column( Enum(base_schema.SupportPlatformType, inherit_schema=True), comment='数据来源:PSA、BGS、CGC、GBTC、CCG、BCTC、CIC' ) class Product(BaseModel, DateModel): __tablename__ = 'system_products' __table_args__ = {'comment': '产品信息表'} name: Mapped[str] = mapped_column(String(20), comment='产品名') category: Mapped[base_schema.ProductCategory] = mapped_column( Enum(base_schema.ProductCategory, inherit_schema=True), comment='商品分类, vip-会员服务 points-积分服务 gift-礼物' ) price: Mapped[int] = mapped_column(Integer, comment='价格,不带小数点 单位为分') action: Mapped[base_schema.ProductAction] = mapped_column( Enum(base_schema.ProductAction, inherit_schema=True), comment='付款后的动作,会员续期:vip_renewal 积分:recharge_points' ) number: Mapped[int] = mapped_column(Integer, comment='执行动作的增量值') gift_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='礼物产品id') description: Mapped[str] = mapped_column(Text, nullable=True, comment='商品描述') class Order(BaseModel, DateModel): __tablename__ = 'system_orders' __table_args__ = ( Index('idx_order_on_order_id', 'order_id'), {'comment': '产品订单表'} ) order_id: Mapped[str] = mapped_column(String(32), comment='订单id') transaction_id: Mapped[str] = mapped_column(String(32), nullable=True, comment='微信订单id') user_id: Mapped[int] = mapped_column(Integer, comment='用户id') product_id: Mapped[int] = mapped_column(Integer, comment='产品id') total_fee: Mapped[int] = mapped_column(Integer, nullable=True, comment='发起的总金额,单位为分') notify_total_fee: Mapped[int] = mapped_column(Integer, nullable=True, comment='回调成功支付的总金额,单位为分') pay_url: Mapped[str] = mapped_column(String(255), nullable=True, comment='支付地址') status: Mapped[base_schema.OrderStatusType] = mapped_column( Enum(base_schema.OrderStatusType, inherit_schema=True), server_default=base_schema.OrderStatusType.INIT.value, comment='订单状态,init:初始化 fail:失败 ok:完成 wait payment:待支付 paid:已支付 cancel. 取消' ) ip_address: Mapped[str] = mapped_column(String(255), nullable=True, comment='操作ip') class Chat(BaseModel, DateModel): __tablename__ = 'system_chats' __table_args__ = ( Index('idx_chat_on_guid', 'guid'), {'comment': '聊天表'} ) guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4())) users: Mapped[List['User']] = relationship(secondary='system_chat_participant', back_populates='chats') messages: Mapped[List['Message']] = relationship(back_populates='chat', cascade='all,delete') is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) chat_participants: Mapped[List['ChatParticipant']] = relationship(back_populates='chat', cascade='all,delete') class Message(BaseModel, DateModel): __tablename__ = 'system_messages' __table_args__ = ( Index('idx_message_on_user_id', 'user_id'), Index('idx_message_on_chat_id', 'chat_id'), Index('idx_message_on_user_id_chat_id', 'chat_id', 'user_id'), {'comment': '聊天消息表'} ) guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4())) user_id: Mapped[int] = mapped_column(ForeignKey('system_users.user_id')) chat_id: Mapped[int] = mapped_column(ForeignKey('system_chats.id')) message_type: Mapped[str] = mapped_column( Enum(base_schema.MessageType, inherit_schema=True), default=base_schema.MessageType.TEXT ) content: Mapped[str] = mapped_column(String(5000), nullable=True, comment='文本消息') chat: Mapped['Chat'] = relationship(back_populates='messages') user: Mapped['User'] = relationship(back_populates='messages') def __str__(self): return f'{self.content}' class PointsTransaction(BaseModel, DateModel): __tablename__ = 'system_points_transaction' __table_args__ = ( {'comment': '积分消费记录表'} ) user_id: Mapped[int] = mapped_column(Integer, comment='用户id') transaction_type: Mapped[int] = mapped_column(Integer, comment='交易类型:1-充值 2-消费 3-赠送 4-过期 5-系统调整') product_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='关联产品id') points: Mapped[int] = mapped_column(Integer, comment='积分数量(正数表示增加,负数表示减少)') balance: Mapped[int] = mapped_column(Integer, comment='交易后余额') status: Mapped[int] = mapped_column(Integer, default=1, comment='状态:0-无效 1-有效 2-已撤销') description: Mapped[str] = mapped_column(String(255), nullable=True, comment='交易描述') class ChatParticipant(BaseModel): __tablename__ = 'system_chat_participant' __table_args__ = ( {'comment': '聊天参与者关联表'} ) id: None = None user_id: Mapped[int] = mapped_column(Integer, ForeignKey('system_users.user_id'), primary_key=True) chat_id: Mapped[int] = mapped_column(Integer, ForeignKey('system_chats.id'), primary_key=True) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) last_read_message_id: Mapped[int] = mapped_column(default=0, nullable=True) last_del_message_id: Mapped[int] = mapped_column(default=0, nullable=True) chat: Mapped['Chat'] = relationship(back_populates='chat_participants') user: Mapped['User'] = relationship(back_populates='chat_participants')