models_db.py 35 KB

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