修改用户授权调用位置和逻辑

This commit is contained in:
张成
2025-11-18 08:00:38 +08:00
parent 4568e758a7
commit 988f57aa5e
12 changed files with 343 additions and 81 deletions

81
fetchUserInfo_analysis.md Normal file
View File

@@ -0,0 +1,81 @@
# fetchUserInfo 调用分析
## 调用位置汇总
### ✅ 合理的调用
1. **src/services/loginService.ts** (第160行)
- 登录成功后调用,确保用户信息同步到 store
- ✅ 合理
2. **src/main_pages/index.tsx** (第63行)
- 微信授权成功后调用
- ✅ 合理(主入口,需要确保用户信息加载)
3. **src/game_pages/list/index.tsx** (第228行)
- 在 useEffect 中,等待 waitForAuthInit 后调用
- ✅ 合理
4. **src/game_pages/detail/index.tsx** (第55行)
- 在 useEffect 中,等待 waitForAuthInit 后调用
- ✅ 合理
5. **src/game_pages/sharePoster/index.tsx** (第47行)
- 在 handleGenPoster 中,等待 waitForAuthInit 后调用
- ✅ 合理(需要用户信息生成海报)
6. **src/components/NTRPTestEntryCard/index.tsx** (第35行)
- 在 useEffect 中,等待 waitForAuthInit 后调用
- ✅ 合理
7. **src/utils/authInit.ts** (第29、39行)
- 在静默登录成功后调用
- ✅ 合理(核心授权逻辑)
8. **src/home_pages/index.tsx** (第20行)
- 在静默登录成功后调用
- ✅ 合理
### ⚠️ 可能重复的调用
1. **src/main_pages/components/ListPageContent.tsx** (第204行)
- 在 useEffect 中调用,等待 waitForAuthInit
- ⚠️ **问题**:这是 `main_pages/index.tsx` 的子组件
- `main_pages/index.tsx` 已经在授权成功后调用了 `fetchUserInfo`
- **建议**:移除这里的调用,因为父组件已经调用了
2. **src/other_pages/ntrp-evaluate/index.tsx - Intro组件** (第159、180行)
- 第159行在 useEffect 中检查 userInfo 为空时调用
- 第180行在 getLastResult 中检查 userInfo 为空时调用
- ⚠️ **问题**:两个地方都可能调用,有重复风险
- **建议**移除第159行的调用只在 getLastResult 中调用(因为已经等待了 waitForAuthInit
3. **src/other_pages/ntrp-evaluate/index.tsx - Result组件** (第463、475行)
- 第463行在 useEffect 中检查 userInfo 为空时调用
- 第475行在 init 中检查 userInfo 为空时调用
- ⚠️ **问题**:两个地方都可能调用,有重复风险
- **建议**移除第463行的调用只在 init 中调用(因为已经等待了 waitForAuthInit
## 优化建议
### 1. 移除重复调用
- `main_pages/components/ListPageContent.tsx` - 移除 fetchUserInfo 调用(父组件已调用)
- `ntrp-evaluate/index.tsx` - Intro 组件:移除第一个 useEffect 中的调用
- `ntrp-evaluate/index.tsx` - Result 组件:移除第一个 useEffect 中的调用
### 2. 调用原则
- ✅ 主入口页面main_pages/index.tsx应该在授权成功后调用
- ✅ 子组件不应该重复调用,应该依赖父组件或 store 中的数据
- ✅ 独立页面(如 game_pages/*)可以调用,但应该等待 waitForAuthInit
- ✅ 工具函数authInit.ts中的调用是必要的
## 总结
**当前问题**
1. `main_pages/components/ListPageContent.tsx` 与父组件重复调用
2. `ntrp-evaluate/index.tsx` 中 Intro 和 Result 组件都有重复调用
**建议修复**
- 移除子组件中的重复调用
- 统一在等待 waitForAuthInit 后的逻辑中调用

View File

@@ -5,8 +5,7 @@ import "./app.scss";
import "./scss/qweather-icons/qweather-icons.css";
import { useGlobalStore } from "./store/global";
import { removeStorage } from "./store/storage";
import { sceneRedirectLogic } from './utils/helper'
import { silentLogin } from "./services/loginService";
import { sceneRedirectLogic } from './utils/helper';
interface AppProps {
children: ReactNode;
@@ -26,8 +25,8 @@ class App extends Component<AppProps> {
resolve({ event: 'agree' }); // 同意隐私协议
});
// 移除这里的静默登录调用,避免重复调用
// 静默登录在 home_pages/index.tsx统一执行
// 微信授权逻辑已转移到 main_pages/index.tsx 中
// 不再在 app.ts 中执行静默登录
}
componentDidMount() {
@@ -40,6 +39,7 @@ class App extends Component<AppProps> {
componentDidShow(options) {
sceneRedirectLogic(options.query, options.path);
// 微信授权逻辑已转移到 main_pages/index.tsx 中
}
componentDidHide() { }

View File

@@ -4,6 +4,7 @@ import Taro from "@tarojs/taro";
import { useUserInfo, useUserActions } from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils";
import evaluateService, { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
import ArrowRight from "@/static/ntrp/ntrp_arrow_right_color.svg";
import {
@@ -26,12 +27,18 @@ function NTRPTestEntryCard(props: {
console.log(userInfo);
useEffect(() => {
const init = async () => {
// 先等待静默登录完成
await waitForAuthInit();
// 然后再获取用户信息
if (!userInfo.id) {
fetchUserInfo();
await fetchUserInfo();
}
evaluateService.getLastResult().then((res) => {
// 获取测试结果
const res = await evaluateService.getLastResult();
setTestFlag(res.code === 0 && res.data.has_ntrp_level);
});
};
init();
}, [userInfo.id]);
const handleTest = useCallback(

View File

@@ -7,6 +7,7 @@ import OrderService from "@/services/orderService";
import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore";
import { MATCH_STATUS, IsSubstituteSupported } from "@/services/detailService";
import { GameManagePopup, NTRPEvaluatePopup } from "@/components";
import { useUserInfo } from "@/store/userStore";
import img from "@/config/images";
import RMB_ICON from "@/static/detail/rmb.svg";
import { toast, navto } from "@/utils/helper";
@@ -76,6 +77,7 @@ export default function StickyButton(props) {
currentUserInfo,
} = props;
const [commentCount, setCommentCount] = useState(0);
const userInfo = useUserInfo();
const ntrpRef = useRef<{
show: (evaluateCallback: EvaluateCallback) => void;
}>({ show: () => {} });
@@ -93,6 +95,36 @@ export default function StickyButton(props) {
const { ntrp_level } = currentUserInfo || {};
// 检查手机号绑定的包装函数
const checkPhoneAndExecute = (action: () => void) => {
return () => {
if (!userInfo?.phone) {
Taro.showModal({
title: '提示',
content: '该功能需要绑定手机号',
confirmText: '去绑定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const currentPath = Taro.getCurrentInstance().router?.path || '';
const currentParams = Taro.getCurrentInstance().router?.params || {};
const queryString = Object.keys(currentParams)
.map(key => `${key}=${currentParams[key]}`)
.join('&');
const fullPath = queryString ? `${currentPath}?${queryString}` : currentPath;
Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent(fullPath)}`,
});
}
}
});
return;
}
action();
};
};
const matchNtrpReq = matchNtrpRequestment(
ntrp_level,
skill_level_min,
@@ -224,14 +256,14 @@ export default function StickyButton(props) {
<Text className={styles.btnText}></Text>
</>
),
action: async () => {
action: checkPhoneAndExecute(async () => {
const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) {
navto(
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
);
}
},
}),
};
} else if (!matchNtrpReq) {
return {
@@ -255,7 +287,7 @@ export default function StickyButton(props) {
<Text className={styles.btnText}></Text>
</>
),
action: handleJoinGame,
action: checkPhoneAndExecute(handleJoinGame),
};
} else if (can_join) {
return {
@@ -268,7 +300,7 @@ export default function StickyButton(props) {
</>
);
},
action: handleJoinGame,
action: checkPhoneAndExecute(handleJoinGame),
};
} else if (can_assess) {
return {
@@ -279,7 +311,7 @@ export default function StickyButton(props) {
<Text className={styles.btnText}></Text>
</>
),
action: handleSelfEvaluate,
action: checkPhoneAndExecute(handleSelfEvaluate),
};
}
return {

View File

@@ -10,6 +10,7 @@ import * as LoginService from "@/services/loginService";
import { getCurrentLocation } from "@/utils/locationUtils";
import { useUserInfo, useUserActions } from "@/store/userStore";
import { useGlobalState } from "@/store/global";
import { waitForAuthInit } from "@/utils/authInit";
import { requireLoginWithPhone } from "@/utils/helper";
import GameTags from "./components/GameTags";
import Carousel from "./components/Carousel";
@@ -46,8 +47,14 @@ function Index() {
const commentRef = useRef();
useEffect(() => {
const init = async () => {
updateLocation();
fetchUserInfo();
// 先等待静默登录完成
await waitForAuthInit();
// 然后再获取用户信息
await fetchUserInfo();
};
init();
}, []);
useDidShow(() => {

View File

@@ -16,6 +16,7 @@ import { updateUserLocation } from "@/services/userService";
import { useUserActions } from "@/store/userStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
import { saveImage } from "@/utils";
import { waitForAuthInit } from "@/utils/authInit";
const ListPage = () => {
// 从 store 获取数据和方法
@@ -220,11 +221,14 @@ const ListPage = () => {
getCities();
getCityQrCode();
// 2. 延迟执行:获取用户信息(不阻塞渲染)
requestAnimationFrame(() => {
fetchUserInfo().catch((error) => {
// 2. 延迟执行:等待静默登录完成后获取用户信息
requestAnimationFrame(async () => {
try {
await waitForAuthInit();
await fetchUserInfo();
} catch (error) {
console.error('获取用户信息失败:', error);
});
}
});
// 3. 延迟执行:获取位置信息(可能较慢,不阻塞首屏)

View File

@@ -14,6 +14,7 @@ import WechatTimeline from "@/static/detail/wechat_timeline.svg";
import { useUserActions } from "@/store/userStore";
import { DayOfWeekMap } from "../detail/config";
import { genNTRPRequirementText } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import styles from "./index.module.scss";
function SharePoster(props) {
@@ -41,6 +42,8 @@ function SharePoster(props) {
image_list,
title,
} = detail || {};
// 先等待静默登录完成
await waitForAuthInit();
const userInfo = await fetchUserInfo();
const { avatar_url, nickname } = userInfo;
const startTime = dayjs(start_time);

View File

@@ -9,7 +9,6 @@ import { View, Image, Text, ScrollView } from "@tarojs/components";
import ListContainer from "@/container/listContainer";
import DistanceQuickFilter from "@/components/DistanceQuickFilter";
import { updateUserLocation } from "@/services/userService";
import { useUserActions } from "@/store/userStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
import { saveImage, navigateTo } from "@/utils";
@@ -35,7 +34,6 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
onFilterPopupVisibleChange,
}) => {
const store = useListStore() || {};
const { fetchUserInfo } = useUserActions();
const { statusNavbarHeightInfo, getCurrentLocationInfo } =
useGlobalState() || {};
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
@@ -196,12 +194,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
getCities();
getCityQrCode();
// 2. 延迟执行:获取用户信息(不阻塞渲染)
requestAnimationFrame(() => {
fetchUserInfo().catch((error) => {
console.error('获取用户信息失败:', error);
});
});
// 2. 移除 fetchUserInfo 调用,因为父组件 main_pages/index.tsx 已经在授权成功后调用了
// 这里直接使用 store 中的用户信息即可
// 3. 延迟执行:获取位置信息(可能较慢,不阻塞首屏)
requestAnimationFrame(() => {

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect, useCallback } from "react";
import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import { check_login_status, silentLogin } from "@/services/loginService";
import { wechat_auth_login, save_login_state } from "@/services/loginService";
import { useUserActions } from "@/store/userStore";
import tokenManager from "@/utils/tokenManager";
import GuideBar from "@/components/GuideBar";
import { GeneralNavbar } from "@/components";
import HomeNavbar from "@/components/HomeNavbar";
@@ -24,41 +25,66 @@ const MainPage: React.FC = () => {
const [isShowInputCustomerNavBar, setIsShowInputCustomerNavBar] = useState(false);
const [listPageScrollToTopTrigger, setListPageScrollToTopTrigger] = useState(0);
const [showGuideBar, setShowGuideBar] = useState(true);
const [showAuthError, setShowAuthError] = useState(false);
const [authErrorMessage, setAuthErrorMessage] = useState('');
const { fetchUserInfo } = useUserActions();
// 初始化:尝试静默登录并获取用户信息
// 初始化:自动微信授权并获取用户信息
useEffect(() => {
const init = async () => {
// 先检查是否已登录
const login_status = check_login_status();
if (login_status) {
// 已登录,获取用户信息
const hasValidToken = tokenManager.hasValidToken();
if (!hasValidToken) {
try {
console.log('开始微信授权...');
const loginRes = await wechat_auth_login();
if (loginRes.success && loginRes.token) {
save_login_state(loginRes.token, loginRes.user_info);
console.log('微信授权成功');
} else {
// 显示错误提示
setAuthErrorMessage(loginRes.message || '微信授权失败');
setShowAuthError(true);
return;
}
} catch (error) {
console.error('微信授权异常:', error);
setAuthErrorMessage('微信授权失败,请重试');
setShowAuthError(true);
return;
}
}
// 如果有有效token获取用户详细信息
if (tokenManager.hasValidToken()) {
try {
await fetchUserInfo();
} catch (error) {
console.error('获取用户信息失败:', error);
}
} else {
// 未登录,尝试静默登录
try {
const loginResult = await silentLogin();
if (loginResult.success) {
// 静默登录成功,获取用户信息
fetchUserInfo().catch((error) => {
console.error('获取用户信息失败:', error);
});
}
} catch (error) {
console.error('静默登录失败:', error);
// 静默登录失败不影响使用
}
}
};
init();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // fetchUserInfo 是稳定的函数引用,不需要加入依赖项
// 显示授权错误提示
useEffect(() => {
if (showAuthError) {
Taro.showModal({
title: '授权失败',
content: `${authErrorMessage}\n\n请重启小程序后重试`,
showCancel: false,
confirmText: '我知道了',
success: () => {
setShowAuthError(false);
}
});
}
}, [showAuthError, authErrorMessage]);
// 处理标签切换
const handleTabChange = useCallback((code: string) => {
if (code === currentTab) {

View File

@@ -15,6 +15,7 @@ import { useEvaluate, EvaluateScene } from "@/store/evaluateStore";
import { useGlobalState } from "@/store/global";
import { delay, getCurrentFullPath } from "@/utils";
import { formatNtrpDisplay } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg";
@@ -151,16 +152,14 @@ function Intro() {
const { ntrp_level, create_time, id } = last_test_result || {};
const lastTestTime = create_time ? dayjs(create_time).format("YYYY年M月D日") : "";
// 组件初始化时立即获取用户信息
useEffect(() => {
// 如果用户信息为空,立即获取
if (!userInfo || Object.keys(userInfo).length === 0) {
fetchUserInfo();
}
}, []);
useEffect(() => {
getLastResult();
const init = async () => {
// 先等待静默登录完成
await waitForAuthInit();
// 然后再调用接口
await getLastResult();
};
init();
}, []);
async function getLastResult() {
@@ -193,8 +192,7 @@ function Intro() {
});
}
Taro.redirectTo({
url: `/other_pages/ntrp-evaluate/index?stage=${type}${
type === StageType.RESULT ? `&id=${id}` : ""
url: `/other_pages/ntrp-evaluate/index?stage=${type}${type === StageType.RESULT ? `&id=${id}` : ""
}`,
});
}
@@ -450,20 +448,18 @@ function Result() {
[propName: string, prop: number][]
>([]);
// 组件初始化时立即获取用户信息
useEffect(() => {
// 如果用户信息为空,立即获取
if (!userInfo || Object.keys(userInfo).length === 0) {
fetchUserInfo();
}
}, []);
useEffect(() => {
getResultById();
const init = async () => {
// 先等待静默登录完成
await waitForAuthInit();
// 然后再调用接口
await getResultById();
// 确保用户信息已加载
if (!userInfo || Object.keys(userInfo).length === 0) {
fetchUserInfo();
await fetchUserInfo();
}
};
init();
}, [id]);
async function getResultById() {
@@ -624,8 +620,7 @@ function Result() {
}
const currentPage = getCurrentFullPath();
Taro.redirectTo({
url: `/login_pages/index/index${
currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
url: `/login_pages/index/index${currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
}`,
});
}

View File

@@ -80,6 +80,7 @@ export interface UserInfoType {
}
// 微信授权登录
// phone_code: 可选参数,如果提供则绑定手机号,否则只进行微信授权
export const wechat_auth_login = async (
phone_code?: string,
): Promise<LoginResponse> => {
@@ -94,11 +95,18 @@ export const wechat_auth_login = async (
};
}
// 使用 httpService 调用微信授权接口传递手机号code
const auth_response = await httpService.post("user/wx_auth", {
// 构建请求参数
const requestData: any = {
code: login_result.code,
phone_code: phone_code, // 传递手机号加密code
});
};
// 只有在提供 phone_code 时才添加到请求参数中
if (phone_code) {
requestData.phone_code = phone_code;
}
// 使用 httpService 调用微信授权接口
const auth_response = await httpService.post("user/wx_auth", requestData);
if (auth_response.code === 0) {
return {

105
src/utils/authInit.ts Normal file
View File

@@ -0,0 +1,105 @@
import { check_login_status, get_user_token, silentLogin } from "@/services/loginService";
import { useUser } from "@/store/userStore";
import tokenManager from "@/utils/tokenManager";
// 防止重复调用静默登录
let isInitializingAuth = false;
let authInitPromise: Promise<void> | null = null;
// 初始化用户授权的核心逻辑
const initUserAuthCore = async (): Promise<void> => {
// 如果正在初始化,直接返回现有的 Promise
if (isInitializingAuth && authInitPromise) {
return authInitPromise;
}
// 如果已经登录且有用户信息,跳过
const login_status = check_login_status();
const { user, fetchUserInfo } = useUser.getState();
if (login_status && user && Object.keys(user).length > 0) {
return Promise.resolve();
}
isInitializingAuth = true;
authInitPromise = (async () => {
try {
if (login_status) {
// 已登录,获取用户信息
try {
await fetchUserInfo();
} catch (error) {
console.error('获取用户信息失败:', error);
}
} else {
// 未登录,尝试静默登录
try {
const loginResult = await silentLogin();
if (loginResult.success) {
// 静默登录成功,获取用户信息
await fetchUserInfo();
}
} catch (error) {
console.error('静默登录失败:', error);
// 静默登录失败不影响使用
}
}
} catch (error) {
console.error('初始化用户授权失败:', error);
} finally {
isInitializingAuth = false;
authInitPromise = null;
}
})();
return authInitPromise;
};
// 导出等待静默登录完成的函数,供页面组件使用
export const waitForAuthInit = async (): Promise<void> => {
// 检查是否已经有有效的 token最优先检查
if (tokenManager.hasValidToken() || get_user_token()) {
return Promise.resolve();
}
// 如果正在初始化,等待完成
if (authInitPromise) {
await authInitPromise;
// 等待完成后再次检查 token
if (tokenManager.hasValidToken() || get_user_token()) {
return;
}
}
// 检查是否已经登录且有用户信息
const login_status = check_login_status();
const { user } = useUser.getState();
if (login_status && user && Object.keys(user).length > 0) {
// 即使有登录状态,也确保 token 存在
if (tokenManager.hasValidToken() || get_user_token()) {
return Promise.resolve();
}
}
// 触发初始化
const promise = initUserAuthCore();
await promise;
// 等待 token 真正存在(最多等待 2 秒)
let retryCount = 0;
const maxRetries = 20; // 20 * 100ms = 2秒
while (retryCount < maxRetries && !tokenManager.hasValidToken() && !get_user_token()) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
// 如果还是没有 token记录警告但继续执行
if (!tokenManager.hasValidToken() && !get_user_token()) {
console.warn('等待静默登录完成,但未获取到 token');
}
};
// 导出初始化函数供 app.ts 使用
export const initUserAuth = () => {
return initUserAuthCore();
};