user.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. """用户 CRUD(sys_user + sys_user_role + sys_user_post)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import date, datetime
  5. from typing import Any, Dict, List, Optional, Tuple, Union
  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. def get_by_wx_openid(self, db: Session, wx_openid: str) -> Optional[SysUser]:
  27. oid = (wx_openid or "").strip()
  28. if not oid:
  29. return None
  30. return (
  31. db.query(SysUser)
  32. .filter(SysUser.wx_openid == oid, SysUser.del_flag == 0)
  33. .first()
  34. )
  35. def get_by_wx_mp_openid(self, db: Session, wx_mp_openid: str) -> Optional[SysUser]:
  36. oid = (wx_mp_openid or "").strip()
  37. if not oid:
  38. return None
  39. return (
  40. db.query(SysUser)
  41. .filter(SysUser.wx_mp_openid == oid, SysUser.del_flag == 0)
  42. .first()
  43. )
  44. def list_active_by_phone_no(self, db: Session, phone_no: str) -> List[SysUser]:
  45. phone = (phone_no or "").strip()
  46. if not phone:
  47. return []
  48. return (
  49. db.query(SysUser)
  50. .filter(
  51. SysUser.phone_no == phone,
  52. SysUser.del_flag == 0,
  53. SysUser.status == "1",
  54. )
  55. .all()
  56. )
  57. user_crud = _UserCRUDCompat(SysUser)
  58. def _row_to_dict(u: SysUser, dept_name: str = "") -> Dict[str, Any]:
  59. return {
  60. "id": u.id,
  61. "deptId": u.dept_id or "",
  62. "supervisorUserId": getattr(u, "supervisor_user_id", None) or "",
  63. "userNo": u.user_no,
  64. "nickName": u.nick_name,
  65. "email": u.email or "",
  66. "phoneNo": u.phone_no or "",
  67. "hireDate": u.hire_date.strftime("%Y-%m-%d") if getattr(u, "hire_date", None) else "",
  68. "probationMonths": (
  69. u.probation_months if getattr(u, "probation_months", None) is not None else None
  70. ),
  71. "lastWorkDay": u.last_work_day.strftime("%Y-%m-%d")
  72. if getattr(u, "last_work_day", None)
  73. else "",
  74. "sex": u.sex or "0",
  75. "avatar": u.avatar or "",
  76. "status": u.status,
  77. "tenantNo": u.tenant_no,
  78. "orderNum": u.order_num if u.order_num is not None else 0,
  79. "remark": u.remark or "",
  80. "createTime": u.create_time.strftime("%Y-%m-%d %H:%M:%S") if u.create_time else "",
  81. "updateTime": u.update_time.strftime("%Y-%m-%d %H:%M:%S") if u.update_time else "",
  82. "deptName": dept_name,
  83. }
  84. def list_users(
  85. db: Session,
  86. *,
  87. page: int = 1,
  88. page_size: int = 10,
  89. keyword: Optional[str] = None,
  90. dept_id: Optional[str] = None,
  91. tenant_no: Optional[str] = None,
  92. status: Optional[str] = None,
  93. ) -> Tuple[List[Dict[str, Any]], int]:
  94. def _filtered():
  95. q0 = db.query(SysUser).filter(SysUser.del_flag == 0)
  96. if tenant_no:
  97. q0 = q0.filter(SysUser.tenant_no == tenant_no)
  98. if dept_id:
  99. q0 = q0.filter(SysUser.dept_id == dept_id)
  100. if status:
  101. q0 = q0.filter(SysUser.status == status)
  102. kw = (keyword or "").strip()
  103. if kw:
  104. like = f"%{kw}%"
  105. q0 = q0.filter(
  106. or_(
  107. SysUser.user_no.like(like),
  108. SysUser.nick_name.like(like),
  109. SysUser.phone_no.like(like),
  110. )
  111. )
  112. return q0
  113. total = _filtered().order_by(None).with_entities(func.count(SysUser.id)).scalar() or 0
  114. page = max(1, page)
  115. page_size = min(max(1, page_size), 100)
  116. rows = (
  117. _filtered()
  118. .order_by(SysUser.create_time.desc())
  119. .offset((page - 1) * page_size)
  120. .limit(page_size)
  121. .all()
  122. )
  123. # batch resolve dept names
  124. dept_ids = list({r.dept_id for r in rows if r.dept_id})
  125. dept_map: Dict[str, str] = {}
  126. if dept_ids:
  127. dept_rows = db.query(SysDept).filter(SysDept.id.in_(dept_ids)).all()
  128. dept_map = {d.id: d.dept_name for d in dept_rows}
  129. return [_row_to_dict(r, dept_name=dept_map.get(r.dept_id or "", "")) for r in rows], int(total)
  130. def get_user(db: Session, user_id: str) -> Optional[SysUser]:
  131. return (
  132. db.query(SysUser)
  133. .filter(and_(SysUser.id == user_id, SysUser.del_flag == 0))
  134. .first()
  135. )
  136. def list_post_ids_for_user(
  137. db: Session, user_id: str, tenant_no: Optional[str] = None
  138. ) -> List[str]:
  139. """用户当前有效职位 ID 列表。"""
  140. query = db.query(SysUserPost).filter(
  141. and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
  142. )
  143. tenant = (tenant_no or "").strip()
  144. if tenant:
  145. query = query.filter(SysUserPost.tenant_no == tenant)
  146. return [
  147. r.post_id
  148. for r in query.all()
  149. ]
  150. def set_user_post_ids_only(
  151. db: Session,
  152. user_id: str,
  153. tenant_no: str,
  154. post_ids: List[str],
  155. updater_id: Optional[str],
  156. ) -> None:
  157. """仅替换 sys_user_post,不影响 sys_user_role。"""
  158. now = datetime.now()
  159. uniq = [p for p in dict.fromkeys(post_ids or []) if p]
  160. db.query(SysUserPost).filter(
  161. and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
  162. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  163. for pid in uniq:
  164. db.add(
  165. SysUserPost(
  166. id=generate_uuid(),
  167. user_id=user_id,
  168. post_id=pid,
  169. tenant_no=tenant_no,
  170. order_num=0,
  171. remark=None,
  172. create_by=updater_id,
  173. create_dept_id="",
  174. create_time=now,
  175. update_by=updater_id,
  176. update_time=now,
  177. del_flag=0,
  178. )
  179. )
  180. def list_user_ids_bound_to_post(db: Session, post_id: str, tenant_no: str) -> List[str]:
  181. rows = (
  182. db.query(SysUserPost.user_id)
  183. .join(
  184. SysUser,
  185. and_(
  186. SysUser.id == SysUserPost.user_id,
  187. SysUser.del_flag == 0,
  188. ),
  189. )
  190. .filter(
  191. and_(
  192. SysUserPost.post_id == post_id,
  193. SysUserPost.tenant_no == tenant_no,
  194. SysUserPost.del_flag == 0,
  195. )
  196. )
  197. .all()
  198. )
  199. return [r[0] for r in rows]
  200. def list_user_ids_bound_to_posts(
  201. db: Session, post_ids: List[str], tenant_no: str
  202. ) -> List[str]:
  203. """多职位展开启用用户 ID(仅启用职位;用户启用过滤留给调用方或本函数后处理)。
  204. 返回保序去重的 user_id 列表。仅包含 status=1 的职位下的关联。
  205. """
  206. from app.models.models import SysPost
  207. ordered = list(dict.fromkeys([pid for pid in (post_ids or []) if pid]))
  208. if not ordered:
  209. return []
  210. rows = (
  211. db.query(SysUserPost.user_id)
  212. .join(
  213. SysPost,
  214. and_(
  215. SysPost.id == SysUserPost.post_id,
  216. SysPost.del_flag == 0,
  217. SysPost.status == "1",
  218. SysPost.tenant_no == tenant_no,
  219. ),
  220. )
  221. .filter(
  222. and_(
  223. SysUserPost.post_id.in_(ordered),
  224. SysUserPost.tenant_no == tenant_no,
  225. SysUserPost.del_flag == 0,
  226. )
  227. )
  228. .all()
  229. )
  230. out: List[str] = []
  231. seen = set()
  232. for r in rows:
  233. uid = r[0]
  234. if uid and uid not in seen:
  235. seen.add(uid)
  236. out.append(uid)
  237. return out
  238. def sync_post_user_bindings(
  239. db: Session,
  240. post_id: str,
  241. tenant_no: str,
  242. user_ids: List[str],
  243. updater_id: Optional[str],
  244. ) -> None:
  245. """将本职位绑定的用户集合设为 user_ids(只增删本职位,保留用户其它职位与角色)。"""
  246. ordered = list(dict.fromkeys([uid for uid in (user_ids or []) if uid]))
  247. for uid in ordered:
  248. u = get_user(db, uid)
  249. if not u or u.tenant_no != tenant_no:
  250. raise ValueError("存在无效用户或用户不属于当前租户")
  251. current = set(list_user_ids_bound_to_post(db, post_id, tenant_no))
  252. target = set(ordered)
  253. affected = current | target
  254. for uid in affected:
  255. u = get_user(db, uid)
  256. if not u or u.tenant_no != tenant_no:
  257. continue
  258. existing = list_post_ids_for_user(db, uid)
  259. without = [p for p in existing if p != post_id]
  260. new_posts = without + [post_id] if uid in target else without
  261. set_user_post_ids_only(db, uid, tenant_no, new_posts, updater_id)
  262. db.commit()
  263. def _empty_user_relation_fields(d: Dict[str, Any]) -> None:
  264. d["roleIds"] = []
  265. d["postIds"] = []
  266. d["postInheritedRoleIds"] = []
  267. d["effectiveRoleIds"] = []
  268. d["effectiveRoleNames"] = []
  269. def _attach_user_relations(db: Session, d: Dict[str, Any], user_id: str) -> None:
  270. """为单个用户 dict 补全职位/角色关联字段(与详情接口字段一致)。"""
  271. if not d.get("id"):
  272. d["id"] = user_id
  273. enrich_user_dicts_with_relations(db, [d])
  274. def enrich_user_dicts_with_relations(
  275. db: Session, items: List[Dict[str, Any]]
  276. ) -> List[Dict[str, Any]]:
  277. """批量就地补全列表项的职位/角色关联字段,供列表列展示。
  278. 使用固定次数的 IN 查询,避免按用户 N+1(远程库 RTT 下可达数秒)。
  279. """
  280. if not items:
  281. return items
  282. from app.core.config import settings
  283. from app.models.models import SysPost, SysPostRole, SysRole
  284. tenant = settings.DEFAULT_TENANT_NO
  285. user_ids: List[str] = []
  286. seen_uids: set = set()
  287. for d in items:
  288. uid = str(d.get("id") or "").strip()
  289. if not uid:
  290. _empty_user_relation_fields(d)
  291. continue
  292. if uid not in seen_uids:
  293. seen_uids.add(uid)
  294. user_ids.append(uid)
  295. if not user_ids:
  296. return items
  297. role_ids_by_user: Dict[str, List[str]] = {uid: [] for uid in user_ids}
  298. for row in (
  299. db.query(SysUserRole.user_id, SysUserRole.role_id)
  300. .filter(SysUserRole.user_id.in_(user_ids), SysUserRole.del_flag == 0)
  301. .all()
  302. ):
  303. uid, rid = str(row[0] or ""), str(row[1] or "")
  304. if uid in role_ids_by_user and rid:
  305. role_ids_by_user[uid].append(rid)
  306. post_ids_by_user: Dict[str, List[str]] = {uid: [] for uid in user_ids}
  307. all_post_ids: List[str] = []
  308. seen_posts: set = set()
  309. for row in (
  310. db.query(SysUserPost.user_id, SysUserPost.post_id)
  311. .filter(SysUserPost.user_id.in_(user_ids), SysUserPost.del_flag == 0)
  312. .all()
  313. ):
  314. uid, pid = str(row[0] or ""), str(row[1] or "")
  315. if uid in post_ids_by_user and pid:
  316. post_ids_by_user[uid].append(pid)
  317. if pid not in seen_posts:
  318. seen_posts.add(pid)
  319. all_post_ids.append(pid)
  320. enabled_post_ids: set = set()
  321. if all_post_ids:
  322. enabled_post_ids = {
  323. str(r[0])
  324. for r in db.query(SysPost.id)
  325. .filter(
  326. SysPost.id.in_(all_post_ids),
  327. SysPost.del_flag == 0,
  328. SysPost.status == "1",
  329. SysPost.tenant_no == tenant,
  330. )
  331. .all()
  332. if r[0]
  333. }
  334. post_role_ids: Dict[str, List[str]] = {pid: [] for pid in enabled_post_ids}
  335. if enabled_post_ids:
  336. for row in (
  337. db.query(SysPostRole.post_id, SysRole.id)
  338. .join(SysRole, SysRole.id == SysPostRole.role_id)
  339. .filter(
  340. SysPostRole.post_id.in_(list(enabled_post_ids)),
  341. SysPostRole.del_flag == 0,
  342. SysPostRole.tenant_no == tenant,
  343. SysRole.del_flag == 0,
  344. SysRole.status == "1",
  345. SysRole.tenant_no == tenant,
  346. )
  347. .order_by(SysRole.create_time.desc())
  348. .all()
  349. ):
  350. pid, rid = str(row[0] or ""), str(row[1] or "")
  351. if pid in post_role_ids and rid and rid not in post_role_ids[pid]:
  352. post_role_ids[pid].append(rid)
  353. enabled_direct_by_user: Dict[str, List[str]] = {uid: [] for uid in user_ids}
  354. for row in (
  355. db.query(SysUserRole.user_id, SysRole.id)
  356. .join(SysRole, SysRole.id == SysUserRole.role_id)
  357. .filter(
  358. SysUserRole.user_id.in_(user_ids),
  359. SysUserRole.del_flag == 0,
  360. SysUserRole.tenant_no == tenant,
  361. SysRole.del_flag == 0,
  362. SysRole.status == "1",
  363. SysRole.tenant_no == tenant,
  364. )
  365. .order_by(SysRole.create_time.desc())
  366. .all()
  367. ):
  368. uid, rid = str(row[0] or ""), str(row[1] or "")
  369. if uid in enabled_direct_by_user and rid and rid not in enabled_direct_by_user[uid]:
  370. enabled_direct_by_user[uid].append(rid)
  371. inherited_by_user: Dict[str, List[str]] = {}
  372. effective_by_user: Dict[str, List[str]] = {}
  373. all_effective: List[str] = []
  374. seen_eff: set = set()
  375. for uid in user_ids:
  376. post_inherited: List[str] = []
  377. seen_inh: set = set()
  378. for pid in post_ids_by_user.get(uid, []):
  379. if pid not in enabled_post_ids:
  380. continue
  381. for rid in post_role_ids.get(pid, []):
  382. if rid not in seen_inh:
  383. seen_inh.add(rid)
  384. post_inherited.append(rid)
  385. inherited_by_user[uid] = post_inherited
  386. effective: List[str] = []
  387. seen_e: set = set()
  388. for rid in enabled_direct_by_user.get(uid, []) + post_inherited:
  389. if rid not in seen_e:
  390. seen_e.add(rid)
  391. effective.append(rid)
  392. effective_by_user[uid] = effective
  393. for rid in effective:
  394. if rid not in seen_eff:
  395. seen_eff.add(rid)
  396. all_effective.append(rid)
  397. name_by_id: Dict[str, str] = {}
  398. if all_effective:
  399. for rid, name in (
  400. db.query(SysRole.id, SysRole.role_name)
  401. .filter(
  402. SysRole.id.in_(all_effective),
  403. SysRole.del_flag == 0,
  404. SysRole.status == "1",
  405. SysRole.tenant_no == tenant,
  406. )
  407. .all()
  408. ):
  409. if rid and name:
  410. name_by_id[str(rid).strip()] = str(name).strip()
  411. for d in items:
  412. uid = str(d.get("id") or "").strip()
  413. if not uid:
  414. continue
  415. effective = effective_by_user.get(uid, [])
  416. d["roleIds"] = role_ids_by_user.get(uid, [])
  417. d["postIds"] = post_ids_by_user.get(uid, [])
  418. d["postInheritedRoleIds"] = inherited_by_user.get(uid, [])
  419. d["effectiveRoleIds"] = effective
  420. names: List[str] = []
  421. seen_n: set = set()
  422. for rid in effective:
  423. name = name_by_id.get(rid)
  424. if name and name not in seen_n:
  425. seen_n.add(name)
  426. names.append(name)
  427. d["effectiveRoleNames"] = names
  428. return items
  429. def get_user_with_relations(db: Session, user_id: str) -> Optional[Dict[str, Any]]:
  430. u = get_user(db, user_id)
  431. if not u:
  432. return None
  433. dept_name = ""
  434. if u.dept_id:
  435. dept = db.query(SysDept).filter(SysDept.id == u.dept_id).first()
  436. if dept:
  437. dept_name = dept.dept_name
  438. d = _row_to_dict(u, dept_name=dept_name)
  439. _attach_user_relations(db, d, user_id)
  440. return d
  441. def user_no_exists(db: Session, user_no: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
  442. q = db.query(SysUser).filter(
  443. and_(SysUser.user_no == user_no.strip(), SysUser.tenant_no == tenant_no, SysUser.del_flag == 0)
  444. )
  445. if exclude_id:
  446. q = q.filter(SysUser.id != exclude_id)
  447. return q.first() is not None
  448. def _replace_associations(
  449. db: Session,
  450. user_id: str,
  451. tenant_no: str,
  452. creator_id: Optional[str],
  453. role_ids: Optional[List[str]] = None,
  454. post_ids: Optional[List[str]] = None,
  455. ) -> None:
  456. """全量替换关联。仅当对应参数不为 None 时才替换该侧(避免只改职位时清空直接角色)。"""
  457. now = datetime.now()
  458. if role_ids is not None:
  459. db.query(SysUserRole).filter(
  460. and_(SysUserRole.user_id == user_id, SysUserRole.del_flag == 0)
  461. ).update({"del_flag": 1, "update_time": now})
  462. for rid in role_ids:
  463. if not rid:
  464. continue
  465. db.add(SysUserRole(
  466. id=generate_uuid(),
  467. user_id=user_id,
  468. role_id=rid,
  469. tenant_no=tenant_no,
  470. order_num=0,
  471. remark=None,
  472. create_by=creator_id,
  473. create_dept_id="",
  474. create_time=now,
  475. update_by=creator_id,
  476. update_time=now,
  477. del_flag=0,
  478. ))
  479. if post_ids is not None:
  480. db.query(SysUserPost).filter(
  481. and_(SysUserPost.user_id == user_id, SysUserPost.del_flag == 0)
  482. ).update({"del_flag": 1, "update_time": now})
  483. for pid in post_ids:
  484. if not pid:
  485. continue
  486. db.add(SysUserPost(
  487. id=generate_uuid(),
  488. user_id=user_id,
  489. post_id=pid,
  490. tenant_no=tenant_no,
  491. order_num=0,
  492. remark=None,
  493. create_by=creator_id,
  494. create_dept_id="",
  495. create_time=now,
  496. update_by=creator_id,
  497. update_time=now,
  498. del_flag=0,
  499. ))
  500. def _parse_hire_date(value: Optional[Union[str, date]]) -> Optional[date]:
  501. if value is None or value == "":
  502. return None
  503. if isinstance(value, date) and not isinstance(value, datetime):
  504. return value
  505. s = str(value).strip()
  506. if not s:
  507. return None
  508. return datetime.strptime(s[:10], "%Y-%m-%d").date()
  509. def create_user(
  510. db: Session,
  511. *,
  512. user_no: str,
  513. nick_name: str,
  514. password: str,
  515. dept_id: Optional[str] = None,
  516. email: str = "",
  517. phone_no: str = "",
  518. hire_date: Optional[Union[str, date]] = None,
  519. probation_months: Optional[int] = None,
  520. last_work_day: Optional[Union[str, date]] = None,
  521. sex: str = "0",
  522. avatar: Optional[str] = None,
  523. status: str = "1",
  524. tenant_no: str = "zkxw",
  525. order_num: Optional[int] = None,
  526. remark: Optional[str] = None,
  527. user_id: Optional[str] = None,
  528. role_ids: Optional[List[str]] = None,
  529. post_ids: Optional[List[str]] = None,
  530. supervisor_user_id: Optional[str] = None,
  531. ) -> SysUser:
  532. un = (user_no or "").strip()
  533. if not un:
  534. raise ValueError("用户账号不能为空")
  535. nn = (nick_name or "").strip()
  536. if not nn:
  537. raise ValueError("用户名不能为空")
  538. pw = (password or "").strip()
  539. if not pw:
  540. raise ValueError("密码不能为空")
  541. obj = SysUser(
  542. id=generate_uuid(),
  543. dept_id=dept_id or None,
  544. supervisor_user_id=(supervisor_user_id or "").strip() or None,
  545. user_no=un,
  546. nick_name=nn,
  547. email=(email or "").strip(),
  548. phone_no=(phone_no or "").strip(),
  549. hire_date=_parse_hire_date(hire_date),
  550. probation_months=probation_months,
  551. last_work_day=_parse_hire_date(last_work_day),
  552. sex=sex or "0",
  553. avatar=avatar,
  554. password=_hash_password(pw),
  555. status=status or "1",
  556. tenant_no=tenant_no,
  557. order_num=order_num if order_num is not None else 0,
  558. remark=(remark or "").strip() or None,
  559. create_by=user_id,
  560. create_dept_id="",
  561. create_time=datetime.now(),
  562. update_by=user_id,
  563. update_time=datetime.now(),
  564. del_flag=0,
  565. )
  566. db.add(obj)
  567. db.commit()
  568. db.refresh(obj)
  569. _replace_associations(db, obj.id, tenant_no, user_id, role_ids, post_ids)
  570. db.commit()
  571. return obj
  572. def update_user(
  573. db: Session,
  574. user_id: str,
  575. *,
  576. nick_name: Optional[str] = None,
  577. dept_id: Optional[str] = None,
  578. email: Optional[str] = None,
  579. phone_no: Optional[str] = None,
  580. hire_date: Any = ...,
  581. probation_months: Any = ...,
  582. last_work_day: Any = ...,
  583. sex: Optional[str] = None,
  584. avatar: Optional[str] = None,
  585. status: Optional[str] = None,
  586. order_num: Optional[int] = None,
  587. remark: Optional[str] = None,
  588. user_id_updater: Optional[str] = None,
  589. role_ids: Optional[List[str]] = None,
  590. post_ids: Optional[List[str]] = None,
  591. supervisor_user_id: Optional[str] = None,
  592. ) -> SysUser:
  593. obj = get_user(db, user_id)
  594. if not obj:
  595. raise ValueError("用户不存在")
  596. if nick_name is not None:
  597. obj.nick_name = (nick_name or "").strip() or obj.nick_name
  598. if dept_id is not None:
  599. obj.dept_id = dept_id or None
  600. if supervisor_user_id is not None:
  601. obj.supervisor_user_id = (supervisor_user_id or "").strip() or None
  602. if email is not None:
  603. obj.email = (email or "").strip()
  604. if phone_no is not None:
  605. obj.phone_no = (phone_no or "").strip()
  606. if hire_date is not ...:
  607. obj.hire_date = _parse_hire_date(hire_date)
  608. if probation_months is not ...:
  609. if probation_months is None or probation_months == "":
  610. obj.probation_months = None
  611. else:
  612. obj.probation_months = int(probation_months)
  613. if last_work_day is not ...:
  614. obj.last_work_day = _parse_hire_date(last_work_day)
  615. if sex is not None:
  616. obj.sex = sex
  617. if avatar is not None:
  618. obj.avatar = avatar or None
  619. if status is not None:
  620. obj.status = status
  621. if order_num is not None:
  622. obj.order_num = order_num
  623. if remark is not None:
  624. obj.remark = (remark or "").strip() or None
  625. obj.update_by = user_id_updater
  626. obj.update_time = datetime.now()
  627. db.add(obj)
  628. if role_ids is not None or post_ids is not None:
  629. _replace_associations(
  630. db, user_id, obj.tenant_no, user_id_updater,
  631. role_ids=role_ids, post_ids=post_ids,
  632. )
  633. db.commit()
  634. db.refresh(obj)
  635. return obj
  636. def soft_delete_user(db: Session, user_id: str) -> None:
  637. obj = get_user(db, user_id)
  638. if not obj:
  639. raise ValueError("用户不存在")
  640. obj.del_flag = 1
  641. obj.update_time = datetime.now()
  642. db.add(obj)
  643. db.commit()
  644. def reset_password(db: Session, user_id: str, new_password: str) -> None:
  645. obj = get_user(db, user_id)
  646. if not obj:
  647. raise ValueError("用户不存在")
  648. obj.password = _hash_password(new_password)
  649. obj.update_time = datetime.now()
  650. db.add(obj)
  651. db.commit()
  652. def change_password(db: Session, user_id: str, old_password: str, new_password: str) -> None:
  653. from app.core.security import verify_password
  654. obj = get_user(db, user_id)
  655. if not obj:
  656. raise ValueError("用户不存在")
  657. pwd_hash = getattr(obj, "password", None) or ""
  658. if not verify_password(old_password, pwd_hash):
  659. raise ValueError("原密码不正确")
  660. new_pw = (new_password or "").strip()
  661. if len(new_pw) < 6:
  662. raise ValueError("新密码长度不能少于6位")
  663. obj.password = _hash_password(new_pw)
  664. obj.update_by = user_id
  665. obj.update_time = datetime.now()
  666. db.add(obj)
  667. db.commit()
  668. def total_pages(total: int, page_size: int) -> int:
  669. if total <= 0:
  670. return 0
  671. return int(math.ceil(total / float(page_size)))