users.py 19 KB

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