feat(auth): add wechat miniapp customer phone login

Implement Wechat Mini Program phone number login flow for end customers:
- add PhoneLoginIn Pydantic request schema
- create wechat.py module for Wechat API interactions and phone code exchange
- add customer JWT utilities and require_customer authentication dependency
- add new public API endpoints: /api/public/auth/phone-login and /api/public/auth/me
- add required environment config variables and update example .env
- add comprehensive test cases for the new auth flow and endpoints
This commit is contained in:
duanshuwen
2026-07-08 16:11:34 +08:00
parent fe85197e68
commit bac68842ef
8 changed files with 242 additions and 5 deletions

View File

@@ -2,15 +2,28 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import or_, select
from sqlalchemy.orm import Session, selectinload
from .shared import site_config
from ..auth import create_customer_token, require_customer
from ..database import get_db
from ..models import Destination, DestinationAlias, Lead, Product
from ..schemas import LeadCreateIn, ProductQuery, ProductStatus
from ..models import Customer, Destination, DestinationAlias, Lead, Product
from ..schemas import LeadCreateIn, PhoneLoginIn, ProductQuery, ProductStatus
from ..serializers import destination_dict, public_product_dict
from ..wechat import WechatApiError, WechatConfigError, exchange_phone_code
router = APIRouter()
def mask_phone(phone: str) -> str:
normalized = phone.strip()
if len(normalized) <= 7:
return "***"
return f"{normalized[:3]}****{normalized[-4:]}"
def public_customer_dict(customer: Customer) -> dict:
return {"id": customer.id, "phoneMasked": mask_phone(customer.phone)}
@router.get("/health")
def health():
return {"ok": True, "service": "miniapp-api"}
@@ -83,6 +96,30 @@ def list_destinations(db: Session = Depends(get_db)):
return {"items": [destination_dict(destination) for destination in destinations]}
@router.post("/api/public/auth/phone-login")
def phone_login(body: PhoneLoginIn, db: Session = Depends(get_db)):
try:
phone = exchange_phone_code(body.code)
except WechatConfigError as exc:
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)) from exc
except WechatApiError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
customer = db.scalar(select(Customer).where(Customer.phone == phone))
if not customer:
customer = Customer(phone=phone)
db.add(customer)
db.flush()
db.commit()
db.refresh(customer)
return {"token": create_customer_token(customer), "customer": public_customer_dict(customer)}
@router.get("/api/public/auth/me")
def current_customer(customer: Customer = Depends(require_customer)):
return public_customer_dict(customer)
@router.post("/api/public/leads", status_code=status.HTTP_201_CREATED)
def create_lead(body: LeadCreateIn, db: Session = Depends(get_db)):
lead = Lead(**body.model_dump(exclude_none=True), status="new")