users.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. from typing import List, Any
  2. from fastapi import APIRouter, Depends, HTTPException, Body, BackgroundTasks, Request, Query
  3. from sqlalchemy.orm import Session
  4. from sqlalchemy import or_
  5. from sqlalchemy.exc import IntegrityError
  6. from app.api.v1 import deps
  7. from app.core import security
  8. from app.core.utils import generate_english_name
  9. from app.models.user import User, UserRole
  10. from app.models.mapping import AppUserMapping
  11. from app.schemas.user import User as UserSchema, UserCreate, UserUpdate, UserList, PromoteUserRequest
  12. from app.services.webhook_service import WebhookService
  13. from app.services.captcha_service import CaptchaService
  14. from app.services.log_service import LogService
  15. from app.schemas.operation_log import ActionType
  16. router = APIRouter()
  17. @router.get("/search", response_model=List[UserSchema], summary="搜索用户")
  18. def search_users(
  19. keyword: str = Query(None),
  20. limit: int = 20,
  21. db: Session = Depends(deps.get_db),
  22. current_user: User = Depends(deps.get_current_active_user),
  23. ):
  24. """
  25. 搜索可作为应用转让目标的用户(开发者或超级管理员)。
  26. """
  27. # Only Developers and Super Admins can search
  28. if current_user.role not in ["SUPER_ADMIN", "DEVELOPER"]:
  29. raise HTTPException(status_code=403, detail="权限不足")
  30. query = db.query(User).filter(
  31. User.is_deleted == 0,
  32. User.status == "ACTIVE",
  33. User.role.in_(["DEVELOPER", "SUPER_ADMIN"])
  34. )
  35. if keyword:
  36. query = query.filter(User.mobile.ilike(f"%{keyword}%"))
  37. # Exclude self
  38. query = query.filter(User.id != current_user.id)
  39. users = query.limit(limit).all()
  40. return users
  41. @router.get("/", response_model=UserList, summary="获取用户列表")
  42. def read_users(
  43. skip: int = 0,
  44. limit: int = 10,
  45. status: str = None,
  46. role: str = None,
  47. mobile: str = None,
  48. name: str = None,
  49. english_name: str = None,
  50. keyword: str = None,
  51. db: Session = Depends(deps.get_db),
  52. current_user: User = Depends(deps.get_current_active_user),
  53. ):
  54. """
  55. 获取用户列表。只有超级管理员可以查看所有用户。
  56. 支持通过 mobile, name, english_name 精确/模糊筛选,
  57. 也支持通过 keyword 进行跨字段模糊搜索。
  58. """
  59. if current_user.role != "SUPER_ADMIN":
  60. raise HTTPException(status_code=403, detail="权限不足")
  61. # Filter out soft-deleted users
  62. query = db.query(User).filter(User.is_deleted == 0)
  63. if status:
  64. query = query.filter(User.status == status)
  65. if role:
  66. query = query.filter(User.role == role)
  67. # Specific field filters (AND logic)
  68. if mobile:
  69. query = query.filter(User.mobile.ilike(f"%{mobile}%"))
  70. if name:
  71. query = query.filter(User.name.ilike(f"%{name}%"))
  72. if english_name:
  73. query = query.filter(User.english_name.ilike(f"%{english_name}%"))
  74. # Keyword search (OR logic across fields) - intersects with previous filters
  75. if keyword:
  76. query = query.filter(
  77. or_(
  78. User.mobile.ilike(f"%{keyword}%"),
  79. User.name.ilike(f"%{keyword}%"),
  80. User.english_name.ilike(f"%{keyword}%")
  81. )
  82. )
  83. total = query.count()
  84. users = query.order_by(User.id.desc()).offset(skip).limit(limit).all()
  85. return {"total": total, "items": users}
  86. @router.post("/", response_model=UserSchema, summary="创建用户")
  87. def create_user(
  88. *,
  89. db: Session = Depends(deps.get_db),
  90. user_in: UserCreate,
  91. request: Request,
  92. background_tasks: BackgroundTasks,
  93. current_user: User = Depends(deps.get_current_active_user),
  94. ):
  95. """
  96. 创建新用户。只有超级管理员可以直接创建用户。
  97. 公开注册请使用 /open/register。
  98. """
  99. if current_user.role != "SUPER_ADMIN":
  100. raise HTTPException(status_code=403, detail="权限不足")
  101. # Verify Admin Password
  102. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  103. raise HTTPException(status_code=401, detail="管理员密码错误")
  104. user = db.query(User).filter(User.mobile == user_in.mobile).first()
  105. if user:
  106. raise HTTPException(
  107. status_code=400,
  108. detail="该手机号已在系统中注册",
  109. )
  110. if user_in.password:
  111. user_in.password = user_in.password.strip()
  112. # Generate default name if missing
  113. if not user_in.name:
  114. random_suffix = security.generate_alphanumeric_password(6)
  115. user_in.name = f"用户{random_suffix}"
  116. # Ensure unique name
  117. if user_in.name:
  118. original_name = user_in.name
  119. counter = 1
  120. while db.query(User).filter(User.name == user_in.name, User.is_deleted == 0).first():
  121. user_in.name = f"{original_name}{counter}"
  122. counter += 1
  123. english_name = user_in.english_name
  124. if not english_name and user_in.name:
  125. english_name = generate_english_name(user_in.name)
  126. # Ensure unique english_name
  127. if english_name:
  128. original_english_name = english_name
  129. counter = 1
  130. while db.query(User).filter(User.english_name == english_name, User.is_deleted == 0).first():
  131. english_name = f"{original_english_name}{counter}"
  132. counter += 1
  133. db_user = User(
  134. mobile=user_in.mobile,
  135. name=user_in.name,
  136. english_name=english_name,
  137. password_hash=security.get_password_hash(user_in.password),
  138. status="ACTIVE", # Admin created users are active by default
  139. role=user_in.role or UserRole.ORDINARY_USER
  140. )
  141. db.add(db_user)
  142. db.commit()
  143. db.refresh(db_user)
  144. # Log Operation
  145. LogService.create_log(
  146. db=db,
  147. operator_id=current_user.id,
  148. action_type=ActionType.MANUAL_ADD,
  149. target_user_id=db_user.id,
  150. target_mobile=db_user.mobile,
  151. ip_address=request.client.host,
  152. details={"role": db_user.role}
  153. )
  154. return db_user
  155. @router.put("/{user_id}", response_model=UserSchema, summary="更新用户")
  156. def update_user(
  157. *,
  158. db: Session = Depends(deps.get_db),
  159. user_id: int,
  160. user_in: UserUpdate,
  161. request: Request,
  162. background_tasks: BackgroundTasks,
  163. current_user: User = Depends(deps.get_current_active_user),
  164. ):
  165. """
  166. 更新用户信息。超级管理员可以更新任何人,用户只能更新自己。
  167. """
  168. user = db.query(User).filter(User.id == user_id).first()
  169. if not user:
  170. raise HTTPException(status_code=404, detail="用户不存在")
  171. # Permission Check
  172. if current_user.role != "SUPER_ADMIN" and current_user.id != user_id:
  173. raise HTTPException(status_code=403, detail="权限不足")
  174. update_data = user_in.model_dump(exclude_unset=True)
  175. # Track actions for logging
  176. actions = []
  177. # Only Super Admin can change mobile
  178. if "mobile" in update_data:
  179. if current_user.role != "SUPER_ADMIN":
  180. del update_data["mobile"]
  181. else:
  182. # Require admin password for mobile change
  183. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  184. raise HTTPException(status_code=401, detail="管理员密码错误")
  185. # Check uniqueness
  186. existing_user = db.query(User).filter(User.mobile == update_data["mobile"]).first()
  187. if existing_user and existing_user.id != user_id:
  188. raise HTTPException(status_code=400, detail="该手机号已存在")
  189. if user.mobile != update_data["mobile"]:
  190. old_mobile = user.mobile
  191. new_mobile = update_data["mobile"]
  192. # Update Mappings first (logic: user mobile change should propagate to mappings)
  193. # Find mappings where mapped_key matches old_mobile
  194. mappings = db.query(AppUserMapping).filter(
  195. AppUserMapping.user_id == user.id,
  196. AppUserMapping.mapped_key == old_mobile
  197. ).all()
  198. for mapping in mappings:
  199. mapping.mapped_key = new_mobile
  200. db.add(mapping)
  201. try:
  202. db.flush()
  203. except IntegrityError:
  204. db.rollback()
  205. raise HTTPException(status_code=400, detail="修改失败:新手机号在某些应用中已存在映射关联")
  206. actions.append((ActionType.UPDATE, {"field": "mobile", "old": old_mobile, "new": new_mobile}))
  207. # Only Super Admin can change status or role
  208. if "status" in update_data:
  209. if current_user.role != "SUPER_ADMIN":
  210. del update_data["status"]
  211. elif update_data["status"] == "DISABLED" and user_id == current_user.id:
  212. raise HTTPException(status_code=400, detail="超级管理员不能禁用自己")
  213. else:
  214. # Require admin password for status change
  215. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  216. raise HTTPException(status_code=401, detail="管理员密码错误")
  217. # Add Log Action
  218. action_type = ActionType.DISABLE if update_data["status"] == "DISABLED" else ActionType.ENABLE
  219. actions.append((action_type, {"old": user.status, "new": update_data["status"]}))
  220. if "role" in update_data:
  221. if current_user.role != "SUPER_ADMIN":
  222. del update_data["role"]
  223. elif user_id == current_user.id and update_data["role"] != "SUPER_ADMIN":
  224. # Prevent demoting self
  225. raise HTTPException(status_code=400, detail="超级管理员不能降级自己")
  226. else:
  227. # Require admin password for role change
  228. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  229. raise HTTPException(status_code=401, detail="管理员密码错误")
  230. actions.append((ActionType.CHANGE_ROLE, {"old": user.role, "new": update_data["role"]}))
  231. if "admin_password" in update_data:
  232. del update_data["admin_password"]
  233. if "is_deleted" in update_data:
  234. if current_user.role != "SUPER_ADMIN":
  235. del update_data["is_deleted"]
  236. elif update_data["is_deleted"] == 1 and user_id == current_user.id:
  237. raise HTTPException(status_code=400, detail="超级管理员不能删除自己")
  238. elif update_data["is_deleted"] == 1:
  239. actions.append((ActionType.DELETE, {}))
  240. if "password" in update_data:
  241. password = update_data["password"]
  242. if password:
  243. password = password.strip()
  244. hashed_password = security.get_password_hash(password)
  245. del update_data["password"]
  246. user.password_hash = hashed_password
  247. # Auto-generate english_name if name changed and english_name not provided
  248. if "name" in update_data and update_data["name"]:
  249. if "english_name" not in update_data or not update_data["english_name"]:
  250. update_data["english_name"] = generate_english_name(update_data["name"])
  251. for field, value in update_data.items():
  252. setattr(user, field, value)
  253. db.add(user)
  254. db.commit()
  255. db.refresh(user)
  256. # Trigger Webhook
  257. event_type = "UPDATE"
  258. if user.status == "DISABLED":
  259. event_type = "DISABLE"
  260. background_tasks.add_task(WebhookService.trigger_user_event, db, user.id, event_type)
  261. # Record Logs
  262. if current_user.role == "SUPER_ADMIN" and actions:
  263. for action_type, details in actions:
  264. LogService.create_log(
  265. db=db,
  266. operator_id=current_user.id,
  267. action_type=action_type,
  268. target_user_id=user.id,
  269. target_mobile=user.mobile,
  270. ip_address=request.client.host,
  271. details=details
  272. )
  273. return user
  274. @router.post("/{user_id}/promote", response_model=UserSchema, summary="提升用户权限")
  275. def promote_user(
  276. *,
  277. db: Session = Depends(deps.get_db),
  278. user_id: int,
  279. req: PromoteUserRequest,
  280. request: Request,
  281. current_user: User = Depends(deps.get_current_active_user),
  282. ):
  283. """
  284. 将开发者提升为超级管理员。
  285. 需要验证(管理员密码 + 验证码)。
  286. """
  287. if current_user.role != "SUPER_ADMIN":
  288. raise HTTPException(status_code=403, detail="权限不足")
  289. # 1. Verify Password
  290. if not security.verify_password(req.password, current_user.password_hash):
  291. raise HTTPException(status_code=401, detail="密码错误")
  292. # 2. Verify Captcha
  293. if not CaptchaService.verify_captcha(req.captcha_id, req.captcha_code):
  294. raise HTTPException(status_code=400, detail="验证码错误")
  295. user = db.query(User).filter(User.id == user_id).first()
  296. if not user:
  297. raise HTTPException(status_code=404, detail="用户不存在")
  298. old_role = user.role
  299. user.role = "SUPER_ADMIN"
  300. user.status = "ACTIVE" # Ensure they are active
  301. db.add(user)
  302. db.commit()
  303. db.refresh(user)
  304. # Log Operation
  305. LogService.create_log(
  306. db=db,
  307. operator_id=current_user.id,
  308. action_type=ActionType.CHANGE_ROLE,
  309. target_user_id=user.id,
  310. target_mobile=user.mobile,
  311. ip_address=request.client.host,
  312. details={"old": old_role, "new": "SUPER_ADMIN"}
  313. )
  314. return user
  315. @router.delete("/{user_id}", response_model=UserSchema, summary="删除用户")
  316. def delete_user(
  317. *,
  318. db: Session = Depends(deps.get_db),
  319. user_id: int,
  320. request: Request,
  321. current_user: User = Depends(deps.get_current_active_user),
  322. background_tasks: BackgroundTasks,
  323. ):
  324. """
  325. 软删除用户。仅限超级管理员。
  326. """
  327. if current_user.role != "SUPER_ADMIN":
  328. raise HTTPException(status_code=403, detail="权限不足")
  329. if user_id == current_user.id:
  330. raise HTTPException(status_code=400, detail="超级管理员不能删除自己")
  331. user = db.query(User).filter(User.id == user_id).first()
  332. if not user:
  333. raise HTTPException(status_code=404, detail="用户不存在")
  334. user.is_deleted = 1 # Using Integer 1 for True
  335. user.status = "DISABLED" # Also disable login
  336. db.add(user)
  337. db.commit()
  338. background_tasks.add_task(WebhookService.trigger_user_event, db, user.id, "DELETE")
  339. # Log Operation
  340. LogService.create_log(
  341. db=db,
  342. operator_id=current_user.id,
  343. action_type=ActionType.DELETE,
  344. target_user_id=user.id,
  345. target_mobile=user.mobile,
  346. ip_address=request.client.host,
  347. details={"status": "DISABLED"}
  348. )
  349. return user
  350. @router.get("/me", response_model=UserSchema, summary="获取当前用户信息")
  351. def read_user_me(
  352. current_user: User = Depends(deps.get_current_active_user),
  353. ):
  354. """
  355. 获取当前登录用户的信息。
  356. """
  357. return current_user
  358. @router.get("/{user_id}", response_model=UserSchema, summary="根据ID获取用户")
  359. def read_user_by_id(
  360. user_id: int,
  361. current_user: User = Depends(deps.get_current_active_user),
  362. db: Session = Depends(deps.get_db),
  363. ):
  364. """
  365. 根据用户ID获取特定用户信息。
  366. """
  367. user = db.query(User).filter(User.id == user_id).first()
  368. if not user:
  369. raise HTTPException(
  370. status_code=404,
  371. detail="该用户ID在系统中不存在",
  372. )
  373. return user