from fastapi import APIRouter, Depends, HTTPException, status from app.controllers.app_user import app_user_controller from app.schemas.app_user import ( AppUserRegisterSchema, AppUserLoginSchema, AppUserJWTOut, AppUserInfoOut, AppUserUpdateSchema, AppUserChangePasswordSchema, AppUserDashboardOut, AppUserQuotaOut, ) from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut from app.schemas.base import BasicResponse, MessageOut, Success from app.utils.app_user_jwt import ( create_app_user_access_token, get_current_app_user, ACCESS_TOKEN_EXPIRE_MINUTES, verify_app_user_token ) from app.models.user import AppUser from app.controllers.user_valuation import user_valuation_controller from app.controllers.invoice import invoice_controller from app.core.token_blacklist import add_to_blacklist from fastapi import Header from pydantic import BaseModel, Field from typing import Optional import time from app.models.valuation import ValuationAssessment from app.services.sms_store import store from app.settings import settings router = APIRouter() @router.post("/register", response_model=BasicResponse[dict], summary="用户注册") async def register( register_data: AppUserRegisterSchema ): """ 用户注册 - 只需要手机号 默认密码为手机号后六位 """ try: user = await app_user_controller.register(register_data) return Success(data={ "user_id": user.id, "phone": user.phone, "default_password": register_data.phone[-6:] }) except Exception as e: raise HTTPException(status_code=200, detail=str(e)) @router.post("/login", response_model=BasicResponse[dict], summary="用户登录") async def login( login_data: AppUserLoginSchema ): """ 用户登录 """ user = await app_user_controller.authenticate(login_data) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="手机号或密码错误" ) # 更新最后登录时间 await app_user_controller.update_last_login(user.id) # 生成访问令牌 access_token = create_app_user_access_token(user.id, user.phone) return Success(data={ "access_token": access_token, "token_type": "bearer", "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60 }) @router.post("/logout", summary="用户登出", response_model=BasicResponse[dict]) async def logout(current_user: AppUser = Depends(get_current_app_user)): """ 用户登出(客户端需要删除本地token) """ return Success(data={"message": "登出成功"}) class DeleteAccountRequest(BaseModel): code: Optional[str] = Field(None, description="短信验证码或绕过码") @router.delete("/account", summary="注销用户信息", response_model=BasicResponse[dict]) async def delete_account(current_user: AppUser = Depends(get_current_app_user), token: str = Header(None), payload: Optional[DeleteAccountRequest] = None): if payload and payload.code: if settings.SMS_BYPASS_CODE and payload.code == settings.SMS_BYPASS_CODE: store.mark_verified(current_user.phone) else: ok, reason = store.can_verify(current_user.phone) if not ok: raise HTTPException(status_code=423, detail=str(reason)) record = store.get_code(current_user.phone) if not record: raise HTTPException(status_code=400, detail="验证码已过期") code_stored, expires_at = record if time.time() > expires_at: store.clear_code(current_user.phone) raise HTTPException(status_code=400, detail="验证码已过期") if payload.code != code_stored: count, locked = store.record_verify_failure(current_user.phone) if locked: raise HTTPException(status_code=423, detail="尝试次数过多,已锁定") raise HTTPException(status_code=401, detail="验证码错误") store.clear_code(current_user.phone) store.reset_failures(current_user.phone) store.mark_verified(current_user.phone) else: if not store.is_recently_verified(current_user.phone): raise HTTPException(status_code=403, detail="请先完成手机号验证码验证") remaining_quota = int(getattr(current_user, "remaining_quota", 0) or 0) if remaining_quota > 0: raise HTTPException(status_code=400, detail="当前剩余估值次数大于0,无法注销账号") ok = await app_user_controller.delete_user_account(current_user.id) if token: payload = verify_app_user_token(token) exp = getattr(payload, "exp", None) if payload else None await add_to_blacklist(token, current_user.id, exp) if not ok: raise HTTPException(status_code=404, detail="用户不存在") return Success(data={"message": "账号已注销"}) @router.get("/profile", response_model=BasicResponse[dict], summary="获取用户信息") async def get_profile(current_user: AppUser = Depends(get_current_app_user)): """ 获取当前用户信息 """ user_info = AppUserInfoOut( id=current_user.id, phone=current_user.phone, nickname=getattr(current_user, "alias", None), avatar=None, company_name=current_user.company_name, company_address=current_user.company_address, company_contact=current_user.company_contact, company_phone=current_user.company_phone, company_email=current_user.company_email, is_active=current_user.is_active, last_login=current_user.last_login, created_at=current_user.created_at, updated_at=current_user.updated_at, remaining_quota=current_user.remaining_quota, ) return Success(data=user_info.model_dump()) @router.get("/dashboard", response_model=BasicResponse[dict], summary="用户首页摘要") async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)): """ 用户首页摘要 功能: - 返回剩余估值次数(暂以 0 占位,后续可接入配额系统) - 返回最近一条估值评估记录(若有) - 返回待处理发票数量 """ # 最近估值记录 latest = await user_valuation_controller.model.filter(user_id=current_user.id).order_by("-created_at").first() latest_out = None if latest: latest_out = { "id": latest.id, "asset_name": latest.asset_name, "valuation_result": latest.final_value_ab, "status": latest.status, "created_at": latest.created_at.isoformat() if latest.created_at else "", } # 待处理发票数量 try: pending_invoices = await invoice_controller.count_pending_for_user(current_user.id) except Exception: pending_invoices = 0 # 剩余估值次数 remaining_quota = current_user.remaining_quota return Success(data={"remaining_quota": remaining_quota, "latest_valuation": latest_out, "pending_invoices": pending_invoices}) @router.get("/quota", response_model=BasicResponse[dict], summary="剩余估值次数") async def get_quota(current_user: AppUser = Depends(get_current_app_user)): """ 剩余估值次数查询 说明: - 当前实现返回默认 0 次与用户类型占位 - 若后续接入配额系统,可从数据库中读取真实值 """ remaining_count = current_user.remaining_quota return Success(data={"remaining_count": remaining_count}) @router.put("/profile", response_model=BasicResponse[dict], summary="更新用户信息") async def update_profile( update_data: AppUserUpdateSchema, current_user: AppUser = Depends(get_current_app_user) ): """ 更新用户信息 """ updated_user = await app_user_controller.update_user_info(current_user.id, update_data) if not updated_user: raise HTTPException(status_code=404, detail="用户不存在") user_info = AppUserInfoOut( id=updated_user.id, phone=updated_user.phone, nickname=getattr(updated_user, "alias", None), avatar=None, company_name=updated_user.company_name, company_address=updated_user.company_address, company_contact=updated_user.company_contact, company_phone=updated_user.company_phone, company_email=updated_user.company_email, is_active=updated_user.is_active, last_login=updated_user.last_login, created_at=updated_user.created_at, updated_at=updated_user.updated_at, remaining_quota=updated_user.remaining_quota, ) return Success(data=user_info.model_dump()) @router.post("/change-password", summary="修改密码", response_model=BasicResponse[dict]) async def change_password( password_data: AppUserChangePasswordSchema, current_user: AppUser = Depends(get_current_app_user) ): """ 修改密码 """ success = await app_user_controller.change_password( current_user.id, password_data.old_password, password_data.new_password ) if not success: raise HTTPException(status_code=400, detail="原密码错误") return Success(data={"message": "密码修改成功"}) @router.get("/validate-token", summary="验证token", response_model=BasicResponse[dict]) async def validate_token(current_user: AppUser = Depends(get_current_app_user)): """ 验证token是否有效 """ return Success(data={"user_id": current_user.id, "phone": current_user.phone})