models_db.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. """
  2. SQLAlchemy 数据库模型定义
  3. """
  4. from datetime import datetime
  5. from typing import Optional
  6. from sqlalchemy import (
  7. Column,
  8. String,
  9. Integer,
  10. BigInteger,
  11. Text,
  12. Enum,
  13. Boolean,
  14. JSON,
  15. TIMESTAMP,
  16. Index,
  17. UniqueConstraint,
  18. )
  19. from sqlalchemy.orm import declarative_base
  20. from sqlalchemy.sql import func
  21. PKBigInt = BigInteger().with_variant(Integer, "sqlite")
  22. Base = declarative_base()
  23. class User(Base):
  24. """用户表模型"""
  25. __tablename__ = "users"
  26. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  27. user_id = Column(String(64), unique=True, nullable=False, index=True, comment="用户唯一标识(UUID)")
  28. # 基本信息
  29. username = Column(String(100), nullable=False, comment="用户名/昵称")
  30. user_type = Column(Enum("guest", "registered", name="user_type"), default="guest", comment="用户类型")
  31. bot_name = Column(String(100), nullable=True, comment="用户给机器人取的名字")
  32. # 注册信息(可选)
  33. area_code = Column(String(10), nullable=True, comment="手机区号(如 +86)")
  34. phone = Column(String(20), unique=True, nullable=True, index=True, comment="手机号")
  35. email = Column(String(200), unique=True, nullable=True, index=True, comment="邮箱")
  36. wechat_openid = Column(String(100), unique=True, nullable=True, index=True, comment="微信OpenID")
  37. qq_openid = Column(String(100), unique=True, nullable=True, index=True, comment="QQ OpenID")
  38. qq_unionid = Column(String(128), unique=True, nullable=True, index=True, comment="QQ UnionID")
  39. google_email = Column(String(200), unique=True, nullable=True, index=True, comment="Google邮箱")
  40. # 认证信息
  41. is_verified = Column(Boolean, default=False, comment="是否已认证")
  42. registered_at = Column(TIMESTAMP, nullable=True, comment="注册时间")
  43. # 统计信息
  44. total_messages = Column(Integer, default=0, comment="总消息数")
  45. total_rounds = Column(Integer, default=0, comment="总对话轮次")
  46. total_speech_duration_ms = Column(BigInteger, default=0, comment="累积说话时长(毫秒)")
  47. user_level = Column(Integer, default=0, nullable=False, comment="用户等级(0-5)")
  48. last_active_at = Column(TIMESTAMP, nullable=True, comment="最后活跃时间")
  49. # 背景图片
  50. current_background_image_id = Column(String(64), nullable=True, comment="当前使用的背景图片ID")
  51. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  52. updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(), onupdate=func.current_timestamp(), comment="更新时间")
  53. def to_dict(self) -> dict:
  54. """转换为字典"""
  55. return {
  56. "user_id": self.user_id,
  57. "username": self.username,
  58. "user_type": self.user_type,
  59. "bot_name": self.bot_name,
  60. "area_code": self.area_code,
  61. "phone": self.phone,
  62. "email": self.email,
  63. "wechat_openid": self.wechat_openid,
  64. "qq_openid": self.qq_openid,
  65. "qq_unionid": self.qq_unionid,
  66. "google_email": self.google_email,
  67. "is_verified": self.is_verified,
  68. "registered_at": self.registered_at.isoformat() if self.registered_at else None,
  69. "total_messages": self.total_messages,
  70. "total_rounds": self.total_rounds,
  71. "total_speech_duration_ms": self.total_speech_duration_ms,
  72. "user_level": self.user_level,
  73. "last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
  74. "current_background_image_id": self.current_background_image_id,
  75. "created_at": self.created_at.isoformat() if self.created_at else None,
  76. }
  77. class Message(Base):
  78. """消息表模型"""
  79. __tablename__ = "messages"
  80. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  81. message_id = Column(String(64), unique=True, nullable=False, comment="消息ID")
  82. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  83. # 消息内容
  84. role = Column(Enum("user", "assistant", "system", name="role"), nullable=False, comment="角色")
  85. content = Column(Text, nullable=False, comment="消息内容")
  86. # 轮次信息
  87. round_number = Column(Integer, nullable=False, comment="对话轮次(从1开始)")
  88. # 扩展信息
  89. emotion = Column(String(50), nullable=True, comment="情感标签")
  90. message_metadata = Column(JSON, nullable=True, comment="扩展信息(音频路径、识别耗时等)")
  91. token_count = Column(Integer, nullable=True, comment="Token数量")
  92. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), index=True, comment="创建时间")
  93. # 复合索引
  94. __table_args__ = (
  95. Index("idx_user_round", "user_id", "round_number"),
  96. )
  97. def to_dict(self) -> dict:
  98. """转换为字典"""
  99. return {
  100. "message_id": self.message_id,
  101. "user_id": self.user_id,
  102. "role": self.role,
  103. "content": self.content,
  104. "round_number": self.round_number,
  105. "emotion": self.emotion,
  106. "metadata": self.message_metadata, # 对外保持 metadata 字段名
  107. "token_count": self.token_count,
  108. "created_at": self.created_at.isoformat() if self.created_at else None,
  109. }
  110. class ConversationSummary(Base):
  111. """对话总结表模型"""
  112. __tablename__ = "conversation_summaries"
  113. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  114. summary_id = Column(String(64), unique=True, nullable=False, comment="总结ID")
  115. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  116. # 总结范围
  117. start_round = Column(Integer, nullable=False, comment="起始轮次")
  118. end_round = Column(Integer, nullable=False, comment="结束轮次")
  119. # 总结内容
  120. summary_text = Column(Text, nullable=False, comment="总结内容")
  121. summary_type = Column(Enum("auto", "manual", name="summary_type"), default="auto", comment="总结类型")
  122. # 统计信息
  123. message_count = Column(Integer, nullable=True, comment="总结的消息数量")
  124. token_count = Column(Integer, nullable=True, comment="总结文本Token数")
  125. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  126. # 复合索引和唯一约束
  127. __table_args__ = (
  128. Index("idx_user_end_round", "user_id", "end_round"),
  129. UniqueConstraint("user_id", "start_round", "end_round", name="uk_user_round_range"),
  130. )
  131. def to_dict(self) -> dict:
  132. """转换为字典"""
  133. return {
  134. "summary_id": self.summary_id,
  135. "user_id": self.user_id,
  136. "start_round": self.start_round,
  137. "end_round": self.end_round,
  138. "summary_text": self.summary_text,
  139. "summary_type": self.summary_type,
  140. "message_count": self.message_count,
  141. "token_count": self.token_count,
  142. "created_at": self.created_at.isoformat() if self.created_at else None,
  143. }
  144. class MissionType(Base):
  145. """任务类型配置"""
  146. __tablename__ = "mission_types"
  147. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  148. type_key = Column(String(64), unique=True, nullable=False, index=True, comment="任务类型唯一标识")
  149. display_name = Column(String(100), nullable=False, comment="任务显示名称")
  150. description = Column(String(255), nullable=True, comment="类型描述")
  151. is_repeatable = Column(Boolean, default=False, comment="是否允许重复触发")
  152. is_active = Column(Boolean, default=True, comment="是否启用")
  153. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
  154. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  155. updated_at = Column(
  156. TIMESTAMP,
  157. server_default=func.current_timestamp(),
  158. onupdate=func.current_timestamp(),
  159. comment="更新时间"
  160. )
  161. __table_args__ = (
  162. Index("idx_mission_type_active", "is_active"),
  163. )
  164. class MissionEnergyStone(Base):
  165. """任务能量石(首次对话奖励)"""
  166. __tablename__ = "mission_energy_stones"
  167. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  168. stone_id = Column(String(64), unique=True, nullable=False, index=True, comment="能量石ID")
  169. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  170. request_id = Column(String(64), nullable=True, comment="触发的请求ID")
  171. appear_reason = Column(String(64), nullable=False, default="first_dialog", comment="触发原因")
  172. mission_type = Column(String(64), nullable=False, default="first_dialog", comment="任务类型")
  173. appear_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="出现时间")
  174. clicked_at = Column(TIMESTAMP, nullable=True, comment="点击时间")
  175. auto_removed = Column(Boolean, default=False, comment="是否由系统自动消失")
  176. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  177. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  178. updated_at = Column(
  179. TIMESTAMP,
  180. server_default=func.current_timestamp(),
  181. onupdate=func.current_timestamp(),
  182. comment="更新时间"
  183. )
  184. __table_args__ = (
  185. Index("idx_mission_stone_user", "user_id", "appear_at"),
  186. Index("idx_mission_type_user", "mission_type", "user_id"),
  187. )
  188. class ConversationEnergyStone(Base):
  189. """对话能量石(按时长发放)"""
  190. __tablename__ = "conversation_energy_stones"
  191. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  192. stone_id = Column(String(64), unique=True, nullable=False, index=True, comment="能量石ID")
  193. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  194. request_id = Column(String(64), nullable=True, comment="触发的请求ID")
  195. threshold_index = Column(Integer, nullable=False, comment="达到的阈值序号(30秒一档)")
  196. total_duration_ms = Column(BigInteger, nullable=False, comment="达到阈值后的累计说话时长")
  197. duration_increment_ms = Column(Integer, nullable=False, comment="本次增加的说话时长")
  198. appear_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="出现时间")
  199. clicked_at = Column(TIMESTAMP, nullable=True, comment="点击时间")
  200. auto_removed = Column(Boolean, default=False, comment="是否由系统自动消失")
  201. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  202. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  203. updated_at = Column(
  204. TIMESTAMP,
  205. server_default=func.current_timestamp(),
  206. onupdate=func.current_timestamp(),
  207. comment="更新时间"
  208. )
  209. __table_args__ = (
  210. UniqueConstraint("user_id", "threshold_index", name="uk_conversation_stone_threshold"),
  211. Index("idx_conversation_stone_user_threshold", "user_id", "threshold_index"),
  212. )
  213. class UserUIState(Base):
  214. """用户前端可视化状态表(页面与提示状态)"""
  215. __tablename__ = "user_ui_states"
  216. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  217. user_id = Column(String(64), unique=True, nullable=False, index=True, comment="用户ID(关联 users.user_id)")
  218. # 页面与路由状态
  219. current_page = Column(String(64), nullable=True, comment="当前业务流程页面标识")
  220. visited_routes = Column(JSON, nullable=True, comment="已访问路由列表")
  221. # 新手引导与提示状态
  222. has_shown_tap_to_chat = Column(Boolean, default=False, nullable=False, comment="是否已展示过 TapToChat 提示")
  223. has_shown_cancel_tooltip = Column(Boolean, default=False, nullable=False, comment="是否已展示过取消按钮提示")
  224. has_shown_send_tooltip = Column(Boolean, default=False, nullable=False, comment="是否已展示过发送按钮提示")
  225. has_shown_progress_bar_tooltip = Column(Boolean, default=False, nullable=False, comment="是否已展示过进度条提示")
  226. has_shown_mission_stone_modal = Column(Boolean, default=False, nullable=False, comment="是否已展示过任务能量石弹窗")
  227. has_shown_conversation_stone_modal = Column(Boolean, default=False, nullable=False, comment="是否已展示过对话能量石弹窗")
  228. has_clicked_mission_stone = Column(Boolean, default=False, nullable=False, comment="是否已点击过任务能量石(控制光晕显示)")
  229. has_clicked_conversation_stone = Column(Boolean, default=False, nullable=False, comment="是否已点击过对话能量石(控制光晕显示)")
  230. # Avatar-C 解锁状态
  231. avatar_c_unlocked = Column(Boolean, default=False, nullable=False, comment="是否已解锁 Avatar-C")
  232. has_shared_avatar = Column(Boolean, default=False, nullable=False, comment="是否已分享头像奖励")
  233. # 当前选中的形象
  234. current_avatar_id = Column(String(64), nullable=True, comment="当前使用的形象ID(关联 avatars.avatar_id)")
  235. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  236. updated_at = Column(
  237. TIMESTAMP,
  238. server_default=func.current_timestamp(),
  239. onupdate=func.current_timestamp(),
  240. comment="更新时间",
  241. )
  242. def to_dict(self) -> dict:
  243. return {
  244. "user_id": self.user_id,
  245. "current_page": self.current_page,
  246. "visited_routes": self.visited_routes or [],
  247. "has_shown_tap_to_chat": self.has_shown_tap_to_chat,
  248. "has_shown_cancel_tooltip": self.has_shown_cancel_tooltip,
  249. "has_shown_send_tooltip": self.has_shown_send_tooltip,
  250. "has_shown_progress_bar_tooltip": self.has_shown_progress_bar_tooltip,
  251. "has_shown_mission_stone_modal": self.has_shown_mission_stone_modal,
  252. "has_shown_conversation_stone_modal": self.has_shown_conversation_stone_modal,
  253. "has_clicked_mission_stone": self.has_clicked_mission_stone,
  254. "has_clicked_conversation_stone": self.has_clicked_conversation_stone,
  255. "avatar_c_unlocked": self.avatar_c_unlocked,
  256. "has_shared_avatar": self.has_shared_avatar,
  257. "current_avatar_id": self.current_avatar_id,
  258. "created_at": self.created_at.isoformat() if self.created_at else None,
  259. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  260. }
  261. class Avatar(Base):
  262. """形象配置表"""
  263. __tablename__ = "avatars"
  264. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  265. avatar_id = Column(String(64), unique=True, nullable=False, index=True, comment="形象唯一标识")
  266. # 基本信息
  267. avatar_name = Column(String(100), nullable=False, comment="形象名称")
  268. description = Column(String(255), nullable=True, comment="形象描述")
  269. is_active = Column(Boolean, default=True, nullable=False, comment="是否启用")
  270. # 扩展信息
  271. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
  272. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  273. updated_at = Column(
  274. TIMESTAMP,
  275. server_default=func.current_timestamp(),
  276. onupdate=func.current_timestamp(),
  277. comment="更新时间",
  278. )
  279. __table_args__ = (
  280. Index("idx_avatar_is_active", "is_active"),
  281. )
  282. def to_dict(self) -> dict:
  283. return {
  284. "avatar_id": self.avatar_id,
  285. "avatar_name": self.avatar_name,
  286. "description": self.description,
  287. "is_active": self.is_active,
  288. "metadata": self.metadata_json,
  289. "created_at": self.created_at.isoformat() if self.created_at else None,
  290. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  291. }
  292. class AvatarAsset(Base):
  293. """形象动作/情绪资源表"""
  294. __tablename__ = "avatar_assets"
  295. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  296. avatar_id = Column(String(64), nullable=False, index=True, comment="形象ID")
  297. state = Column(
  298. Enum(
  299. "default",
  300. "positive",
  301. "negative",
  302. "talking",
  303. "idle",
  304. "sleep",
  305. name="avatar_state",
  306. ),
  307. nullable=False,
  308. default="default",
  309. comment="动作/情绪状态",
  310. )
  311. image_url = Column(String(500), nullable=False, comment="GIF 资源 URL")
  312. thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
  313. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
  314. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  315. updated_at = Column(
  316. TIMESTAMP,
  317. server_default=func.current_timestamp(),
  318. onupdate=func.current_timestamp(),
  319. comment="更新时间",
  320. )
  321. __table_args__ = (
  322. UniqueConstraint("avatar_id", "state", name="uk_avatar_asset_state"),
  323. Index("idx_avatar_asset_avatar_id", "avatar_id"),
  324. )
  325. def to_dict(self) -> dict:
  326. return {
  327. "id": self.id,
  328. "avatar_id": self.avatar_id,
  329. "state": self.state,
  330. "image_url": self.image_url,
  331. "thumbnail_url": self.thumbnail_url,
  332. "metadata": self.metadata_json,
  333. "created_at": self.created_at.isoformat() if self.created_at else None,
  334. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  335. }
  336. class UserAvatar(Base):
  337. """用户已拥有的形象"""
  338. __tablename__ = "user_avatars"
  339. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  340. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  341. avatar_id = Column(String(64), nullable=False, index=True, comment="形象ID")
  342. obtained_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="获得时间")
  343. obtained_type = Column(
  344. Enum("default", "register", "level_up", "mission", "purchase", "gift", "event", name="avatar_obtained_type"),
  345. default="default",
  346. comment="获得方式",
  347. )
  348. obtained_source = Column(String(64), nullable=True, comment="获得来源(等级/任务/订单等)")
  349. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  350. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  351. __table_args__ = (
  352. UniqueConstraint("user_id", "avatar_id", name="uk_user_avatar"),
  353. Index("idx_user_avatar_user_obtained", "user_id", "obtained_at"),
  354. )
  355. def to_dict(self) -> dict:
  356. return {
  357. "id": self.id,
  358. "user_id": self.user_id,
  359. "avatar_id": self.avatar_id,
  360. "obtained_at": self.obtained_at.isoformat() if self.obtained_at else None,
  361. "obtained_type": self.obtained_type,
  362. "obtained_source": self.obtained_source,
  363. "metadata": self.metadata_json,
  364. "created_at": self.created_at.isoformat() if self.created_at else None,
  365. }
  366. class Feedback(Base):
  367. """用户反馈表模型"""
  368. __tablename__ = "feedbacks"
  369. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  370. feedback_id = Column(String(64), unique=True, nullable=False, index=True, comment="反馈ID")
  371. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  372. # 反馈内容
  373. content = Column(Text, nullable=False, comment="反馈内容")
  374. # 状态信息
  375. status = Column(
  376. Enum("pending", "reviewed", "resolved", "closed", name="feedback_status"),
  377. default="pending",
  378. nullable=False,
  379. comment="反馈状态"
  380. )
  381. # 扩展信息
  382. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  383. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), index=True, comment="创建时间")
  384. updated_at = Column(
  385. TIMESTAMP,
  386. server_default=func.current_timestamp(),
  387. onupdate=func.current_timestamp(),
  388. comment="更新时间",
  389. )
  390. __table_args__ = (
  391. Index("idx_feedback_user_created", "user_id", "created_at"),
  392. )
  393. def to_dict(self) -> dict:
  394. """转换为字典"""
  395. return {
  396. "feedback_id": self.feedback_id,
  397. "user_id": self.user_id,
  398. "content": self.content,
  399. "status": self.status,
  400. "metadata": self.metadata_json,
  401. "created_at": self.created_at.isoformat() if self.created_at else None,
  402. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  403. }
  404. def _attach_metadata_alias(model_cls):
  405. """Allow using `metadata` keyword/attribute while storing in metadata_json."""
  406. original_init = model_cls.__init__
  407. def __init__(self, *args, **kwargs):
  408. metadata_value = kwargs.pop("metadata", None)
  409. original_init(self, *args, **kwargs)
  410. if metadata_value is not None:
  411. setattr(self, "metadata_json", metadata_value)
  412. model_cls.__init__ = __init__
  413. def getter(self):
  414. return getattr(self, "metadata_json")
  415. def setter(self, value):
  416. setattr(self, "metadata_json", value)
  417. model_cls.metadata = property(getter, setter)
  418. class BackgroundImage(Base):
  419. """背景图片配置表"""
  420. __tablename__ = "background_images"
  421. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  422. image_id = Column(String(64), unique=True, nullable=False, index=True, comment="背景图片唯一标识")
  423. # 基本信息
  424. image_name = Column(String(100), nullable=False, comment="背景图片名称")
  425. image_url = Column(String(500), nullable=False, comment="背景图片URL")
  426. thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
  427. # 解锁条件
  428. unlock_type = Column(Enum("default", "level", "purchase", "mission", name="unlock_type"),
  429. default="default", comment="解锁类型")
  430. unlock_level = Column(Integer, nullable=True, comment="解锁所需等级(unlock_type=level时)")
  431. unlock_mission_type = Column(String(64), nullable=True, comment="解锁所需任务类型(unlock_type=mission时)")
  432. # 显示信息
  433. display_order = Column(Integer, default=0, comment="显示顺序")
  434. is_active = Column(Boolean, default=True, comment="是否启用")
  435. description = Column(String(255), nullable=True, comment="背景图片描述")
  436. # 扩展信息
  437. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展配置")
  438. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  439. updated_at = Column(TIMESTAMP, server_default=func.current_timestamp(),
  440. onupdate=func.current_timestamp(), comment="更新时间")
  441. __table_args__ = (
  442. Index("idx_unlock_type", "unlock_type"),
  443. Index("idx_display_order", "display_order"),
  444. )
  445. def to_dict(self) -> dict:
  446. """转换为字典"""
  447. return {
  448. "image_id": self.image_id,
  449. "image_name": self.image_name,
  450. "image_url": self.image_url,
  451. "thumbnail_url": self.thumbnail_url,
  452. "unlock_type": self.unlock_type,
  453. "unlock_level": self.unlock_level,
  454. "unlock_mission_type": self.unlock_mission_type,
  455. "display_order": self.display_order,
  456. "is_active": self.is_active,
  457. "description": self.description,
  458. "metadata": self.metadata_json,
  459. "created_at": self.created_at.isoformat() if self.created_at else None,
  460. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  461. }
  462. class UserBackgroundImage(Base):
  463. """用户背景图片关联表(记录用户已获得的背景图片)"""
  464. __tablename__ = "user_background_images"
  465. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  466. user_id = Column(String(64), nullable=False, index=True, comment="用户ID")
  467. image_id = Column(String(64), nullable=False, index=True, comment="背景图片ID")
  468. # 获得信息
  469. obtained_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="获得时间")
  470. obtained_type = Column(Enum("default", "level_up", "purchase", "mission", "gift", name="obtained_type"),
  471. default="default", comment="获得方式")
  472. obtained_source = Column(String(64), nullable=True, comment="获得来源(任务类型、等级等)")
  473. # 扩展信息
  474. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息")
  475. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), comment="创建时间")
  476. __table_args__ = (
  477. UniqueConstraint("user_id", "image_id", name="uk_user_background_image"),
  478. Index("idx_user_obtained", "user_id", "obtained_at"),
  479. )
  480. def to_dict(self) -> dict:
  481. """转换为字典"""
  482. return {
  483. "id": self.id,
  484. "user_id": self.user_id,
  485. "image_id": self.image_id,
  486. "obtained_at": self.obtained_at.isoformat() if self.obtained_at else None,
  487. "obtained_type": self.obtained_type,
  488. "obtained_source": self.obtained_source,
  489. "metadata": self.metadata_json,
  490. "created_at": self.created_at.isoformat() if self.created_at else None,
  491. }
  492. class VerificationCode(Base):
  493. """验证码表模型"""
  494. __tablename__ = "verification_codes"
  495. id = Column(PKBigInt, primary_key=True, autoincrement=True)
  496. code_id = Column(String(64), unique=True, nullable=False, index=True, comment="验证码ID")
  497. # 接收方信息(手机号或邮箱,根据发送类型二选一)
  498. area_code = Column(String(10), nullable=True, comment="手机区号(如 +86)")
  499. phone = Column(String(20), nullable=True, index=True, comment="手机号")
  500. email = Column(String(200), nullable=True, index=True, comment="邮箱")
  501. # 验证码信息
  502. code = Column(String(10), nullable=False, comment="验证码")
  503. send_type = Column(
  504. Enum("domestic_sms", "international_sms", "email", name="send_type"),
  505. nullable=False,
  506. comment="发送类型:国内短信、国外短信、邮箱"
  507. )
  508. # 状态信息
  509. is_sent = Column(Boolean, default=False, nullable=False, comment="是否已发送")
  510. is_valid = Column(Boolean, default=True, nullable=False, comment="是否有效")
  511. # 时间信息
  512. created_at = Column(TIMESTAMP, server_default=func.current_timestamp(), index=True, comment="创建时间")
  513. sent_at = Column(TIMESTAMP, nullable=True, comment="发送时间")
  514. # 扩展信息
  515. metadata_json = Column("metadata", JSON, nullable=True, comment="扩展信息(发送结果、错误信息等)")
  516. __table_args__ = (
  517. Index("idx_verification_code_phone", "phone", "is_valid", "created_at"),
  518. Index("idx_verification_code_email", "email", "is_valid", "created_at"),
  519. Index("idx_verification_code_send_type", "send_type", "is_sent", "is_valid"),
  520. )
  521. def to_dict(self) -> dict:
  522. """转换为字典"""
  523. return {
  524. "code_id": self.code_id,
  525. "area_code": self.area_code,
  526. "phone": self.phone,
  527. "email": self.email,
  528. "code": self.code,
  529. "send_type": self.send_type,
  530. "is_sent": self.is_sent,
  531. "is_valid": self.is_valid,
  532. "created_at": self.created_at.isoformat() if self.created_at else None,
  533. "sent_at": self.sent_at.isoformat() if self.sent_at else None,
  534. "metadata": self.metadata_json,
  535. }
  536. for _model in (
  537. MissionType,
  538. MissionEnergyStone,
  539. ConversationEnergyStone,
  540. BackgroundImage,
  541. UserBackgroundImage,
  542. Avatar,
  543. AvatarAsset,
  544. UserAvatar,
  545. ):
  546. _attach_metadata_alias(_model)