Files
life-echo/api/database/models.py
2026-03-01 10:12:23 +01:00

190 lines
8.1 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, timezone
from typing import Optional, List
from sqlalchemy import Column, String, Integer, DateTime, Boolean, Text, ForeignKey, JSON
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
def utc_now():
"""返回当前 UTC 时间(带时区信息)"""
return datetime.now(timezone.utc)
class User(Base):
"""用户表"""
__tablename__ = "users"
id = Column(String, primary_key=True)
phone = Column(String, unique=True, nullable=False, index=True) # 手机号(唯一,必填)
password_hash = Column(String, nullable=False) # 密码哈希
email = Column(String, unique=True, nullable=True) # 邮箱(可选)
openid = Column(String, unique=True, nullable=True) # 微信 OpenID可选
nickname = Column(String, nullable=False)
avatar_url = Column(String, nullable=True)
subscription_type = Column(String, default="free") # free, premium
subscription_expires_at = Column(DateTime(timezone=True), nullable=True) # 订阅到期时间
created_at = Column(DateTime(timezone=True), default=utc_now)
birth_year = Column(Integer, nullable=True)
birth_place = Column(String, nullable=True)
grew_up_place = Column(String, nullable=True)
occupation = Column(String, nullable=True)
# Relationships
conversations = relationship("Conversation", back_populates="user")
chapters = relationship("Chapter", back_populates="user")
books = relationship("Book", back_populates="user")
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
memoir_state = relationship("MemoirState", back_populates="user", uselist=False, cascade="all, delete-orphan")
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
class Conversation(Base):
"""对话会话表"""
__tablename__ = "conversations"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
started_at = Column(DateTime(timezone=True), default=utc_now)
ended_at = Column(DateTime(timezone=True), nullable=True)
duration_seconds = Column(Integer, default=0)
summary = Column(Text, nullable=True)
status = Column(String, default="active") # active, ended, processing
current_topic = Column(String, nullable=True)
conversation_stage = Column(String, nullable=True) # childhood, education, career, family, beliefs, summary
# Relationships
user = relationship("User", back_populates="conversations")
segments = relationship("Segment", back_populates="conversation", cascade="all, delete-orphan")
class Segment(Base):
"""对话段落表"""
__tablename__ = "segments"
id = Column(String, primary_key=True)
conversation_id = Column(String, ForeignKey("conversations.id"), nullable=False)
audio_url = Column(String, nullable=True)
transcript_text = Column(Text, nullable=False)
created_at = Column(DateTime(timezone=True), default=utc_now)
processed = Column(Boolean, default=False)
topic_category = Column(String, nullable=True)
agent_response = Column(Text, nullable=True)
# Relationships
conversation = relationship("Conversation", back_populates="segments")
class Chapter(Base):
"""章节表"""
__tablename__ = "chapters"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
order_index = Column(Integer, nullable=False)
status = Column(String, default="draft") # draft, completed
images = Column(JSON, nullable=True) # 图片 URL 列表
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
category = Column(String, nullable=True) # 章节分类
is_new = Column(Boolean, default=True) # 是否为新内容(未读)
is_active = Column(Boolean, default=True) # 是否启用(清除回忆后置为 False
source_segments = Column(JSON, nullable=True) # 来源 segment IDs 列表
# Relationships
user = relationship("User", back_populates="chapters")
class Book(Base):
"""回忆录表"""
__tablename__ = "books"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
title = Column(String, nullable=False)
total_pages = Column(Integer, default=0)
total_words = Column(Integer, default=0)
cover_image_url = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
has_update = Column(Boolean, default=False) # 是否有新内容
last_update_chapter_id = Column(String, nullable=True) # 最近更新的章节 ID
# Relationships
user = relationship("User", back_populates="books")
class MemoirState(Base):
"""回忆录状态表 - 对话 Agent 与后台 Agent 共享"""
__tablename__ = "memoir_states"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), unique=True, nullable=False)
stage_order = Column(JSON, default=list) # 阶段顺序
current_stage = Column(String, default="childhood") # 当前阶段
covered_stages = Column(JSON, default=list) # 已完成阶段列表
slots = Column(JSON, nullable=False) # 各阶段 slot 信息
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
# Relationships
user = relationship("User", back_populates="memoir_state")
class RefreshToken(Base):
"""刷新令牌表"""
__tablename__ = "refresh_tokens"
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
token = Column(String, unique=True, nullable=False, index=True) # 刷新令牌(唯一)
expires_at = Column(DateTime(timezone=True), nullable=False) # 过期时间30天后
created_at = Column(DateTime(timezone=True), default=utc_now)
is_revoked = Column(Boolean, default=False) # 是否已撤销
device_info = Column(String, nullable=True) # 设备信息(用于全设备登出)
# Relationships
user = relationship("User", back_populates="refresh_tokens")
class SmsVerificationCode(Base):
"""短信验证码表"""
__tablename__ = "sms_verification_codes"
id = Column(String, primary_key=True)
phone = Column(String, nullable=False, index=True) # 手机号
code = Column(String, nullable=False) # 6位验证码
purpose = Column(String, nullable=False) # register/login/reset_password/change_phone
is_used = Column(Boolean, default=False) # 是否已使用
is_expired = Column(Boolean, default=False) # 是否已过期
expires_at = Column(DateTime(timezone=True), nullable=False) # 过期时间5分钟后
created_at = Column(DateTime(timezone=True), default=utc_now)
verified_at = Column(DateTime(timezone=True), nullable=True) # 验证时间
ip_address = Column(String, nullable=True) # 请求IP地址
class Order(Base):
"""支付订单表"""
__tablename__ = "orders"
id = Column(String, primary_key=True) # 内部订单号
user_id = Column(String, ForeignKey("users.id"), nullable=False, index=True)
plan_id = Column(String, nullable=False) # 套餐 IDfree / premium
plan_name = Column(String, nullable=False) # 套餐名称
amount = Column(Integer, nullable=False) # 金额(单位:分)
currency = Column(String, default="CNY")
payment_method = Column(String, nullable=False) # wechat / alipay
status = Column(String, default="pending") # pending / paid / failed / cancelled / refunded
trade_no = Column(String, nullable=True, index=True) # 第三方交易号(微信/支付宝)
paid_at = Column(DateTime(timezone=True), nullable=True) # 支付完成时间
created_at = Column(DateTime(timezone=True), default=utc_now)
expired_at = Column(DateTime(timezone=True), nullable=True) # 订单超时时间
# Relationships
user = relationship("User", back_populates="orders")