user.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. """用户 CRUD(sys_user + sys_user_role + sys_user_post)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. import bcrypt
  7. from sqlalchemy import and_, func, or_
  8. from sqlalchemy.orm import Session
  9. from app.core.config import settings
  10. from app.core.delete_guard import assert_row_deletable_by_creator
  11. from app.core.utils import generate_uuid
  12. from app.crud.base import CRUDBase
  13. from app.models.models import SysUser, SysUserRole, SysUserPost, SysDept
  14. def _hash_password(password: str) -> str:
  15. return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
  16. class _UserCRUDCompat(CRUDBase[SysUser]):
  17. def get_by_user_no(self, db: Session, user_no: str, tenant_no: Optional[str] = None) -> Optional[SysUser]:
  18. tenant = tenant_no or settings.DEFAULT_TENANT_NO
  19. un = (user_no or "").strip()
  20. if not un:
  21. return None
  22. return (
  23. db.query(SysUser)
  24. .filter(and_(SysUser.user_no == un, SysUser.tenant_no == tenant, SysUser.del_flag == 0))
  25. .first()
  26. )
  27. def get_by_user_no_for_web_login(self, db: Session, user_no: str) -> Optional[SysUser]:
  28. """Web 登录:优先 DEFAULT_TENANT_NO;未命中时按 user_no 回退(兼容未执行租户迁移的库)。"""
  29. user = self.get_by_user_no(db, user_no, settings.DEFAULT_TENANT_NO)
  30. if user:
  31. return user
  32. un = (user_no or "").strip()
  33. if not un:
  34. return None
  35. return (
  36. db.query(SysUser)
  37. .filter(and_(SysUser.user_no == un, SysUser.del_flag == 0))
  38. .order_by(SysUser.create_time.desc())
  39. .first()
  40. )
  41. user_crud = _UserCRUDCompat(SysUser)
  42. def _row_to_dict(u: SysUser, dept_name: str = "") -> Dict[str, Any]:
  43. return {
  44. "id": u.id,
  45. "deptId": u.dept_id or "",
  46. "userNo": u.user_no,
  47. "nickName": u.nick_name,
  48. "email": u.email or "",
  49. "phoneNo": u.phone_no or "",
  50. "sex": u.sex or "0",
  51. "avatar": u.avatar or "",
  52. "status": u.status,
  53. "tenantNo": u.tenant_no,
  54. "orderNum": u.order_num if u.order_num is not None else 0,
  55. "remark": u.remark or "",
  56. "createBy": u.create_by or "",
  57. "createTime": u.create_time.strftime("%Y-%m-%d %H:%M:%S") if u.create_time else "",
  58. "updateTime": u.update_time.strftime("%Y-%m-%d %H:%M:%S") if u.update_time else "",
  59. "deptName": dept_name,
  60. }
  61. def list_users(
  62. db: Session,
  63. *,
  64. page: int = 1,
  65. page_size: int = 10,
  66. keyword: Optional[str] = None,
  67. dept_id: Optional[str] = None,
  68. tenant_no: Optional[str] = None,
  69. status: Optional[str] = None,
  70. ) -> Tuple[List[Dict[str, Any]], int]:
  71. def _filtered():
  72. q0 = db.query(SysUser).filter(SysUser.del_flag == 0)
  73. if tenant_no:
  74. q0 = q0.filter(SysUser.tenant_no == tenant_no)
  75. if dept_id:
  76. q0 = q0.filter(SysUser.dept_id == dept_id)
  77. if status:
  78. q0 = q0.filter(SysUser.status == status)
  79. kw = (keyword or "").strip()
  80. if kw:
  81. like = f"%{kw}%"
  82. q0 = q0.filter(
  83. or_(
  84. SysUser.user_no.like(like),
  85. SysUser.nick_name.like(like),
  86. SysUser.phone_no.like(like),
  87. )
  88. )
  89. return q0
  90. total = _filtered().order_by(None).with_entities(func.count(SysUser.id)).scalar() or 0
  91. page = max(1, page)
  92. page_size = min(max(1, page_size), 100)
  93. rows = (
  94. _filtered()
  95. .order_by(SysUser.create_time.desc())
  96. .offset((page - 1) * page_size)
  97. .limit(page_size)
  98. .all()
  99. )
  100. # batch resolve dept names
  101. dept_ids = list({r.dept_id for r in rows if r.dept_id})
  102. dept_map: Dict[str, str] = {}
  103. if dept_ids:
  104. dept_rows = db.query(SysDept).filter(SysDept.id.in_(dept_ids)).all()
  105. dept_map = {d.id: d.dept_name for d in dept_rows}
  106. return [_row_to_dict(r, dept_name=dept_map.get(r.dept_id or "", "")) for r in rows], int(total)
  107. def get_user(db: Session, user_id: str) -> Optional[SysUser]:
  108. return (
  109. db.query(SysUser)
  110. .filter(and_(SysUser.id == user_id, SysUser.del_flag == 0))
  111. .first()
  112. )
  113. def list_post_ids_for_user(db: Session, user_id: str) -> List[str]:
  114. """用户当前有效职位 ID 列表。"""
  115. return [
  116. r.post_id
  117. for r in db.query(SysUserPost)
  118. .filter(and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0))
  119. .all()
  120. ]
  121. def set_user_post_ids_only(
  122. db: Session,
  123. user_id: str,
  124. tenant_no: str,
  125. post_ids: List[str],
  126. updater_id: Optional[str],
  127. ) -> None:
  128. """仅替换 sys_user_post,不影响 sys_user_role。"""
  129. now = datetime.now()
  130. uniq = [p for p in dict.fromkeys(post_ids or []) if p]
  131. db.query(SysUserPost).filter(
  132. and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
  133. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  134. for pid in uniq:
  135. db.add(
  136. SysUserPost(
  137. id=generate_uuid(),
  138. user_id=user_id,
  139. post_id=pid,
  140. tenant_no=tenant_no,
  141. order_num=0,
  142. remark=None,
  143. create_by=updater_id,
  144. create_dept_id="",
  145. create_time=now,
  146. update_by=updater_id,
  147. update_time=now,
  148. del_flag=0,
  149. )
  150. )
  151. def list_user_ids_bound_to_post(db: Session, post_id: str, tenant_no: str) -> List[str]:
  152. rows = (
  153. db.query(SysUserPost.user_id)
  154. .filter(
  155. and_(
  156. SysUserPost.post_id == post_id,
  157. SysUserPost.tenant_no == tenant_no,
  158. SysUserPost.del_flag == 0,
  159. )
  160. )
  161. .all()
  162. )
  163. return [r[0] for r in rows]
  164. def sync_post_user_bindings(
  165. db: Session,
  166. post_id: str,
  167. tenant_no: str,
  168. user_ids: List[str],
  169. updater_id: Optional[str],
  170. ) -> None:
  171. """将本职位绑定的用户集合设为 user_ids(只增删本职位,保留用户其它职位与角色)。"""
  172. ordered = list(dict.fromkeys([uid for uid in (user_ids or []) if uid]))
  173. for uid in ordered:
  174. u = get_user(db, uid)
  175. if not u or u.tenant_no != tenant_no:
  176. raise ValueError("存在无效用户或用户不属于当前租户")
  177. current = set(list_user_ids_bound_to_post(db, post_id, tenant_no))
  178. target = set(ordered)
  179. affected = current | target
  180. for uid in affected:
  181. u = get_user(db, uid)
  182. if not u or u.tenant_no != tenant_no:
  183. continue
  184. existing = list_post_ids_for_user(db, uid)
  185. without = [p for p in existing if p != post_id]
  186. new_posts = without + [post_id] if uid in target else without
  187. set_user_post_ids_only(db, uid, tenant_no, new_posts, updater_id)
  188. db.commit()
  189. def get_user_with_relations(db: Session, user_id: str) -> Optional[Dict[str, Any]]:
  190. u = get_user(db, user_id)
  191. if not u:
  192. return None
  193. dept_name = ""
  194. if u.dept_id:
  195. dept = db.query(SysDept).filter(SysDept.id == u.dept_id).first()
  196. if dept:
  197. dept_name = dept.dept_name
  198. d = _row_to_dict(u, dept_name=dept_name)
  199. role_ids = [
  200. r.role_id
  201. for r in db.query(SysUserRole)
  202. .filter(and_(SysUserRole.user_id == user_id, SysUserRole.del_flag == 0))
  203. .all()
  204. ]
  205. post_ids = list_post_ids_for_user(db, user_id)
  206. d["roleIds"] = role_ids
  207. d["postIds"] = post_ids
  208. return d
  209. def user_no_exists(db: Session, user_no: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
  210. q = db.query(SysUser).filter(
  211. and_(SysUser.user_no == user_no.strip(), SysUser.tenant_no == tenant_no, SysUser.del_flag == 0)
  212. )
  213. if exclude_id:
  214. q = q.filter(SysUser.id != exclude_id)
  215. return q.first() is not None
  216. def _replace_associations(
  217. db: Session,
  218. user_id: str,
  219. tenant_no: str,
  220. creator_id: Optional[str],
  221. role_ids: Optional[List[str]] = None,
  222. post_ids: Optional[List[str]] = None,
  223. ) -> None:
  224. now = datetime.now()
  225. db.query(SysUserRole).filter(
  226. and_(SysUserRole.user_id == user_id, SysUserRole.del_flag == 0)
  227. ).update({"del_flag": 1, "update_time": now})
  228. db.query(SysUserPost).filter(
  229. and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
  230. ).update({"del_flag": 1, "update_time": now})
  231. if role_ids:
  232. for rid in role_ids:
  233. db.add(SysUserRole(
  234. id=generate_uuid(),
  235. user_id=user_id,
  236. role_id=rid,
  237. tenant_no=tenant_no,
  238. order_num=0,
  239. remark=None,
  240. create_by=creator_id,
  241. create_dept_id="",
  242. create_time=now,
  243. update_by=creator_id,
  244. update_time=now,
  245. del_flag=0,
  246. ))
  247. if post_ids:
  248. for pid in post_ids:
  249. db.add(SysUserPost(
  250. id=generate_uuid(),
  251. user_id=user_id,
  252. post_id=pid,
  253. tenant_no=tenant_no,
  254. order_num=0,
  255. remark=None,
  256. create_by=creator_id,
  257. create_dept_id="",
  258. create_time=now,
  259. update_by=creator_id,
  260. update_time=now,
  261. del_flag=0,
  262. ))
  263. def create_user(
  264. db: Session,
  265. *,
  266. user_no: str,
  267. nick_name: str,
  268. password: str,
  269. dept_id: Optional[str] = None,
  270. email: str = "",
  271. phone_no: str = "",
  272. sex: str = "0",
  273. avatar: Optional[str] = None,
  274. status: str = "1",
  275. tenant_no: str = "shanxia2.0",
  276. order_num: Optional[int] = None,
  277. remark: Optional[str] = None,
  278. user_id: Optional[str] = None,
  279. role_ids: Optional[List[str]] = None,
  280. post_ids: Optional[List[str]] = None,
  281. ) -> SysUser:
  282. un = (user_no or "").strip()
  283. if not un:
  284. raise ValueError("用户账号不能为空")
  285. nn = (nick_name or "").strip()
  286. if not nn:
  287. raise ValueError("用户名不能为空")
  288. pw = (password or "").strip()
  289. if not pw:
  290. raise ValueError("密码不能为空")
  291. obj = SysUser(
  292. id=generate_uuid(),
  293. dept_id=dept_id or None,
  294. user_no=un,
  295. nick_name=nn,
  296. email=(email or "").strip(),
  297. phone_no=(phone_no or "").strip(),
  298. sex=sex or "0",
  299. avatar=avatar,
  300. password=_hash_password(pw),
  301. status=status or "1",
  302. tenant_no=tenant_no,
  303. order_num=order_num if order_num is not None else 0,
  304. remark=(remark or "").strip() or None,
  305. create_by=user_id,
  306. create_dept_id="",
  307. create_time=datetime.now(),
  308. update_by=user_id,
  309. update_time=datetime.now(),
  310. del_flag=0,
  311. )
  312. db.add(obj)
  313. db.commit()
  314. db.refresh(obj)
  315. _replace_associations(db, obj.id, tenant_no, user_id, role_ids, post_ids)
  316. db.commit()
  317. return obj
  318. def update_user(
  319. db: Session,
  320. user_id: str,
  321. *,
  322. nick_name: Optional[str] = None,
  323. dept_id: Optional[str] = None,
  324. email: Optional[str] = None,
  325. phone_no: Optional[str] = None,
  326. sex: Optional[str] = None,
  327. avatar: Optional[str] = None,
  328. status: Optional[str] = None,
  329. order_num: Optional[int] = None,
  330. remark: Optional[str] = None,
  331. user_id_updater: Optional[str] = None,
  332. role_ids: Optional[List[str]] = None,
  333. post_ids: Optional[List[str]] = None,
  334. ) -> SysUser:
  335. obj = get_user(db, user_id)
  336. if not obj:
  337. raise ValueError("用户不存在")
  338. if nick_name is not None:
  339. obj.nick_name = (nick_name or "").strip() or obj.nick_name
  340. if dept_id is not None:
  341. obj.dept_id = dept_id or None
  342. if email is not None:
  343. obj.email = (email or "").strip()
  344. if phone_no is not None:
  345. obj.phone_no = (phone_no or "").strip()
  346. if sex is not None:
  347. obj.sex = sex
  348. if avatar is not None:
  349. obj.avatar = avatar or None
  350. if status is not None:
  351. obj.status = status
  352. if order_num is not None:
  353. obj.order_num = order_num
  354. if remark is not None:
  355. obj.remark = (remark or "").strip() or None
  356. obj.update_by = user_id_updater
  357. obj.update_time = datetime.now()
  358. db.add(obj)
  359. if role_ids is not None or post_ids is not None:
  360. _replace_associations(
  361. db, user_id, obj.tenant_no, user_id_updater,
  362. role_ids=role_ids, post_ids=post_ids,
  363. )
  364. db.commit()
  365. db.refresh(obj)
  366. return obj
  367. def soft_delete_user(db: Session, user_id: str, *, user_info: dict = None) -> None:
  368. obj = get_user(db, user_id)
  369. if not obj:
  370. raise ValueError("用户不存在")
  371. assert_row_deletable_by_creator(obj, user_info)
  372. obj.del_flag = 1
  373. obj.update_time = datetime.now()
  374. db.add(obj)
  375. db.commit()
  376. def change_password(db: Session, user_id: str, old_password: str, new_password: str) -> None:
  377. from app.core.security import verify_password
  378. obj = get_user(db, user_id)
  379. if not obj:
  380. raise ValueError("用户不存在")
  381. pwd_hash = getattr(obj, "password", None) or ""
  382. if not verify_password(old_password, pwd_hash):
  383. raise ValueError("原密码不正确")
  384. new_pw = (new_password or "").strip()
  385. if len(new_pw) < 6:
  386. raise ValueError("新密码长度不能少于6位")
  387. if old_password == new_pw:
  388. raise ValueError("新密码不能与原密码相同")
  389. obj.password = _hash_password(new_pw)
  390. obj.update_time = datetime.now()
  391. db.add(obj)
  392. db.commit()
  393. def reset_password(db: Session, user_id: str, new_password: str) -> None:
  394. obj = get_user(db, user_id)
  395. if not obj:
  396. raise ValueError("用户不存在")
  397. obj.password = _hash_password(new_password)
  398. obj.update_time = datetime.now()
  399. db.add(obj)
  400. db.commit()
  401. def total_pages(total: int, page_size: int) -> int:
  402. if total <= 0:
  403. return 0
  404. return int(math.ceil(total / float(page_size)))