models_db.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. """
  2. SQLAlchemy 数据库模型定义
  3. """
  4. from datetime import datetime
  5. from typing import Optional
  6. from sqlalchemy import (
  7. Column, String, Integer, BigInteger, Text,
  8. Enum, Boolean, JSON, TIMESTAMP, Index, UniqueConstraint
  9. )
  10. from sqlalchemy.orm import declarative_base
  11. from sqlalchemy.sql import func
  12. PKBigInt = BigInteger().with_variant(Integer, "sqlite")
  13. Base = declarative_base()
  14. class User(Base):
  15. """用户表模型"""
  16. __tablename__ = "users"
  17. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  18. user_id = Column(String(64), unique=True, nullable=False, index=True, comment="用户唯一标识(UUID)")
  19. # 基本信息
  20. username = Column(String(100), nullable=False, comment="用户名/昵称")
  21. user_type = Column(Enum("guest", "registered", name="user_type"), default="guest", comment="用户类型")
  22. bot_name = Column(String(100), nullable=True, comment="用户给机器人取的名字")
  23. # 注册信息(可选)
  24. phone = Column(String(20), unique=True, nullable=True, index=True, comment="手机号")
  25. wechat_openid = Column(String(100), unique=True, nullable=True, index=True, comment="微信OpenID")
  26. google_email = Column(String(200), unique=True, nullable=True, index=True, comment="Google邮箱")
  27. # 认证信息
  28. is_verified = Column(Boolean, default=False, comment="是否已认证")
  29. registered_at = Column(TIMESTAMP, nullable=True, comment="注册时间")
  30. # 统计信息
  31. total_messages = Column(Integer, default=0, comment="总消息数")
  32. total_rounds = Column(Integer, default=0, comment="总对话轮次")
  33. total_speech_duration_ms = Column(BigInteger, default=0, comment="累积说话时长(毫秒)")
  34. last_active_at = Column(TIMESTAMP, nullable=True, comment="最后活跃时间")
  35. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  36. updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
  37. def to_dict(self) -> dict:
  38. """转换为字典"""
  39. return {
  40. "user_id": self.user_id,
  41. "username": self.username,
  42. "user_type": self.user_type,
  43. "bot_name": self.bot_name,
  44. "phone": self.phone,
  45. "wechat_openid": self.wechat_openid,
  46. "google_email": self.google_email,
  47. "is_verified": self.is_verified,
  48. "registered_at": self.registered_at.isoformat() if self.registered_at else None,
  49. "total_messages": self.total_messages,
  50. "total_rounds": self.total_rounds,
  51. "total_speech_duration_ms": self.total_speech_duration_ms,
  52. "last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
  53. "created_at": self.created_at.isoformat() if self.created_at else None,
  54. }
  55. class Message(Base):
  56. """消息表模型"""
  57. __tablename__ = "messages"
  58. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  59. message_id = Column(String(64), unique=True, nullable=False, comment="消息ID")
  60. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  61. # 消息内容
  62. role = Column(Enum("user", "assistant", "system", name="role"), nullable=False, comment="角色")
  63. content = Column(Text, nullable=False, comment="消息内容")
  64. # 轮次信息
  65. round_number = Column(Integer, nullable=False, comment="对话轮次(从1开始)")
  66. # 扩展信息
  67. emotion = Column(String(50), nullable=True, comment="情感标签")
  68. message_metadata = Column(JSON, nullable=True, comment="扩展信息(音频路径、识别耗时等)")
  69. token_count = Column(Integer, nullable=True, comment="Token数量")
  70. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), index=True, comment="创建时间")
  71. # 复合索引
  72. __table_args__ = (
  73. Index("idx_user_round", "user_id", "round_number"),
  74. )
  75. def to_dict(self) -> dict:
  76. """转换为字典"""
  77. return {
  78. "message_id": self.message_id,
  79. "user_id": self.user_id,
  80. "role": self.role,
  81. "content": self.content,
  82. "round_number": self.round_number,
  83. "emotion": self.emotion,
  84. "metadata": self.message_metadata, # 对外保持 metadata 字段名
  85. "token_count": self.token_count,
  86. "created_at": self.created_at.isoformat() if self.created_at else None,
  87. }
  88. class ConversationSummary(Base):
  89. """对话总结表模型"""
  90. __tablename__ = "conversation_summaries"
  91. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  92. summary_id = Column(String(64), unique=True, nullable=False, comment="总结ID")
  93. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  94. # 总结范围
  95. start_round = Column(Integer, nullable=False, comment="起始轮次")
  96. end_round = Column(Integer, nullable=False, comment="结束轮次")
  97. # 总结内容
  98. summary_text = Column(Text, nullable=False, comment="总结内容")
  99. summary_type = Column(Enum("auto", "manual", name="summary_type"), default="auto", comment="总结类型")
  100. # 统计信息
  101. message_count = Column(Integer, nullable=True, comment="总结的消息数量")
  102. token_count = Column(Integer, nullable=True, comment="总结文本Token数")
  103. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  104. # 复合索引和唯一约束
  105. __table_args__ = (
  106. Index("idx_user_end_round", "user_id", "end_round"),
  107. UniqueConstraint("user_id", "start_round", "end_round", name="uk_user_round_range"),
  108. )
  109. def to_dict(self) -> dict:
  110. """转换为字典"""
  111. return {
  112. "summary_id": self.summary_id,
  113. "user_id": self.user_id,
  114. "start_round": self.start_round,
  115. "end_round": self.end_round,
  116. "summary_text": self.summary_text,
  117. "summary_type": self.summary_type,
  118. "message_count": self.message_count,
  119. "token_count": self.token_count,
  120. "created_at": self.created_at.isoformat() if self.created_at else None,
  121. }
  122. class MissionType(Base):
  123. """任务类型配置"""
  124. __tablename__ = "mission_types"
  125. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  126. type_key = Column(String(64), unique=True, nullable=False, index=True, comment="任务类型唯一标识")
  127. display_name = Column(String(100), nullable=False, comment="任务显示名称")
  128. description = Column(String(255), nullable=True, comment="类型描述")
  129. is_repeatable = Column(Boolean, default=False, comment="是否允许重复触发")
  130. is_active = Column(Boolean, default=True, comment="是否启用")
  131. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
  132. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  133. updated_at = Column(
  134. TIMESTAMP,
  135. server_default=func.current_timestamp(),
  136. onupdate=func.current_timestamp(),
  137. comment="更新时间"
  138. )
  139. __table_args__ = (
  140. Index("idx_mission_type_active", "is_active"),
  141. )
  142. class MissionEnergyStone(Base):
  143. """任务能量石(首次对话奖励)"""
  144. __tablename__ = "mission_energy_stones"
  145. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  146. stone_id = Column(String(64), unique=True, nullable=False, index=True, comment="能量石ID")
  147. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  148. request_id = Column(String(64), nullable=True, comment="触发的请求ID")
  149. appear_reason = Column(String(64), nullable=False, default="first_dialog", comment="触发原因")
  150. mission_type = Column(String(64), nullable=False, default="first_dialog", comment="任务类型")
  151. appear_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="出现时间")
  152. clicked_at = Column(TIMESTAMP, nullable=True, comment="点击时间")
  153. auto_removed = Column(Boolean, default=False, comment="是否由系统自动消失")
  154. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  155. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  156. updated_at = Column(
  157. TIMESTAMP,
  158. server_default=func.current_timestamp(),
  159. onupdate=func.current_timestamp(),
  160. comment="更新时间"
  161. )
  162. __table_args__ = (
  163. Index("idx_mission_stone_user", "user_id", "appear_at"),
  164. Index("idx_mission_type_user", "mission_type", "user_id"),
  165. )
  166. class ConversationEnergyStone(Base):
  167. """对话能量石(按时长发放)"""
  168. __tablename__ = "conversation_energy_stones"
  169. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  170. stone_id = Column(String(64), unique=True, nullable=False, index=True, comment="能量石ID")
  171. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  172. request_id = Column(String(64), nullable=True, comment="触发的请求ID")
  173. threshold_index = Column(Integer, nullable=False, comment="达到的阈值序号(30秒一档)")
  174. total_duration_ms = Column(BigInteger, nullable=False, comment="达到阈值后的累计说话时长")
  175. duration_increment_ms = Column(Integer, nullable=False, comment="本次增加的说话时长")
  176. appear_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="出现时间")
  177. clicked_at = Column(TIMESTAMP, nullable=True, comment="点击时间")
  178. auto_removed = Column(Boolean, default=False, comment="是否由系统自动消失")
  179. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  180. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  181. updated_at = Column(
  182. TIMESTAMP,
  183. server_default=func.current_timestamp(),
  184. onupdate=func.current_timestamp(),
  185. comment="更新时间"
  186. )
  187. __table_args__ = (
  188. UniqueConstraint("user_id", "threshold_index", name="uk_conversation_stone_threshold"),
  189. Index("idx_conversation_stone_user_threshold", "user_id", "threshold_index"),
  190. )
  191. def _attach_metadata_alias(model_cls):
  192. """Allow using `metadata` keyword/attribute while storing in metadata_json."""
  193. original_init = model_cls.__init__
  194. def __init__(self, *args, **kwargs):
  195. metadata_value = kwargs.pop("metadata", None)
  196. original_init(self, *args, **kwargs)
  197. if metadata_value is not None:
  198. setattr(self, "metadata_json", metadata_value)
  199. model_cls.__init__ = __init__
  200. def getter(self):
  201. return getattr(self, "metadata_json")
  202. def setter(self, value):
  203. setattr(self, "metadata_json", value)
  204. model_cls.metadata = property(getter, setter)
  205. for _model in (MissionType, MissionEnergyStone, ConversationEnergyStone):
  206. _attach_metadata_alias(_model)