models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. """
  2. 数据库模型定义
  3. 根据提供的SQL文件创建对应的SQLAlchemy模型
  4. """
  5. from datetime import datetime
  6. from typing import Optional
  7. from flask_sqlalchemy import SQLAlchemy
  8. from sqlalchemy import text
  9. from sqlalchemy import BigInteger, String, Text, DateTime, Boolean, Integer, Enum
  10. from utils import get_beijing_time
  11. # 创建数据库实例
  12. db = SQLAlchemy()
  13. class User(db.Model):
  14. """增强版用户表"""
  15. __tablename__ = 'users'
  16. # 基础信息
  17. user_id = db.Column(BigInteger, primary_key=True, autoincrement=True)
  18. username = db.Column(String(50), nullable=False, unique=True, comment='用户名')
  19. # 登录方式相关
  20. login_type = db.Column(Enum('dingtalk', 'wecom', 'email', 'phone'), nullable=False, comment='登录方式')
  21. dingtalk_id = db.Column(String(200), unique=True, comment='钉钉用户ID')
  22. wecom_id = db.Column(String(200), unique=True, comment='企业微信用户ID')
  23. # 联系信息
  24. email = db.Column(String(255), unique=True, comment='邮箱地址')
  25. phone = db.Column(String(20), unique=True, comment='手机号码')
  26. # 密码安全(邮箱/手机登录使用)
  27. password_hash = db.Column(String(255), comment='密码哈希值')
  28. salt = db.Column(String(100), comment='密码盐值')
  29. failed_login_attempts = db.Column(Integer, default=0, comment='失败登录次数')
  30. locked_until = db.Column(DateTime, nullable=True, comment='账户锁定到期时间')
  31. # 个人信息
  32. avatar_url = db.Column(String(500), comment='用户头像URL')
  33. nickname = db.Column(String(100), comment='用户昵称')
  34. # 权限与状态
  35. role = db.Column(Enum('admin', 'user'), default='user', comment='用户角色')
  36. status = db.Column(Enum('active', 'inactive', 'suspended'), default='active', comment='用户状态')
  37. is_deleted = db.Column(Boolean, default=False, comment='是否已删除')
  38. deleted_at = db.Column(DateTime, nullable=True, comment='删除时间')
  39. # 第三方集成信息
  40. dingtalk_unionid = db.Column(String(200), comment='钉钉UnionID')
  41. wecom_unionid = db.Column(String(200), comment='企业微信UnionID')
  42. openid = db.Column(String(200), comment='通用OpenID')
  43. # 时间戳
  44. created_at = db.Column(DateTime, default=get_beijing_time, comment='创建时间')
  45. updated_at = db.Column(DateTime, default=get_beijing_time, onupdate=get_beijing_time, comment='更新时间')
  46. last_login_at = db.Column(DateTime, nullable=True, comment='最后登录时间')
  47. last_login_ip = db.Column(String(45), comment='最后登录IP')
  48. # 关系
  49. conversations = db.relationship('Conversation', backref='user', lazy=True, cascade='all, delete-orphan')
  50. def __repr__(self):
  51. return f'<User {self.username}>'
  52. def to_dict(self):
  53. """转换为字典格式"""
  54. return {
  55. 'user_id': self.user_id,
  56. 'username': self.username,
  57. 'login_type': self.login_type,
  58. 'dingtalk_id': self.dingtalk_id,
  59. 'wecom_id': self.wecom_id,
  60. 'email': self.email,
  61. 'phone': self.phone,
  62. 'avatar_url': self.avatar_url,
  63. 'nickname': self.nickname,
  64. 'role': self.role,
  65. 'status': self.status,
  66. 'is_deleted': self.is_deleted,
  67. 'deleted_at': self.deleted_at.isoformat() if self.deleted_at else None,
  68. 'dingtalk_unionid': self.dingtalk_unionid,
  69. 'wecom_unionid': self.wecom_unionid,
  70. 'openid': self.openid,
  71. 'created_at': self.created_at.isoformat() if self.created_at else None,
  72. 'updated_at': self.updated_at.isoformat() if self.updated_at else None,
  73. 'last_login_at': self.last_login_at.isoformat() if self.last_login_at else None,
  74. 'last_login_ip': self.last_login_ip
  75. }
  76. class Conversation(db.Model):
  77. """对话会话表"""
  78. __tablename__ = 'conversations'
  79. conversation_id = db.Column(BigInteger, primary_key=True, autoincrement=True)
  80. user_id = db.Column(BigInteger, db.ForeignKey('users.user_id'), nullable=False, comment='用户ID')
  81. title = db.Column(String(500), nullable=False, comment='对话标题')
  82. conversation_type = db.Column(Enum('chat', 'qa', 'assistant'), default='chat', comment='对话类型')
  83. session_id = db.Column(String(255), nullable=True, unique=True, comment='AI会话ID,用于上下文隔离')
  84. is_archived = db.Column(Boolean, default=False, comment='是否已归档')
  85. is_deleted = db.Column(Boolean, default=False, comment='是否已删除')
  86. deleted_at = db.Column(DateTime, nullable=True, comment='删除时间')
  87. message_count = db.Column(Integer, default=0, comment='消息数量')
  88. last_message_at = db.Column(DateTime, nullable=True, comment='最后消息时间')
  89. created_at = db.Column(DateTime, default=get_beijing_time, comment='创建时间')
  90. updated_at = db.Column(DateTime, default=get_beijing_time, onupdate=get_beijing_time, comment='更新时间')
  91. # 关系
  92. messages = db.relationship('Message', backref='conversation', lazy=True, cascade='all, delete-orphan')
  93. def __repr__(self):
  94. return f'<Conversation {self.conversation_id}: {self.title}>'
  95. def to_dict(self):
  96. """转换为字典格式"""
  97. return {
  98. 'conversation_id': self.conversation_id,
  99. 'user_id': self.user_id,
  100. 'title': self.title,
  101. 'conversation_type': self.conversation_type,
  102. 'session_id': self.session_id,
  103. 'is_archived': self.is_archived,
  104. 'is_deleted': self.is_deleted,
  105. 'deleted_at': self.deleted_at.isoformat() if self.deleted_at else None,
  106. 'message_count': self.message_count,
  107. 'last_message_at': self.last_message_at.isoformat() if self.last_message_at else None,
  108. 'created_at': self.created_at.isoformat() if self.created_at else None,
  109. 'updated_at': self.updated_at.isoformat() if self.updated_at else None
  110. }
  111. class Message(db.Model):
  112. """消息记录表"""
  113. __tablename__ = 'messages'
  114. message_id = db.Column(BigInteger, primary_key=True, autoincrement=True)
  115. conversation_id = db.Column(BigInteger, db.ForeignKey('conversations.conversation_id'), nullable=False, comment='会话ID')
  116. role = db.Column(Enum('user', 'assistant', 'system', 'thinking'), nullable=False, comment='消息角色')
  117. content = db.Column(Text, nullable=False, comment='消息内容')
  118. thinking_content = db.Column(Text, nullable=True, comment='AI思考过程内容')
  119. content_type = db.Column(Enum('text', 'image', 'file'), default='text', comment='内容类型')
  120. message_order = db.Column(Integer, nullable=False, comment='消息顺序(从1开始)')
  121. token_count = db.Column(Integer, default=0, comment='Token数量')
  122. is_deleted = db.Column(Boolean, default=False, comment='是否已删除')
  123. deleted_at = db.Column(DateTime, nullable=True, comment='删除时间')
  124. created_at = db.Column(DateTime, default=get_beijing_time, comment='创建时间')
  125. def __repr__(self):
  126. return f'<Message {self.message_id}: {self.role}>'
  127. def to_dict(self):
  128. """转换为字典格式"""
  129. return {
  130. 'message_id': self.message_id,
  131. 'conversation_id': self.conversation_id,
  132. 'role': self.role,
  133. 'content': self.content,
  134. 'thinking_content': self.thinking_content,
  135. 'content_type': self.content_type,
  136. 'message_order': self.message_order,
  137. 'token_count': self.token_count,
  138. 'is_deleted': self.is_deleted,
  139. 'deleted_at': self.deleted_at.isoformat() if self.deleted_at else None,
  140. 'created_at': self.created_at.isoformat() if self.created_at else None
  141. }
  142. class SystemConfig(db.Model):
  143. """系统配置表"""
  144. __tablename__ = 'system_config'
  145. config_id = db.Column(Integer, primary_key=True, autoincrement=True)
  146. config_key = db.Column(String(100), nullable=False, unique=True, comment='配置键')
  147. config_value = db.Column(Text, nullable=True, comment='配置值')
  148. config_type = db.Column(Enum('string', 'number', 'boolean', 'json'), default='string', comment='配置类型')
  149. description = db.Column(String(500), nullable=True, comment='配置描述')
  150. created_at = db.Column(DateTime, default=get_beijing_time)
  151. updated_at = db.Column(DateTime, default=get_beijing_time, onupdate=get_beijing_time)
  152. def __repr__(self):
  153. return f'<SystemConfig {self.config_key}>'
  154. def to_dict(self):
  155. """转换为字典格式"""
  156. return {
  157. 'config_id': self.config_id,
  158. 'config_key': self.config_key,
  159. 'config_value': self.config_value,
  160. 'config_type': self.config_type,
  161. 'description': self.description,
  162. 'created_at': self.created_at.isoformat() if self.created_at else None,
  163. 'updated_at': self.updated_at.isoformat() if self.updated_at else None
  164. }
  165. def init_database(app):
  166. """初始化数据库"""
  167. db.init_app(app)
  168. with app.app_context():
  169. # 创建所有表
  170. db.create_all()
  171. # 创建索引
  172. create_indexes()
  173. # 插入默认配置
  174. insert_default_configs()
  175. def create_indexes():
  176. """创建数据库索引"""
  177. try:
  178. # 定义所有需要创建的索引
  179. indexes = [
  180. # 用户表索引
  181. ("idx_users_login_type_status", "CREATE INDEX idx_users_login_type_status ON users (login_type, status)"),
  182. ("idx_users_role_status", "CREATE INDEX idx_users_role_status ON users (role, status)"),
  183. ("idx_users_email", "CREATE INDEX idx_users_email ON users (email)"),
  184. ("idx_users_phone", "CREATE INDEX idx_users_phone ON users (phone)"),
  185. ("idx_users_dingtalk", "CREATE INDEX idx_users_dingtalk ON users (dingtalk_id)"),
  186. ("idx_users_wecom", "CREATE INDEX idx_users_wecom ON users (wecom_id)"),
  187. ("idx_users_created_at", "CREATE INDEX idx_users_created_at ON users (created_at)"),
  188. ("idx_users_last_login", "CREATE INDEX idx_users_last_login ON users (last_login_at)"),
  189. ("idx_users_is_deleted", "CREATE INDEX idx_users_is_deleted ON users (is_deleted)"),
  190. ("idx_users_status_deleted", "CREATE INDEX idx_users_status_deleted ON users (status, is_deleted)"),
  191. # 对话表索引
  192. ("idx_conversations_user_id", "CREATE INDEX idx_conversations_user_id ON conversations (user_id)"),
  193. ("idx_conversations_user_updated", "CREATE INDEX idx_conversations_user_updated ON conversations (user_id, updated_at)"),
  194. ("idx_conversations_user_archived", "CREATE INDEX idx_conversations_user_archived ON conversations (user_id, is_archived)"),
  195. ("idx_conversations_user_deleted", "CREATE INDEX idx_conversations_user_deleted ON conversations (user_id, is_deleted)"),
  196. ("idx_conversations_updated_at", "CREATE INDEX idx_conversations_updated_at ON conversations (updated_at)"),
  197. ("idx_conversations_last_message", "CREATE INDEX idx_conversations_last_message ON conversations (last_message_at)"),
  198. ("idx_conversations_type_status", "CREATE INDEX idx_conversations_type_status ON conversations (conversation_type, is_deleted)"),
  199. # 消息表索引
  200. ("idx_messages_conversation_id", "CREATE INDEX idx_messages_conversation_id ON messages (conversation_id)"),
  201. ("idx_messages_conversation_order", "CREATE INDEX idx_messages_conversation_order ON messages (conversation_id, message_order)"),
  202. ("idx_messages_conversation_deleted", "CREATE INDEX idx_messages_conversation_deleted ON messages (conversation_id, is_deleted)"),
  203. ("idx_messages_created_at", "CREATE INDEX idx_messages_created_at ON messages (created_at)"),
  204. ("idx_messages_role", "CREATE INDEX idx_messages_role ON messages (role)"),
  205. ("idx_messages_content_type", "CREATE INDEX idx_messages_content_type ON messages (content_type)")
  206. ]
  207. db.session.commit()
  208. except Exception as e:
  209. db.session.rollback()
  210. def insert_default_configs():
  211. """插入默认系统配置"""
  212. try:
  213. # 检查是否已有配置
  214. if SystemConfig.query.first():
  215. return
  216. # 只插入必要的配置
  217. essential_configs = [
  218. ('system_name', 'AI聊天系统', 'string', '系统名称'),
  219. ('system_version', '2.0.0', 'string', '系统版本'),
  220. ('user_registration', 'true', 'boolean', '是否允许用户注册'),
  221. ('default_user_role', 'user', 'string', '默认用户角色')
  222. ]
  223. for config_key, config_value, config_type, description in essential_configs:
  224. config = SystemConfig(
  225. config_key=config_key,
  226. config_value=config_value,
  227. config_type=config_type,
  228. description=description
  229. )
  230. db.session.add(config)
  231. db.session.commit()
  232. except Exception as e:
  233. db.session.rollback()
  234. def get_or_create_user(login_type: str, **kwargs) -> User:
  235. """获取或创建用户"""
  236. try:
  237. # 根据登录类型查找用户
  238. if login_type == 'dingtalk':
  239. dingtalk_id = kwargs.get('dingtalk_id')
  240. if dingtalk_id:
  241. user = User.query.filter_by(dingtalk_id=dingtalk_id, is_deleted=False).first()
  242. if user:
  243. return user
  244. # 如果没找到,创建新用户
  245. # 确保username不重复且符合长度要求
  246. username = kwargs.get('username', f"{login_type}_user_{get_beijing_time().timestamp()}")
  247. # 确保用户名长度符合要求(至少3个字符)
  248. if len(username) < 3:
  249. username = f"{username}_{get_beijing_time().timestamp()}"
  250. # 检查username是否已存在,如果存在则生成新的
  251. counter = 1
  252. original_username = username
  253. while User.query.filter_by(username=username, is_deleted=False).first():
  254. username = f"{original_username}_{counter}"
  255. counter += 1
  256. # 从kwargs中移除username,避免重复参数
  257. if 'username' in kwargs:
  258. del kwargs['username']
  259. # 处理唯一约束冲突(email, phone等)
  260. # 如果email已存在,清空email
  261. if 'email' in kwargs and kwargs['email']:
  262. existing_email = User.query.filter_by(email=kwargs['email'], is_deleted=False).first()
  263. if existing_email:
  264. kwargs['email'] = ''
  265. # 如果phone已存在,清空phone
  266. if 'phone' in kwargs and kwargs['phone']:
  267. existing_phone = User.query.filter_by(phone=kwargs['phone'], is_deleted=False).first()
  268. if existing_phone:
  269. kwargs['phone'] = ''
  270. # 确保空字符串字段设置为None,避免数据库约束错误
  271. if 'email' in kwargs and kwargs['email'] == '':
  272. kwargs['email'] = None
  273. if 'phone' in kwargs and kwargs['phone'] == '':
  274. kwargs['phone'] = None
  275. user = User(
  276. login_type=login_type,
  277. username=username,
  278. **kwargs
  279. )
  280. db.session.add(user)
  281. db.session.commit()
  282. return user
  283. except Exception as e:
  284. db.session.rollback()
  285. raise
  286. def update_user_login_info(user: User, login_ip: str = None):
  287. """更新用户登录信息"""
  288. try:
  289. user.last_login_at = get_beijing_time()
  290. if login_ip:
  291. user.last_login_ip = login_ip
  292. db.session.commit()
  293. except Exception as e:
  294. db.session.rollback()
  295. raise