user.py 13 KB

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