from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from datetime import datetime from app.core.security import create_access_token from app.config import get_settings from app.db import get_db from app.deps import get_current_user from app.integrations.sms_client import SmsClient from app.integrations.wechat_client import WechatMiniClient, WechatMiniClientError from app.models import Permission, User from app.schemas.auth import ( CurrentUserResponse, LoginRequest, SmsCodeRequest, TokenResponse, WechatLoginRequest, ) from app.services.audit_service import write_audit from app.services.user_service import get_or_create_login_user router = APIRouter() def _build_current_user(db: Session, user: User) -> CurrentUserResponse: if user.is_superuser: permissions = sorted(db.scalars(select(Permission.code)).all()) else: permissions = sorted({ p.code for role in user.roles for p in role.permissions }) return CurrentUserResponse( id=user.id, phone=user.phone, name=user.name, job_title=user.job_title, unit_id=user.unit_id, unit_name=user.unit.name if user.unit else None, department_id=user.department_id, department_name=user.department.name if user.department else None, status=user.status, roles=[role.code for role in user.roles], permissions=permissions, is_superuser=user.is_superuser, ) @router.post("/sms-code") def send_sms_code(payload: SmsCodeRequest) -> dict: code = "123456" SmsClient().send_code(payload.phone, code) return {"message": "验证码已发送", "mock_code": code} @router.post("/login", response_model=TokenResponse) def login(payload: LoginRequest, db: Session = Depends(get_db)) -> TokenResponse: if payload.code != "123456": raise HTTPException(status_code=400, detail="验证码错误或已过期") user = get_or_create_login_user(db, payload.phone) if user.status != "active": raise HTTPException(status_code=403, detail="账号已禁用") write_audit(db, "auth.login", actor_id=user.id, target_type="user", target_id=str(user.id)) db.commit() db.refresh(user) return TokenResponse( access_token=create_access_token(str(user.id)), user=_build_current_user(db, user), ) @router.post("/wechat-login", response_model=TokenResponse) def wechat_login(payload: WechatLoginRequest, db: Session = Depends(get_db)) -> TokenResponse: """微信授权登录:拿手机号在用户表中匹配,找到则登录,找不到提示联系管理员。""" settings = get_settings() client = WechatMiniClient() phone: str | None = None if client.configured and payload.phone_code: try: phone = client.get_phone_number(payload.phone_code) except WechatMiniClientError as exc: raise HTTPException(status_code=400, detail=str(exc)) elif payload.phone: # 未配置微信凭据或前端未拿到 phone_code 时的兜底(开发/联调) phone = payload.phone.strip() elif not client.configured: # mock 模式且未传 phone:用配置里的测试手机号,方便本地联调 phone = settings.wx_mini_mock_phone if not phone: raise HTTPException(status_code=400, detail="未获取到微信手机号") # 可选:换 openid 做审计追踪,失败不阻断登录 if client.configured and payload.js_code: try: client.code2session(payload.js_code) except WechatMiniClientError: pass user = db.scalar(select(User).where(User.phone == phone)) if user is None: raise HTTPException(status_code=403, detail="未找到匹配账号,请联系管理员添加账号后使用") if user.status != "active": raise HTTPException(status_code=403, detail="账号已禁用") user.last_login_at = datetime.utcnow() write_audit(db, "auth.wechat_login", actor_id=user.id, target_type="user", target_id=str(user.id)) db.commit() db.refresh(user) return TokenResponse( access_token=create_access_token(str(user.id)), user=_build_current_user(db, user), ) @router.post("/logout") def logout(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)) -> dict: write_audit(db, "auth.logout", actor_id=current_user.id, target_type="user", target_id=str(current_user.id)) db.commit() return {"message": "已退出"} @router.get("/me", response_model=CurrentUserResponse) def me( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> CurrentUserResponse: return _build_current_user(db, current_user)