diff --git a/backend/app/config.py b/backend/app/config.py index f75fc0e..0e82fae 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -32,7 +32,11 @@ class Settings(BaseSettings): max_file_size: int = 10 * 1024 * 1024 # 10MB # CORS - cors_origins: str = "*" # 多个域名用逗号分隔,如 https://a.com,https://b.com + cors_origins: str = "*" + + # CRMEB internal API + crmeb_base_url: str = "" + crmeb_api_key: str = "" # Server host: str = "0.0.0.0" diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 73e021e..5dee083 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -5,6 +5,7 @@ import os from fastapi import APIRouter, HTTPException from jose import jwt +import httpx from pydantic import BaseModel from app.config import settings @@ -101,6 +102,57 @@ async def params_login(req: ParamsLoginRequest) -> TokenResponse: ) +class OpenidRequest(BaseModel): + openid: str + + +@router.post("/openid-login") +async def openid_login(req: OpenidRequest) -> TokenResponse: + """仅传 openid → 调 CRMEB 获取用户信息 → 创建/更新 User → 返回 token""" + if not settings.crmeb_base_url or not settings.crmeb_api_key: + raise HTTPException(status_code=500, detail="CRMEB 未配置") + + async with httpx.AsyncClient() as client: + resp = await client.get( + f"{settings.crmeb_base_url}/api/internal/user-info", + params={"openid": req.openid}, + headers={"X-Internal-Key": settings.crmeb_api_key}, + timeout=10, + ) + data = resp.json() + + if data.get("code") != 200: + raise HTTPException(status_code=400, detail="获取用户信息失败") + + info = data["data"] + + # 查找或创建用户 + user = await User.find_one(User.openid == req.openid) + if not user: + user = User( + openid=req.openid, + name=info.get("nickname", "") or "", + phone=info.get("phone") or None, + avatar_url=info.get("avatar") or None, + ) + await user.insert() + else: + user.name = info.get("nickname", "") or user.name + if info.get("phone"): + user.phone = info["phone"] + if info.get("avatar"): + user.avatar_url = info["avatar"] + await user.save() + + token = _build_token(user) + return TokenResponse( + token=token, + user_id=str(user.id), + openid=user.openid, + mobile=user.phone or "", + ) + + @router.post("/dev-login") async def dev_login() -> TokenResponse: """开发环境:使用固定测试账号,不依赖微信""" diff --git a/frontend/src/app/context/UserContext.tsx b/frontend/src/app/context/UserContext.tsx index ca61f56..93e0015 100644 --- a/frontend/src/app/context/UserContext.tsx +++ b/frontend/src/app/context/UserContext.tsx @@ -12,6 +12,7 @@ export interface UserProfile { name: string; age: number | null; phone: string; + avatar_url: string | null; lastPeriodDate: string; // YYYY-MM-DD dueDate: string; currentWeek: number; @@ -35,6 +36,7 @@ const DEFAULT_PROFILE: UserProfile = { name: "", age: null, phone: "", + avatar_url: null, lastPeriodDate: "", dueDate: "", currentWeek: 0, @@ -154,6 +156,7 @@ function mapDtoToProfile(dto: UserProfileDTO): UserProfile { name: dto.name || "", age: dto.age, phone: dto.phone || "", + avatar_url: dto.avatar_url || null, lastPeriodDate: lmpDate, dueDate, currentWeek, diff --git a/frontend/src/app/pages/LoginPage.tsx b/frontend/src/app/pages/LoginPage.tsx index 43a3190..19528b3 100644 --- a/frontend/src/app/pages/LoginPage.tsx +++ b/frontend/src/app/pages/LoginPage.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router"; import { motion } from "motion/react"; import { Heart } from "lucide-react"; import { useUser } from "../context/UserContext"; -import { devLogin, createDummyToken, updateUserProfile } from "../services/api"; +import { devLogin, fetchApi } from "../services/api"; import { debugLog, clearDebugLogs } from "../services/debug"; export function LoginPage() { @@ -20,39 +20,23 @@ export function LoginPage() { const openidParam = params.get('openid'); if (openidParam && openidParam.trim()) { - const uid = parseInt(params.get('uid') || '0'); - const phone = params.get('phone') || ''; - const token = createDummyToken(uid, openidParam, phone); - debugLog(`dummy token: ${token.substring(0, 50)}...`); - - try { - const payload = JSON.parse(atob(token.split('.')[1])); - debugLog(`payload: uid=${payload.uid}, openid=${payload.openid}, phone=${payload.phone}`); - } catch (e: any) { - debugLog(`decode error: ${e.message}`); - } - - setTokenAndUser(token, { id: String(uid), openid: openidParam, phone }); - debugLog(`setTokenAndUser: id=${uid}, openid=${openidParam}, phone=${phone}`); - - // 同步昵称和头像到后端 - const nameParam = params.get('name'); - const avatarParam = params.get('avatar'); - if (nameParam || avatarParam) { - const updateData: Record = {}; - if (nameParam) updateData.name = decodeURIComponent(nameParam); - if (avatarParam) updateData.avatar_url = decodeURIComponent(avatarParam); - updateUserProfile(updateData).then(() => { - debugLog(`synced: name=${nameParam}, avatar=${(avatarParam || '').substring(0, 30)}...`); - }).catch((e) => debugLog(`sync error: ${e.message}`)); - } - - fetchProfile().then((dto) => { - if (dto) debugLog(`fetchProfile: name=${dto.name}, phone=${dto.phone}, onboarded=${dto.is_onboarded}`); - navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); + debugLog(`openid: ${openidParam}`); + fetchApi("/auth/openid-login", { + method: "POST", + body: JSON.stringify({ openid: openidParam }), + }).then((res: any) => { + debugLog(`openid-login OK: id=${res.user_id}, phone=${res.mobile}`); + setTokenAndUser(res.token, { id: res.user_id, openid: res.openid, phone: res.mobile }); + fetchProfile().then((dto) => { + if (dto) debugLog(`fetchProfile: name=${dto.name}, phone=${dto.phone}, onboarded=${dto.is_onboarded}`); + navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); + }).catch((e: any) => { + debugLog(`fetchProfile error: ${e?.message}`); + setError("登录已失效,请在小程序重新进入"); + }); }).catch((e: any) => { - debugLog(`fetchProfile error: ${e?.message}`); - setError("登录已失效,请在小程序重新进入"); + debugLog(`openid-login error: ${e?.message}`); + setError("登录失败: " + (e?.message || "")); }); return; } diff --git a/frontend/src/app/services/api.ts b/frontend/src/app/services/api.ts index bce78ac..5d1b6e7 100644 --- a/frontend/src/app/services/api.ts +++ b/frontend/src/app/services/api.ts @@ -26,8 +26,8 @@ export function setToken(token: string) { export function clearToken() { localStorage.removeItem(TOKEN_KEY); } - -async function fetchApi(path: string, options: RequestInit = {}): Promise { +/** 通用 API 请求封装 */ +export async function fetchApi(path: string, options: RequestInit = {}): Promise { const token = getToken(); const headers: Record = { "Content-Type": "application/json",