users.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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, BatchResetEnglishNameRequest
  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.post("/batch/reset-english-name", summary="批量重置用户英文名")
  156. def batch_reset_english_name(
  157. *,
  158. db: Session = Depends(deps.get_db),
  159. req: BatchResetEnglishNameRequest,
  160. request: Request,
  161. current_user: User = Depends(deps.get_current_active_user),
  162. ):
  163. """
  164. 批量重置选中用户的英文名。
  165. 规则:根据姓名生成拼音,如有重复自动追加数字后缀。
  166. 需要管理员密码验证。
  167. """
  168. if current_user.role != "SUPER_ADMIN":
  169. raise HTTPException(status_code=403, detail="权限不足")
  170. # Verify Admin Password
  171. if not security.verify_password(req.admin_password, current_user.password_hash):
  172. raise HTTPException(status_code=401, detail="管理员密码错误")
  173. if not req.user_ids:
  174. raise HTTPException(status_code=400, detail="请选择用户")
  175. success_count = 0
  176. users = db.query(User).filter(User.id.in_(req.user_ids), User.is_deleted == 0).all()
  177. for user in users:
  178. if not user.name:
  179. continue
  180. old_english_name = user.english_name
  181. new_english_name = generate_english_name(user.name)
  182. # Uniqueness check
  183. if new_english_name:
  184. original_base = new_english_name
  185. counter = 1
  186. # Check against DB (excluding self if it accidentally matches, though unlikely for reset)
  187. # Actually for reset, even if it matches self, we might want to keep it or update it.
  188. # But the goal is uniqueness.
  189. # Helper to check existence
  190. def check_exists(name, current_id):
  191. return db.query(User).filter(
  192. User.english_name == name,
  193. User.is_deleted == 0,
  194. User.id != current_id
  195. ).first()
  196. while check_exists(new_english_name, user.id):
  197. new_english_name = f"{original_base}{counter}"
  198. counter += 1
  199. if old_english_name != new_english_name:
  200. user.english_name = new_english_name
  201. db.add(user)
  202. # Log
  203. LogService.create_log(
  204. db=db,
  205. operator_id=current_user.id,
  206. action_type=ActionType.UPDATE,
  207. target_user_id=user.id,
  208. target_mobile=user.mobile,
  209. ip_address=request.client.host,
  210. details={
  211. "field": "english_name",
  212. "old": old_english_name,
  213. "new": new_english_name,
  214. "reason": "BATCH_RESET"
  215. }
  216. )
  217. success_count += 1
  218. db.commit()
  219. return {"success": True, "count": success_count}
  220. @router.put("/{user_id}", response_model=UserSchema, summary="更新用户")
  221. def update_user(
  222. *,
  223. db: Session = Depends(deps.get_db),
  224. user_id: int,
  225. user_in: UserUpdate,
  226. request: Request,
  227. background_tasks: BackgroundTasks,
  228. current_user: User = Depends(deps.get_current_active_user),
  229. ):
  230. """
  231. 更新用户信息。超级管理员可以更新任何人,用户只能更新自己。
  232. """
  233. user = db.query(User).filter(User.id == user_id).first()
  234. if not user:
  235. raise HTTPException(status_code=404, detail="用户不存在")
  236. # Permission Check
  237. if current_user.role != "SUPER_ADMIN" and current_user.id != user_id:
  238. raise HTTPException(status_code=403, detail="权限不足")
  239. update_data = user_in.model_dump(exclude_unset=True)
  240. # Track actions for logging
  241. actions = []
  242. # Only Super Admin can change mobile
  243. if "mobile" in update_data:
  244. if current_user.role != "SUPER_ADMIN":
  245. del update_data["mobile"]
  246. else:
  247. # Require admin password for mobile change
  248. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  249. raise HTTPException(status_code=401, detail="管理员密码错误")
  250. # Check uniqueness
  251. existing_user = db.query(User).filter(User.mobile == update_data["mobile"]).first()
  252. if existing_user and existing_user.id != user_id:
  253. raise HTTPException(status_code=400, detail="该手机号已存在")
  254. if user.mobile != update_data["mobile"]:
  255. old_mobile = user.mobile
  256. new_mobile = update_data["mobile"]
  257. # Update Mappings first (logic: user mobile change should propagate to mappings)
  258. # Find mappings where mapped_key matches old_mobile
  259. mappings = db.query(AppUserMapping).filter(
  260. AppUserMapping.user_id == user.id,
  261. AppUserMapping.mapped_key == old_mobile
  262. ).all()
  263. for mapping in mappings:
  264. mapping.mapped_key = new_mobile
  265. db.add(mapping)
  266. try:
  267. db.flush()
  268. except IntegrityError:
  269. db.rollback()
  270. raise HTTPException(status_code=400, detail="修改失败:新手机号在某些应用中已存在映射关联")
  271. actions.append((ActionType.UPDATE, {"field": "mobile", "old": old_mobile, "new": new_mobile}))
  272. # Only Super Admin can change status or role
  273. if "status" in update_data:
  274. if current_user.role != "SUPER_ADMIN":
  275. del update_data["status"]
  276. elif update_data["status"] == "DISABLED" and user_id == current_user.id:
  277. raise HTTPException(status_code=400, detail="超级管理员不能禁用自己")
  278. else:
  279. # Require admin password for status change
  280. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  281. raise HTTPException(status_code=401, detail="管理员密码错误")
  282. # Add Log Action
  283. action_type = ActionType.DISABLE if update_data["status"] == "DISABLED" else ActionType.ENABLE
  284. actions.append((action_type, {"old": user.status, "new": update_data["status"]}))
  285. if "role" in update_data:
  286. if current_user.role != "SUPER_ADMIN":
  287. del update_data["role"]
  288. elif user_id == current_user.id and update_data["role"] != "SUPER_ADMIN":
  289. # Prevent demoting self
  290. raise HTTPException(status_code=400, detail="超级管理员不能降级自己")
  291. else:
  292. # Require admin password for role change
  293. if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
  294. raise HTTPException(status_code=401, detail="管理员密码错误")
  295. actions.append((ActionType.CHANGE_ROLE, {"old": user.role, "new": update_data["role"]}))
  296. if "admin_password" in update_data:
  297. del update_data["admin_password"]
  298. if "is_deleted" in update_data:
  299. if current_user.role != "SUPER_ADMIN":
  300. del update_data["is_deleted"]
  301. elif update_data["is_deleted"] == 1 and user_id == current_user.id:
  302. raise HTTPException(status_code=400, detail="超级管理员不能删除自己")
  303. elif update_data["is_deleted"] == 1:
  304. actions.append((ActionType.DELETE, {}))
  305. if "password" in update_data:
  306. password = update_data["password"]
  307. if password:
  308. password = password.strip()
  309. hashed_password = security.get_password_hash(password)
  310. del update_data["password"]
  311. user.password_hash = hashed_password
  312. # Auto-generate english_name if name changed and english_name not provided
  313. if "name" in update_data and update_data["name"]:
  314. if "english_name" not in update_data or not update_data["english_name"]:
  315. update_data["english_name"] = generate_english_name(update_data["name"])
  316. for field, value in update_data.items():
  317. setattr(user, field, value)
  318. db.add(user)
  319. db.commit()
  320. db.refresh(user)
  321. # Trigger Webhook
  322. event_type = "UPDATE"
  323. if user.status == "DISABLED":
  324. event_type = "DISABLE"
  325. background_tasks.add_task(WebhookService.trigger_user_event, db, user.id, event_type)
  326. # Record Logs
  327. if current_user.role == "SUPER_ADMIN" and actions:
  328. for action_type, details in actions:
  329. LogService.create_log(
  330. db=db,
  331. operator_id=current_user.id,
  332. action_type=action_type,
  333. target_user_id=user.id,
  334. target_mobile=user.mobile,
  335. ip_address=request.client.host,
  336. details=details
  337. )
  338. return user
  339. @router.post("/{user_id}/promote", response_model=UserSchema, summary="提升用户权限")
  340. def promote_user(
  341. *,
  342. db: Session = Depends(deps.get_db),
  343. user_id: int,
  344. req: PromoteUserRequest,
  345. request: Request,
  346. current_user: User = Depends(deps.get_current_active_user),
  347. ):
  348. """
  349. 将开发者提升为超级管理员。
  350. 需要验证(管理员密码 + 验证码)。
  351. """
  352. if current_user.role != "SUPER_ADMIN":
  353. raise HTTPException(status_code=403, detail="权限不足")
  354. # 1. Verify Password
  355. if not security.verify_password(req.password, current_user.password_hash):
  356. raise HTTPException(status_code=401, detail="密码错误")
  357. # 2. Verify Captcha
  358. if not CaptchaService.verify_captcha(req.captcha_id, req.captcha_code):
  359. raise HTTPException(status_code=400, detail="验证码错误")
  360. user = db.query(User).filter(User.id == user_id).first()
  361. if not user:
  362. raise HTTPException(status_code=404, detail="用户不存在")
  363. old_role = user.role
  364. user.role = "SUPER_ADMIN"
  365. user.status = "ACTIVE" # Ensure they are active
  366. db.add(user)
  367. db.commit()
  368. db.refresh(user)
  369. # Log Operation
  370. LogService.create_log(
  371. db=db,
  372. operator_id=current_user.id,
  373. action_type=ActionType.CHANGE_ROLE,
  374. target_user_id=user.id,
  375. target_mobile=user.mobile,
  376. ip_address=request.client.host,
  377. details={"old": old_role, "new": "SUPER_ADMIN"}
  378. )
  379. return user
  380. @router.delete("/{user_id}", response_model=UserSchema, summary="删除用户")
  381. def delete_user(
  382. *,
  383. db: Session = Depends(deps.get_db),
  384. user_id: int,
  385. request: Request,
  386. current_user: User = Depends(deps.get_current_active_user),
  387. background_tasks: BackgroundTasks,
  388. ):
  389. """
  390. 软删除用户。仅限超级管理员。
  391. """
  392. if current_user.role != "SUPER_ADMIN":
  393. raise HTTPException(status_code=403, detail="权限不足")
  394. if user_id == current_user.id:
  395. raise HTTPException(status_code=400, detail="超级管理员不能删除自己")
  396. user = db.query(User).filter(User.id == user_id).first()
  397. if not user:
  398. raise HTTPException(status_code=404, detail="用户不存在")
  399. user.is_deleted = 1 # Using Integer 1 for True
  400. user.status = "DISABLED" # Also disable login
  401. db.add(user)
  402. db.commit()
  403. background_tasks.add_task(WebhookService.trigger_user_event, db, user.id, "DELETE")
  404. # Log Operation
  405. LogService.create_log(
  406. db=db,
  407. operator_id=current_user.id,
  408. action_type=ActionType.DELETE,
  409. target_user_id=user.id,
  410. target_mobile=user.mobile,
  411. ip_address=request.client.host,
  412. details={"status": "DISABLED"}
  413. )
  414. return user
  415. @router.get("/me", response_model=UserSchema, summary="获取当前用户信息")
  416. def read_user_me(
  417. current_user: User = Depends(deps.get_current_active_user),
  418. ):
  419. """
  420. 获取当前登录用户的信息。
  421. """
  422. return current_user
  423. @router.get("/{user_id}", response_model=UserSchema, summary="根据ID获取用户")
  424. def read_user_by_id(
  425. user_id: int,
  426. current_user: User = Depends(deps.get_current_active_user),
  427. db: Session = Depends(deps.get_db),
  428. ):
  429. """
  430. 根据用户ID获取特定用户信息。
  431. """
  432. user = db.query(User).filter(User.id == user_id).first()
  433. if not user:
  434. raise HTTPException(
  435. status_code=404,
  436. detail="该用户ID在系统中不存在",
  437. )
  438. return user