guzhi/app/api/v1/app_users/app_users.py

257 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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})