user.py 12 KB

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