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

@@ -5,11 +5,12 @@ from fastapi.testclient import TestClient
from pydantic import ValidationError
from app import serializers
from app.auth import hash_password, require_admin
from app.auth import create_customer_token, hash_password, require_admin
from app.database import get_db
from app.main import create_app
from app.models import AdminUser, Campaign, CtaBanner, DemandFeatureCard, DemandForm, DemandHero, DemandRecommendation, DemandRecommendationProduct, Destination, DestinationAlias, DestinationHero, DestinationRegion, HeroSlide, HotelGroup, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard, VehicleOption
from app.models import AdminUser, Campaign, CtaBanner, Customer, DemandFeatureCard, DemandForm, DemandHero, DemandRecommendation, DemandRecommendationProduct, Destination, DestinationAlias, DestinationHero, DestinationRegion, HeroSlide, HotelGroup, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard, VehicleOption
from app.routers import admin as admin_router
from app.routers import public as public_router
from app.routers.admin import normalize_detail_sections, normalize_images
from app.routers.shared import site_config
from app.schemas import (
@@ -475,6 +476,65 @@ def test_public_leads_endpoint_accepts_date_only_and_returns_minimal_response():
assert fake_db.added[0].travelDate == datetime(2027, 1, 1)
def test_public_phone_login_creates_customer_and_returns_masked_session(monkeypatch):
monkeypatch.setattr(public_router, "exchange_phone_code", lambda code: "10000000000", raising=False)
fake_db = FakeDb(scalar_values=[None])
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
try:
response = TestClient(app).post("/api/public/auth/phone-login", json={"code": "phone-code"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
body = response.json()
assert body["token"]
assert body["customer"] == {"id": "customer-test-id", "phoneMasked": "100****0000"}
assert fake_db.added[0].phone == "10000000000"
assert fake_db.committed
def test_public_phone_login_requires_wechat_configuration(monkeypatch):
def missing_config(_code):
raise public_router.WechatConfigError("微信小程序登录未配置")
monkeypatch.setattr(public_router, "exchange_phone_code", missing_config, raising=False)
app = create_app()
app.dependency_overrides[get_db] = lambda: FakeDb()
try:
response = TestClient(app).post("/api/public/auth/phone-login", json={"code": "phone-code"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 503
assert response.json() == {"message": "微信小程序登录未配置"}
def test_public_me_returns_customer_for_customer_token():
customer = Customer(id="customer-test", phone="10000000000")
fake_db = FakeDb(get_result=customer)
app = create_app()
app.dependency_overrides[get_db] = lambda: fake_db
token = create_customer_token(customer)
try:
response = TestClient(app).get("/api/public/auth/me", headers={"Authorization": f"Bearer {token}"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json() == {"id": "customer-test", "phoneMasked": "100****0000"}
def test_public_me_requires_customer_token():
response = TestClient(create_app()).get("/api/public/auth/me")
assert response.status_code == 401
assert response.json() == {"message": "请先登录"}
def test_admin_destinations_endpoint_uses_precomputed_product_counts():
destination = make_destination()
fake_db = FakeDb(scalar_results=[[destination]], execute_results=[[(destination.id, 7)]])

View File

@@ -13,3 +13,15 @@ def test_oss_settings_are_loaded_from_prefixed_environment(monkeypatch):
assert settings.oss_access_key_secret == "example-access-key-secret"
assert settings.oss_endpoint == "oss-cn-example.aliyuncs.com"
assert settings.oss_bucket_name == "example-bucket"
def test_wechat_miniapp_settings_are_loaded_from_environment(monkeypatch):
monkeypatch.setenv("WECHAT_MINIAPP_APPID", "wx-test-appid")
monkeypatch.setenv("WECHAT_MINIAPP_SECRET", "test-secret")
monkeypatch.setenv("CUSTOMER_JWT_EXPIRES_HOURS", "24")
settings = Settings(_env_file=None)
assert settings.wechat_miniapp_appid == "wx-test-appid"
assert settings.wechat_miniapp_secret == "test-secret"
assert settings.customer_jwt_expires_hours == 24