user_service.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. """用户摘要、组织解析、列表查询与用户维护写操作。"""
  2. from __future__ import annotations
  3. from datetime import UTC, datetime, timezone
  4. from typing import Any
  5. from sqlalchemy import func, or_, select
  6. from sqlalchemy.exc import IntegrityError
  7. from dms.common.enums import (
  8. AllowedModule,
  9. AuditAction,
  10. AuditTarget,
  11. EnabledStatus,
  12. RoleCode,
  13. SecurityLevel,
  14. )
  15. from dms.common.errors import (
  16. AuthenticationError,
  17. ConflictError,
  18. ForbiddenError,
  19. InvalidArgumentError,
  20. ResourceNotFoundError,
  21. UserSelfProtectionError,
  22. )
  23. from dms.common.pagination import PageRequest, page_result
  24. from dms.common.response import serialize_id
  25. from dms.database.transaction import transaction
  26. from dms.extensions import db
  27. from dms.models import Organization, Role, User
  28. from dms.security.auth_context import get_auth_context
  29. from dms.security.passwords import hash_password, validate_password_policy
  30. from dms.services.admin_permission import (
  31. MANAGED_ORG_SELF,
  32. MANAGED_ORG_SELF_ANCHOR,
  33. SUPER_ADMIN_USERNAME,
  34. effective_manage_role,
  35. ensure_org_in_managed_scope,
  36. is_super_admin,
  37. managed_scope_ids,
  38. require_user_editor,
  39. )
  40. from dms.services.audit_service import business_audit
  41. ALLOWED_MODULES = {
  42. RoleCode.USER: [AllowedModule.DOCUMENT_BROWSER.value],
  43. RoleCode.ADMIN: [
  44. AllowedModule.BACKEND_MANAGEMENT.value,
  45. AllowedModule.AUDIT_LOG.value,
  46. AllowedModule.ORGANIZATION_USER_MANAGEMENT.value,
  47. ],
  48. }
  49. CREATE_FIELDS = {
  50. "username",
  51. "realName",
  52. "initialPassword",
  53. "organizationId",
  54. "roleCode",
  55. "roleId",
  56. "securityLevel",
  57. "status",
  58. }
  59. UPDATE_FIELDS = {
  60. "realName",
  61. "organizationId",
  62. "roleCode",
  63. "roleId",
  64. "securityLevel",
  65. "status",
  66. "rowVersion",
  67. }
  68. RESET_PASSWORD_FIELDS = {"newPassword", "rowVersion"}
  69. def _utc_now() -> datetime:
  70. return datetime.now(timezone.utc).replace(tzinfo=None)
  71. def effective_organization(user: User) -> tuple[int | None, str | None]:
  72. if user.organization_id is not None:
  73. organization = db.session.scalar(
  74. select(Organization).where(
  75. Organization.id == user.organization_id,
  76. Organization.is_deleted.is_(False),
  77. Organization.status == EnabledStatus.ENABLED.value,
  78. )
  79. )
  80. if organization is not None:
  81. return organization.id, organization.org_name
  82. return user.organization_id, user.organization_name
  83. def _format_utc(value: datetime | None) -> str | None:
  84. if value is None:
  85. return None
  86. return (
  87. value.replace(tzinfo=UTC)
  88. .isoformat(timespec="milliseconds")
  89. .replace("+00:00", "Z")
  90. )
  91. def allowed_modules_for_user(user: User) -> list[str]:
  92. """按“用户对文档的实际权限”计算可见模块(动态菜单口径)。
  93. - 浏览:登录即有;
  94. - 后台管理:管理员恒可见;普通用户在角色矩阵对任一已发布主案命中
  95. 管理 位(编辑/删除/授权/挂载/解除挂载)时可见;
  96. - 审计 / 机构用户管理:仅管理员。
  97. """
  98. if RoleCode(user.role_code) == RoleCode.ADMIN:
  99. return list(ALLOWED_MODULES[RoleCode.ADMIN])
  100. modules = [AllowedModule.DOCUMENT_BROWSER.value]
  101. from dms.security.auth_context import AuthContext
  102. from dms.services.authorization_service import has_any_manage_action
  103. if has_any_manage_action(
  104. AuthContext(
  105. user_id=user.id,
  106. username=user.username,
  107. real_name=user.real_name,
  108. organization_id=user.organization_id,
  109. organization_name=user.organization_name,
  110. role_code=RoleCode(user.role_code),
  111. security_level=SecurityLevel(user.security_level),
  112. auth_version=user.auth_version,
  113. )
  114. ):
  115. modules.append(AllowedModule.BACKEND_MANAGEMENT.value)
  116. return modules
  117. def effective_role(user: User) -> tuple[str | None, str | None]:
  118. if user.role_id is None:
  119. return None, None
  120. role = db.session.get(Role, user.role_id)
  121. if role is None or role.is_deleted:
  122. return serialize_id(user.role_id), None
  123. return serialize_id(role.id), role.role_name
  124. def user_summary(
  125. user: User,
  126. *,
  127. allowed_modules: list[str] | None = None,
  128. ) -> dict[str, object]:
  129. organization_id, organization_name = effective_organization(user)
  130. role_id, role_name = effective_role(user)
  131. manage_role = effective_manage_role(db.session, user)
  132. # 内置 admin 恒为全权超管(契约1.17③):perm 位恒开、锚点恒全局
  133. super_admin = is_super_admin(user)
  134. return {
  135. "id": serialize_id(user.id),
  136. "username": user.username,
  137. "realName": user.real_name,
  138. "organizationId": serialize_id(organization_id),
  139. "organizationName": organization_name,
  140. "roleCode": user.role_code,
  141. "roleId": role_id,
  142. "roleName": role_name,
  143. "securityLevel": user.security_level,
  144. "status": user.status,
  145. "authVersion": user.auth_version,
  146. "rowVersion": user.row_version,
  147. # 管理权限改挂角色:字段名保留,值取自生效角色
  148. "permCategoryEdit": (
  149. True
  150. if super_admin
  151. else (
  152. bool(manage_role.manage_category_edit)
  153. if manage_role is not None
  154. else False
  155. )
  156. ),
  157. "permOrgEdit": (
  158. True
  159. if super_admin
  160. else (
  161. bool(manage_role.manage_org_edit) if manage_role is not None else False
  162. )
  163. ),
  164. "permUserEdit": (
  165. True
  166. if super_admin
  167. else (
  168. bool(manage_role.manage_user_edit) if manage_role is not None else False
  169. )
  170. ),
  171. "managedOrgId": (
  172. None
  173. if super_admin
  174. else (
  175. MANAGED_ORG_SELF
  176. if manage_role is not None
  177. and manage_role.managed_org_id == MANAGED_ORG_SELF_ANCHOR
  178. else (
  179. serialize_id(manage_role.managed_org_id)
  180. if manage_role is not None
  181. else None
  182. )
  183. )
  184. ),
  185. "lastLoginAt": _format_utc(user.last_login_at),
  186. "lastLoginIp": user.last_login_ip,
  187. "uiTheme": user.ui_theme,
  188. "allowedModules": (
  189. allowed_modules
  190. if allowed_modules is not None
  191. else ALLOWED_MODULES[RoleCode(user.role_code)]
  192. ),
  193. }
  194. def parse_string_id(raw_value: str | None, *, name: str) -> int | None:
  195. if raw_value is None or raw_value == "":
  196. return None
  197. if not raw_value.isdecimal() or int(raw_value) <= 0:
  198. raise InvalidArgumentError(f"{name}必须是正整数形式的字符串ID")
  199. return int(raw_value)
  200. def _descendant_ids(root_id: int) -> set[int]:
  201. organizations = db.session.scalars(
  202. select(Organization).where(Organization.is_deleted.is_(False))
  203. ).all()
  204. if not any(item.id == root_id for item in organizations):
  205. raise ResourceNotFoundError("组织不存在")
  206. children: dict[int | None, list[int]] = {}
  207. for item in organizations:
  208. if item.status == EnabledStatus.ENABLED.value:
  209. children.setdefault(item.parent_id, []).append(item.id)
  210. found = {root_id}
  211. pending = [root_id]
  212. while pending:
  213. current = pending.pop()
  214. for child_id in children.get(current, []):
  215. if child_id not in found:
  216. found.add(child_id)
  217. pending.append(child_id)
  218. return found
  219. def list_users(
  220. *,
  221. organization_id: int | None,
  222. include_descendants: bool,
  223. keyword: str | None,
  224. status: str | None,
  225. page_request: PageRequest,
  226. ) -> dict[str, object]:
  227. filters = [User.is_deleted.is_(False)]
  228. # 锚点管理员强制收敛到管理范围子树(全局管理员不受限),
  229. # 与请求的 organization_id 过滤取交集。
  230. actor = db.session.get(User, get_auth_context().user_id)
  231. if actor is not None:
  232. scope = managed_scope_ids(db.session, actor)
  233. if scope is not None:
  234. filters.append(User.organization_id.in_(scope))
  235. if organization_id is not None:
  236. organization_ids = (
  237. _descendant_ids(organization_id)
  238. if include_descendants
  239. else {organization_id}
  240. )
  241. filters.append(User.organization_id.in_(organization_ids))
  242. if keyword:
  243. pattern = f"%{keyword.strip()}%"
  244. filters.append(or_(User.username.like(pattern), User.real_name.like(pattern)))
  245. if status:
  246. try:
  247. normalized_status = EnabledStatus(status).value
  248. except ValueError as exc:
  249. raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
  250. filters.append(User.status == normalized_status)
  251. total = db.session.scalar(select(func.count(User.id)).where(*filters)) or 0
  252. users = db.session.scalars(
  253. select(User)
  254. .where(*filters)
  255. .order_by(User.real_name.asc(), User.id.asc())
  256. .offset(page_request.offset)
  257. .limit(page_request.page_size)
  258. ).all()
  259. return page_result(
  260. [user_summary(user) for user in users],
  261. page=page_request.page,
  262. page_size=page_request.page_size,
  263. total=total,
  264. )
  265. # ===== 用户维护写操作 =====
  266. def _required_text(
  267. value: Any,
  268. *,
  269. field: str,
  270. max_length: int,
  271. ) -> str:
  272. if not isinstance(value, str) or not value.strip():
  273. raise InvalidArgumentError(f"{field}不能为空")
  274. normalized = value.strip()
  275. if len(normalized) > max_length:
  276. raise InvalidArgumentError(f"{field}长度不能超过{max_length}")
  277. return normalized
  278. def _required_password(value: Any) -> str:
  279. if not isinstance(value, str) or not value:
  280. raise InvalidArgumentError("密码不能为空")
  281. try:
  282. validate_password_policy(value)
  283. except ValueError as exc:
  284. raise InvalidArgumentError(str(exc)) from exc
  285. return value
  286. def _role_code(value: Any) -> str:
  287. if not isinstance(value, str):
  288. raise InvalidArgumentError("roleCode必须是字符串")
  289. try:
  290. return RoleCode(value).value
  291. except ValueError as exc:
  292. raise InvalidArgumentError("roleCode不是有效枚举值") from exc
  293. def _security_level(value: Any) -> str:
  294. if not isinstance(value, str):
  295. raise InvalidArgumentError("securityLevel必须是字符串")
  296. try:
  297. return SecurityLevel(value).value
  298. except ValueError as exc:
  299. raise InvalidArgumentError("securityLevel不是有效枚举值") from exc
  300. def _status_value(value: Any) -> str:
  301. if not isinstance(value, str):
  302. raise InvalidArgumentError("status必须是字符串")
  303. try:
  304. return EnabledStatus(value).value
  305. except ValueError as exc:
  306. raise InvalidArgumentError("status必须是ENABLED或DISABLED") from exc
  307. def _row_version(value: Any) -> int:
  308. if type(value) is not int or value < 0:
  309. raise InvalidArgumentError("rowVersion必须是非负整数")
  310. return value
  311. def _exact_payload(payload: Any, expected: set[str]) -> dict[str, Any]:
  312. if not isinstance(payload, dict) or set(payload) != expected:
  313. raise InvalidArgumentError(
  314. "请求体字段必须且只能包含:" + "、".join(sorted(expected))
  315. )
  316. return payload
  317. def _resolve_organization(
  318. session, organization_id: int | None
  319. ) -> tuple[int | None, str | None]:
  320. if organization_id is None:
  321. return None, None
  322. organization = session.scalar(
  323. select(Organization)
  324. .where(
  325. Organization.id == organization_id,
  326. Organization.is_deleted.is_(False),
  327. Organization.status == EnabledStatus.ENABLED.value,
  328. )
  329. .with_for_update()
  330. )
  331. if organization is None:
  332. raise InvalidArgumentError("organizationId必须指向有效且已启用的机构")
  333. return organization.id, organization.org_name
  334. def _resolve_role(session, role_id: int | None) -> tuple[int | None, str | None]:
  335. """校验并解析自定义角色;USER 与受限 ADMIN 均可关联角色。
  336. ADMIN 关联的角色仅约束其文档操作权限矩阵,管理类权限位不受影响;
  337. 未关联角色(含内置 admin)由授权层回退默认角色。
  338. """
  339. if role_id is None:
  340. return None, None
  341. role = session.scalar(
  342. select(Role).where(Role.id == role_id, Role.is_deleted.is_(False))
  343. )
  344. if role is None:
  345. raise InvalidArgumentError("roleId必须指向有效角色")
  346. return role.id, role.role_name
  347. def _ensure_not_self(actor: Any, target_user_id: int, *, action: str) -> None:
  348. if actor.user_id == target_user_id:
  349. raise UserSelfProtectionError(
  350. f"不能对本账号执行{action}操作",
  351. )
  352. def _ensure_not_builtin_admin(user: User, *, action: str) -> None:
  353. """内置 admin 账号不允许被任何用户编辑、删除或重置密码。"""
  354. if user.username == SUPER_ADMIN_USERNAME:
  355. raise ForbiddenError(f"内置admin账号不允许被{action}")
  356. def _other_active_admin_count(session, exclude_user_id: int) -> int:
  357. """统计除指定用户外的活跃管理员数量。"""
  358. return (
  359. session.scalar(
  360. select(func.count(User.id)).where(
  361. User.role_code == RoleCode.ADMIN.value,
  362. User.status == EnabledStatus.ENABLED.value,
  363. User.is_deleted.is_(False),
  364. User.id != exclude_user_id,
  365. )
  366. )
  367. or 0
  368. )
  369. def _ensure_not_last_active_admin(session, user: User, *, reason: str) -> None:
  370. """禁止影响系统最后一个活跃管理员(仅在 user 当前是活跃管理员时生效)。
  371. 统一的守卫:覆盖删除、降级、停用三种场景。
  372. 若 user 当前不是活跃管理员(角色非 ADMIN 或状态非 ENABLED),
  373. 则其消失不影响活跃管理员数,直接放行。
  374. """
  375. if (
  376. user.role_code != RoleCode.ADMIN.value
  377. or user.status != EnabledStatus.ENABLED.value
  378. ):
  379. return
  380. if _other_active_admin_count(session, user.id) == 0:
  381. raise ConflictError(
  382. "系统至少需保留一个启用的系统管理员",
  383. details={"roleId": user.role_code, "reason": reason},
  384. )
  385. def create_user(payload: Any) -> dict[str, object]:
  386. body = _exact_payload(payload, CREATE_FIELDS)
  387. username = _required_text(body["username"], field="username", max_length=64)
  388. real_name = _required_text(body["realName"], field="realName", max_length=64)
  389. initial_password = _required_password(body["initialPassword"])
  390. organization_id = parse_string_id(body["organizationId"], name="organizationId")
  391. role_code = _role_code(body["roleCode"])
  392. role_id = parse_string_id(body["roleId"], name="roleId")
  393. security_level = _security_level(body["securityLevel"])
  394. status = _status_value(body["status"])
  395. actor_id = get_auth_context().user_id
  396. try:
  397. with transaction() as session:
  398. actor = require_user_editor(session)
  399. ensure_org_in_managed_scope(session, actor, organization_id)
  400. if (
  401. session.scalar(
  402. select(User.id).where(
  403. User.username == username,
  404. User.is_deleted.is_(False),
  405. )
  406. )
  407. is not None
  408. ):
  409. raise ConflictError("用户名已存在")
  410. org_id, org_name = _resolve_organization(session, organization_id)
  411. resolved_role_id, resolved_role_name = _resolve_role(session, role_id)
  412. user = User(
  413. username=username,
  414. password_hash=hash_password(initial_password),
  415. real_name=real_name,
  416. organization_id=org_id,
  417. organization_name=org_name,
  418. role_code=role_code,
  419. role_id=resolved_role_id,
  420. security_level=security_level,
  421. status=status,
  422. created_by=actor_id,
  423. updated_by=actor_id,
  424. )
  425. session.add(user)
  426. session.flush()
  427. session.add(
  428. business_audit(
  429. action=AuditAction.CREATE_USER,
  430. target=AuditTarget.USER,
  431. target_id=user.id,
  432. target_name=user.real_name,
  433. detail={
  434. "username": user.username,
  435. "organizationId": serialize_id(user.organization_id),
  436. "organizationName": user.organization_name,
  437. "roleCode": user.role_code,
  438. "roleId": serialize_id(user.role_id),
  439. "roleName": resolved_role_name,
  440. "securityLevel": user.security_level,
  441. "status": user.status,
  442. },
  443. )
  444. )
  445. return user_summary(user)
  446. except IntegrityError as exc:
  447. raise ConflictError("用户名已存在") from exc
  448. def update_user(user_id: int, payload: Any) -> dict[str, object]:
  449. body = _exact_payload(payload, UPDATE_FIELDS)
  450. real_name = _required_text(body["realName"], field="realName", max_length=64)
  451. organization_id = parse_string_id(
  452. body["organizationId"], name="organizationId"
  453. )
  454. new_role_code = _role_code(body["roleCode"])
  455. new_role_id = parse_string_id(body["roleId"], name="roleId")
  456. new_security_level = _security_level(body["securityLevel"])
  457. new_status = _status_value(body["status"])
  458. expected_version = _row_version(body["rowVersion"])
  459. actor = get_auth_context()
  460. actor_id = actor.user_id
  461. now = _utc_now()
  462. _ensure_not_self(actor, user_id, action="编辑角色或状态")
  463. with transaction() as session:
  464. actor_user = require_user_editor(session)
  465. user = session.scalar(
  466. select(User)
  467. .where(
  468. User.id == user_id,
  469. User.is_deleted.is_(False),
  470. )
  471. .with_for_update()
  472. )
  473. if user is None:
  474. raise ResourceNotFoundError("用户不存在")
  475. _ensure_not_builtin_admin(user, action="编辑")
  476. ensure_org_in_managed_scope(session, actor_user, user.organization_id)
  477. if user.row_version != expected_version:
  478. raise ConflictError(
  479. "数据已被其他用户修改,请刷新后重试",
  480. details={"currentRowVersion": user.row_version},
  481. )
  482. before = {
  483. "realName": user.real_name,
  484. "organizationId": serialize_id(user.organization_id),
  485. "organizationName": user.organization_name,
  486. "roleCode": user.role_code,
  487. "roleId": serialize_id(user.role_id),
  488. "securityLevel": user.security_level,
  489. "status": user.status,
  490. "rowVersion": user.row_version,
  491. }
  492. # 角色变更或停用:合并检查最后一个活跃管理员
  493. role_changes = new_role_code != user.role_code
  494. status_changes = new_status != user.status
  495. will_lose_active_admin = (
  496. role_changes
  497. or (status_changes and new_status == EnabledStatus.DISABLED.value)
  498. )
  499. if will_lose_active_admin:
  500. _ensure_not_last_active_admin(
  501. session, user, reason="role_or_status_change"
  502. )
  503. org_id, org_name = _resolve_organization(session, organization_id)
  504. # 迁入机构同样须在管理范围内:锚点管理员不可将用户移出自己管理的子树
  505. ensure_org_in_managed_scope(session, actor_user, org_id)
  506. resolved_role_id, resolved_role_name = _resolve_role(session, new_role_id)
  507. role_assignment_changes = resolved_role_id != user.role_id
  508. user.real_name = real_name
  509. user.organization_id = org_id
  510. user.organization_name = org_name
  511. user.role_code = new_role_code
  512. user.role_id = resolved_role_id
  513. user.security_level = new_security_level
  514. user.status = new_status
  515. user.updated_by = actor_id
  516. user.updated_at = now
  517. user.row_version += 1
  518. # 内置角色、自定义角色变更或停用都需让旧 Token 失效;
  519. # 管理类权限与锚点调整不改变认证身份,不失效旧 Token
  520. auth_bump = (
  521. role_changes
  522. or role_assignment_changes
  523. or (status_changes and new_status == EnabledStatus.DISABLED.value)
  524. )
  525. if auth_bump:
  526. user.auth_version += 1
  527. after = {
  528. "realName": user.real_name,
  529. "organizationId": serialize_id(user.organization_id),
  530. "organizationName": user.organization_name,
  531. "roleCode": user.role_code,
  532. "roleId": serialize_id(user.role_id),
  533. "securityLevel": user.security_level,
  534. "status": user.status,
  535. "rowVersion": user.row_version,
  536. }
  537. session.add(
  538. business_audit(
  539. action=AuditAction.EDIT_USER,
  540. target=AuditTarget.USER,
  541. target_id=user.id,
  542. target_name=user.real_name,
  543. detail={
  544. "before": before,
  545. "after": after,
  546. "roleName": resolved_role_name,
  547. "authVersionBumped": auth_bump,
  548. },
  549. )
  550. )
  551. return user_summary(user)
  552. def delete_user(user_id: int, row_version: int) -> dict[str, object]:
  553. actor = get_auth_context()
  554. actor_id = actor.user_id
  555. now = _utc_now()
  556. _ensure_not_self(actor, user_id, action="删除")
  557. with transaction() as session:
  558. actor_user = require_user_editor(session)
  559. user = session.scalar(
  560. select(User)
  561. .where(
  562. User.id == user_id,
  563. User.is_deleted.is_(False),
  564. )
  565. .with_for_update()
  566. )
  567. if user is None:
  568. raise ResourceNotFoundError("用户不存在")
  569. _ensure_not_builtin_admin(user, action="删除")
  570. ensure_org_in_managed_scope(session, actor_user, user.organization_id)
  571. if user.row_version != row_version:
  572. raise ConflictError(
  573. "数据已被其他用户修改,请刷新后重试",
  574. details={"currentRowVersion": user.row_version},
  575. )
  576. # 删除:禁止删除最后一个活跃管理员
  577. _ensure_not_last_active_admin(session, user, reason="delete")
  578. target_name = user.real_name
  579. target_username = user.username
  580. user.is_deleted = True
  581. user.deleted_at = now
  582. user.updated_at = now
  583. user.updated_by = actor_id
  584. user.status = EnabledStatus.DISABLED.value
  585. user.auth_version += 1
  586. user.row_version += 1
  587. session.add(
  588. business_audit(
  589. action=AuditAction.DELETE_USER,
  590. target=AuditTarget.USER,
  591. target_id=user.id,
  592. target_name=target_name,
  593. detail={
  594. "username": target_username,
  595. "rowVersion": user.row_version,
  596. },
  597. )
  598. )
  599. return {"id": serialize_id(user.id), "deleted": True}
  600. def reset_user_password(user_id: int, payload: Any) -> dict[str, object]:
  601. body = _exact_payload(payload, RESET_PASSWORD_FIELDS)
  602. new_password = _required_password(body["newPassword"])
  603. expected_version = _row_version(body["rowVersion"])
  604. actor = get_auth_context()
  605. actor_id = actor.user_id
  606. now = _utc_now()
  607. _ensure_not_self(actor, user_id, action="重置密码")
  608. with transaction() as session:
  609. actor_user = require_user_editor(session)
  610. user = session.scalar(
  611. select(User)
  612. .where(
  613. User.id == user_id,
  614. User.is_deleted.is_(False),
  615. )
  616. .with_for_update()
  617. )
  618. if user is None:
  619. raise ResourceNotFoundError("用户不存在")
  620. _ensure_not_builtin_admin(user, action="重置密码")
  621. ensure_org_in_managed_scope(session, actor_user, user.organization_id)
  622. if user.row_version != expected_version:
  623. raise ConflictError(
  624. "数据已被其他用户修改,请刷新后重试",
  625. details={"currentRowVersion": user.row_version},
  626. )
  627. user.password_hash = hash_password(new_password)
  628. user.auth_version += 1
  629. user.updated_by = actor_id
  630. user.updated_at = now
  631. user.row_version += 1
  632. session.add(
  633. business_audit(
  634. action=AuditAction.RESET_USER_PASSWORD,
  635. target=AuditTarget.USER,
  636. target_id=user.id,
  637. target_name=user.real_name,
  638. detail={
  639. "username": user.username,
  640. "rowVersion": user.row_version,
  641. },
  642. )
  643. )
  644. return {
  645. "id": serialize_id(user.id),
  646. "reset": True,
  647. "authVersion": user.auth_version,
  648. }
  649. UI_THEMES = ("blue", "red", "green")
  650. def update_ui_theme(payload: Any) -> dict[str, object]:
  651. """本人界面主题偏好(契约1.20):白名单校验,仅操作本人行。
  652. 主题是账号级漫游的纯视觉偏好:不 bump rowVersion(避免干扰并发
  653. 编辑乐观锁)、不 bump authVersion(不下线会话)、不写审计。
  654. """
  655. body = _exact_payload(payload, {"uiTheme"})
  656. theme = body["uiTheme"]
  657. if not isinstance(theme, str) or theme not in UI_THEMES:
  658. raise InvalidArgumentError("uiTheme必须是blue、red或green")
  659. context = get_auth_context()
  660. with transaction() as session:
  661. user = session.scalar(
  662. select(User).where(
  663. User.id == context.user_id,
  664. User.is_deleted.is_(False),
  665. )
  666. )
  667. if user is None:
  668. raise AuthenticationError()
  669. user.ui_theme = theme
  670. session.flush()
  671. return user_summary(user)