user.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """用户CRUD"""
  2. from typing import Optional, List, Tuple
  3. from sqlalchemy.orm import Session
  4. from sqlalchemy import and_
  5. from app.models.models import SysUser, SysUserHis
  6. from app.crud.base import CRUDBase
  7. from app.core.security import get_password_hash
  8. from app.core.utils import generate_random_password
  9. # 运营端「重置密码」专用明文:仅服务端使用,禁止由 HTTP 请求传入
  10. DEFAULT_ADMIN_RESET_PLAIN_PASSWORD = "111111"
  11. class UserCRUD(CRUDBase[SysUser]):
  12. """用户CRUD操作"""
  13. def get_by_username(self, db: Session, username: str) -> Optional[SysUser]:
  14. """根据用户名获取用户(仅未删除)"""
  15. return db.query(SysUser).filter(
  16. and_(SysUser.username == username, SysUser.is_del == 0)
  17. ).first()
  18. def get_by_username_any(self, db: Session, username: str) -> Optional[SysUser]:
  19. """根据用户名获取用户(含已软删;用于 uk_username 唯一约束校验)"""
  20. if not username:
  21. return None
  22. return db.query(SysUser).filter(SysUser.username == username).first()
  23. @staticmethod
  24. def _release_username(original_username: str, user_id: str) -> str:
  25. """软删后释放 uk_username:改写为不可登录的占位名。"""
  26. suffix = (user_id or "")[:8] or "x"
  27. base = (original_username or "user")[:30]
  28. return f"{base}__del__{suffix}"
  29. def get_by_phone(self, db: Session, phone: str) -> Optional[SysUser]:
  30. """根据手机号获取用户"""
  31. return db.query(SysUser).filter(
  32. and_(SysUser.phone == phone, SysUser.is_del == 0)
  33. ).first()
  34. def get_by_wx_mini_openid(self, db: Session, openid: str) -> Optional[SysUser]:
  35. """根据微信小程序 openid 获取用户"""
  36. if not openid:
  37. return None
  38. return db.query(SysUser).filter(
  39. and_(SysUser.wx_mini_openid == openid, SysUser.is_del == 0)
  40. ).first()
  41. def ensure_wx_mini_openid(
  42. self,
  43. db: Session,
  44. user_id: str,
  45. openid: str,
  46. updater: Optional[dict] = None,
  47. ) -> Optional[str]:
  48. """若用户未绑定 wx_mini_openid 则写入。返回 None 表示成功或无需更新,否则为错误说明。"""
  49. openid = (openid or "").strip()
  50. if not openid:
  51. return "openid 不能为空"
  52. user = self.get(db, user_id)
  53. if not user:
  54. return "用户不存在"
  55. existing = (getattr(user, "wx_mini_openid", None) or "").strip()
  56. if existing:
  57. return None
  58. bound = self.get_by_wx_mini_openid(db, openid)
  59. if bound and bound.id != user_id:
  60. return "该微信已绑定其他账号"
  61. self.update_with_history(db, user_id, {"wx_mini_openid": openid}, updater)
  62. return None
  63. def get_multi_by_role(self, db: Session, role: str, skip: int = 0, limit: int = 100) -> List[SysUser]:
  64. """根据角色获取用户列表"""
  65. return db.query(SysUser).filter(
  66. and_(SysUser.role == role, SysUser.is_del == 0)
  67. ).offset(skip).limit(limit).all()
  68. def get_multi_by_region(self, db: Session, region_id: str, skip: int = 0, limit: int = 100) -> List[SysUser]:
  69. """根据区域获取用户列表"""
  70. return db.query(SysUser).filter(
  71. and_(SysUser.region_id == region_id, SysUser.is_del == 0)
  72. ).offset(skip).limit(limit).all()
  73. def get_multi_with_filters(
  74. self,
  75. db: Session,
  76. role: str = None,
  77. roles: Optional[List[str]] = None,
  78. region_id: str = None,
  79. keyword: str = None,
  80. is_active: bool = None,
  81. skip: int = 0,
  82. limit: int = 10
  83. ) -> tuple[List[SysUser], int]:
  84. """多条件查询用户"""
  85. query = db.query(SysUser).filter(SysUser.is_del == 0)
  86. if role:
  87. query = query.filter(SysUser.role == role)
  88. elif roles:
  89. role_values = [str(x).strip() for x in roles if str(x).strip()]
  90. if role_values:
  91. query = query.filter(SysUser.role.in_(role_values))
  92. if region_id:
  93. query = query.filter(SysUser.region_id == region_id)
  94. if is_active is not None:
  95. query = query.filter(SysUser.is_active == is_active)
  96. if keyword:
  97. query = query.filter(
  98. (SysUser.username.like(f"%{keyword}%")) |
  99. (SysUser.real_name.like(f"%{keyword}%")) |
  100. (SysUser.phone.like(f"%{keyword}%"))
  101. )
  102. total = query.count()
  103. users = query.order_by(SysUser.create_at.desc()).offset(skip).limit(limit).all()
  104. return users, total
  105. def create_with_password(self, db: Session, obj_in: dict, creator: dict = None) -> Tuple[SysUser, Optional[str]]:
  106. """创建用户。
  107. 若请求体包含非空 password(运营端表单),则使用该密码写入 password_hash;
  108. 否则生成随机密码,并在返回值中给出明文 initialPassword(仅此种情况返回,供管理员告知用户)。
  109. """
  110. obj_in = dict(obj_in)
  111. raw_pwd = obj_in.pop("password", None)
  112. plain: Optional[str] = None
  113. if isinstance(raw_pwd, str):
  114. plain = raw_pwd.strip()
  115. if plain:
  116. if len(plain) < 6:
  117. raise ValueError("密码至少6位")
  118. obj_in["password_hash"] = get_password_hash(plain)
  119. initial_reveal: Optional[str] = None
  120. else:
  121. initial_reveal = generate_random_password()
  122. obj_in["password_hash"] = get_password_hash(initial_reveal)
  123. obj_in["is_active"] = obj_in.get("is_active", True)
  124. user = self.create(db, obj_in, creator)
  125. return user, initial_reveal
  126. def reset_password_to_admin_default(self, db: Session, id: str) -> bool:
  127. """将密码重置为服务端固定默认(DEFAULT_ADMIN_RESET_PLAIN_PASSWORD),不由请求传入。"""
  128. user = self.get(db, id)
  129. if not user:
  130. return False
  131. plain = DEFAULT_ADMIN_RESET_PLAIN_PASSWORD
  132. if len(plain) < 6:
  133. raise ValueError("默认重置密码配置无效:至少6位")
  134. user.password_hash = get_password_hash(plain)
  135. db.commit()
  136. return True
  137. def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[SysUser]:
  138. """更新用户并记录历史"""
  139. original = self.get(db, id)
  140. if not original:
  141. return None
  142. his_data = {
  143. "id": original.id,
  144. "username": original.username,
  145. "password_hash": original.password_hash,
  146. "real_name": original.real_name,
  147. "phone": original.phone,
  148. "avatar_url": getattr(original, "avatar_url", None),
  149. "wx_mini_openid": getattr(original, "wx_mini_openid", None),
  150. "role": original.role,
  151. "region_id": original.region_id,
  152. "is_active": original.is_active,
  153. "create_at": original.create_at,
  154. "create_by": original.create_by,
  155. "create_name": original.create_name,
  156. "update_at": original.update_at,
  157. "update_by": original.update_by,
  158. }
  159. his = SysUserHis(**his_data)
  160. db.add(his)
  161. return self.update(db, id, obj_in, updater)
  162. def soft_delete_with_history(self, db: Session, id: str) -> bool:
  163. """删除用户并记录历史;同时释放用户名唯一索引,便于后续重建同名账号。"""
  164. original = self.get(db, id)
  165. if not original:
  166. return False
  167. his_data = {
  168. "id": original.id,
  169. "username": original.username,
  170. "password_hash": original.password_hash,
  171. "real_name": original.real_name,
  172. "phone": original.phone,
  173. "avatar_url": getattr(original, "avatar_url", None),
  174. "wx_mini_openid": getattr(original, "wx_mini_openid", None),
  175. "role": original.role,
  176. "region_id": original.region_id,
  177. "is_active": original.is_active,
  178. "create_at": original.create_at,
  179. "create_by": original.create_by,
  180. "create_name": original.create_name,
  181. "update_at": original.update_at,
  182. "update_by": original.update_by,
  183. }
  184. his = SysUserHis(**his_data)
  185. db.add(his)
  186. # 历史表已保留原用户名;主表改写以释放 uk_username
  187. original.username = self._release_username(original.username, original.id)
  188. original.is_del = 1
  189. db.commit()
  190. return True
  191. user_crud = UserCRUD(SysUser)