feat: openid-login + avatar_url 修复

- 后端: 新增 /auth/openid-login 接口(调 CRMEB 补齐用户信息)
- 后端: config.py 新增 crmeb_base_url / crmeb_api_key 配置
- 后端: api.ts 新增 openid-login 接口声明
- 前端: LoginPage 改为走 openid-login(openid 单参登录)
- 前端: UserContext 修复 avatar_url 未映射导致头像不显示的问题
This commit is contained in:
2026-05-27 22:24:14 +08:00
parent bceb4f4619
commit 96a938e11b
5 changed files with 79 additions and 36 deletions

View File

@@ -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,

View File

@@ -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<string, unknown> = {};
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;
}

View File

@@ -26,8 +26,8 @@ export function setToken(token: string) {
export function clearToken() {
localStorage.removeItem(TOKEN_KEY);
}
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
/** 通用 API 请求封装 */
export async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
"Content-Type": "application/json",