user.py 15 KB

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