257 lines
9.5 KiB
Python
257 lines
9.5 KiB
Python
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})
|