users.py 18 KB

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