Compare commits

...

3 Commits

Author SHA1 Message Date
7ea0406336 feat(passport-invite): 优化新用户扫码邀请加入流程
- 扫码进入邀请页面时保存邀请 token 到本地存储
- 新增登录状态检测,未登录用户点击快速加入时保存邀请信息并跳转登录页
- 登录成功后返回邀请页自动执行加入应用操作,提升用户体验
- 支持已登录用户直接加入应用,简化流程
- 增加清理 pending 邀请数据的方法,防止数据残留
- 监听页面显示事件,处理登录返回后自动操作
- 调整接口请求逻辑,确保携带最新的邀请和授权数据
- 更新项目配置,注册邀请页面路径信息
2026-04-11 23:33:55 +08:00
371b478e33 fix(passport): 优化邀请码接受失败的用户提示和处理
- 针对“用户不存在”、“用户创建失败”及“请先登录”提示,改为弹窗提示需登录
- 在弹窗中增加“去登录”按钮,点击后跳转到登录页面,并携带重定向token参数
- 其他错误仍使用原Toast提示方式展示错误信息
- 保留错误日志打印,便于排查异常情况
2026-04-11 23:21:39 +08:00
7d6c4ea3c6 fix(passport): 添加邀请接受接口调试日志
- 在接受邀请请求前打印token和请求URL、参数
- 在请求响应后输出接口响应及返回数据
- 在调用失败时打印错误信息及异常日志
- 保持请求流程和状态管理不变,增强调试能力
2026-04-11 20:16:51 +08:00
4 changed files with 190 additions and 9 deletions

View File

@@ -22,7 +22,18 @@
"usedAt": 1775908159660, "usedAt": 1775908159660,
"industryId": "all" "industryId": "all"
} }
],
"f2494c1730eb411aac709ec2751d60fc": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775921148885,
"industryId": "all"
}
] ]
}, },
"lastUpdated": 1775909408190 "lastUpdated": 1775921440281
} }

View File

@@ -0,0 +1,44 @@
# 2026-04-11 工作记录
## 任务:修复邀请加入应用流程
### 问题描述
`passport/invite/index` 页面在新用户扫码加入应用时存在问题:
- 新用户点击"微信手机号快速加入"时,如果未注册,后端返回"用户不存在"
- 页面引导用户去登录,但登录完成后**没有自动执行加入应用的操作**
### 解决方案
参考 `UserCard.tsx``phone-auth/index.tsx` 的实现模式,修改了 `src/passport/invite/index.tsx`
#### 1. 新增功能
- 页面加载时保存邀请 token 到本地存储
- 检测用户登录状态
- 未登录用户点击加入时,保存邀请信息并引导到 `phone-auth` 登录页
- 登录成功后返回邀请页面,自动执行加入操作
- 新增 `clearPendingInviteData` 方法清理临时数据
#### 2. 关键修改点
- 使用 `saveInviteParams` 保存邀请信息
- 使用 `pending_invite_*` 系列 storage key 保存待处理的邀请数据
- 页面显示时通过 `AppShow` 事件监听登录返回
- 登录成功后自动调用 `handleJoinApp` 完成加入
#### 3. 流程优化
```
新用户流程:
1. 扫码进入邀请页面 → 保存 invite token
2. 点击"微信手机号快速加入"
3. 检测到未登录 → 保存 pending 数据 → 跳转到 phone-auth
4. 完成登录/注册 → 返回邀请页面
5. 自动检测 pending 数据 → 自动执行加入
6. 加入成功 → 跳转到首页
已登录用户流程:
1. 扫码进入邀请页面
2. 点击"微信手机号快速加入"
3. 直接执行加入操作
4. 加入成功 → 跳转到首页
```
### 文件修改
- `src/passport/invite/index.tsx` - 完整重构邀请流程

View File

@@ -11,6 +11,13 @@
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{
"name": "passport/invite/index",
"pathName": "passport/invite/index",
"query": "",
"launchMode": "default",
"scene": null
},
{ {
"name": "passport/invite", "name": "passport/invite",
"pathName": "passport/invite", "pathName": "passport/invite",

View File

@@ -6,11 +6,20 @@ import Taro, { useRouter } from '@tarojs/taro';
// 注意:使用 /api/_app 前缀表示小程序专用接口(免登录) // 注意:使用 /api/_app 前缀表示小程序专用接口(免登录)
const INVITE_API_URL = 'https://websopy-api.websoft.top'; const INVITE_API_URL = 'https://websopy-api.websoft.top';
import { TenantId } from "@/config/app"; import { TenantId } from "@/config/app";
import { getStoredInviteParams, saveInviteParams, clearInviteParams } from "@/utils/invite";
/** /**
* 邀请加入确认页面 * 邀请加入确认页面
* *
* 用户扫描邀请二维码后,打开此小程序页面确认加入应用 * 用户扫描邀请二维码后,打开此小程序页面确认加入应用
*
* 流程:
* 1. 用户扫码进入页面,解析 token 参数
* 2. 获取邀请信息展示给用户
* 3. 用户点击"微信手机号快速加入"
* 4. 如果用户未登录,保存邀请信息并引导到登录页
* 5. 登录成功后返回此页面,自动执行加入操作
* 6. 已登录用户直接执行加入操作
*/ */
// 微信获取手机号回调参数类型 // 微信获取手机号回调参数类型
@@ -45,8 +54,13 @@ const InvitePage: React.FC = () => {
const [agreementChecked, setAgreementChecked] = useState(false); const [agreementChecked, setAgreementChecked] = useState(false);
const [token, setToken] = useState<string>(''); const [token, setToken] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
// 检查用户是否已登录
const accessToken = Taro.getStorageSync('access_token');
setIsLoggedIn(!!accessToken);
// 从 URL 参数中获取 token // 从 URL 参数中获取 token
const params = router.params; const params = router.params;
let inviteToken = params.scene || params.token || params.qrCodeKey || ''; let inviteToken = params.scene || params.token || params.qrCodeKey || '';
@@ -66,6 +80,12 @@ const InvitePage: React.FC = () => {
// 获取邀请信息 // 获取邀请信息
if (inviteToken) { if (inviteToken) {
// 保存邀请 token 到本地存储,供登录后使用
saveInviteParams({
inviter: inviteToken,
source: 'app_invite',
t: Date.now().toString()
});
fetchInviteInfo(inviteToken); fetchInviteInfo(inviteToken);
} else { } else {
setError('无效的邀请链接'); setError('无效的邀请链接');
@@ -73,6 +93,33 @@ const InvitePage: React.FC = () => {
} }
}, [router.params]); }, [router.params]);
// 页面显示时检查是否需要自动执行加入操作(从登录页返回)
useEffect(() => {
const handleShow = () => {
// 检查是否有 pending 的邀请数据且用户已登录
const pendingToken = Taro.getStorageSync('pending_invite_token');
const accessToken = Taro.getStorageSync('access_token');
if (pendingToken && accessToken) {
console.log('检测到登录后返回,自动执行加入应用操作');
// 更新登录状态
setIsLoggedIn(true);
// 自动执行加入操作
handleJoinApp();
}
};
// 监听页面显示事件
Taro.eventCenter.on('AppShow', handleShow);
// 立即检查一次(处理页面首次加载时已经是登录状态的情况)
handleShow();
return () => {
Taro.eventCenter.off('AppShow', handleShow);
};
}, []);
/** /**
* 获取邀请信息 * 获取邀请信息
*/ */
@@ -110,6 +157,9 @@ const InvitePage: React.FC = () => {
/** /**
* 处理微信手机号授权 * 处理微信手机号授权
*
* 如果用户未登录,先引导到登录页面完成注册/登录
* 登录成功后返回此页面自动执行加入操作
*/ */
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => { const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
const { code, encryptedData, iv, errMsg } = detail; const { code, encryptedData, iv, errMsg } = detail;
@@ -131,28 +181,57 @@ const InvitePage: React.FC = () => {
return; return;
} }
// 检查用户是否已登录
const accessToken = Taro.getStorageSync('access_token');
if (!accessToken) {
// 未登录用户:保存当前邀请信息,引导到登录页面
// 保存邀请 token 和微信授权信息,供登录后使用
Taro.setStorageSync('pending_invite_token', token);
Taro.setStorageSync('pending_invite_phone_code', code);
if (encryptedData) Taro.setStorageSync('pending_invite_encrypted_data', encryptedData);
if (iv) Taro.setStorageSync('pending_invite_iv', iv);
// 引导到手机号授权登录页面
Taro.navigateTo({
url: `/passport/phone-auth/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + token)}`
});
return;
}
// 已登录用户:直接执行加入操作
await handleJoinApp(code, encryptedData, iv); await handleJoinApp(code, encryptedData, iv);
}; };
/** /**
* 加入应用 * 加入应用
*
* 支持两种调用方式:
* 1. 新用户通过手机号授权码完成注册并加入phoneCode 必传)
* 2. 已登录用户:直接加入应用(使用存储的 token
*/ */
const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => { const handleJoinApp = async (phoneCode?: string, encryptedData?: string, iv?: string) => {
try { try {
setAuthLoading(true); setAuthLoading(true);
console.log('开始接受邀请, token:', token); // 获取 pending 的邀请信息(如果是从登录页返回)
const pendingToken = Taro.getStorageSync('pending_invite_token') || token;
const pendingPhoneCode = phoneCode || Taro.getStorageSync('pending_invite_phone_code');
const pendingEncryptedData = encryptedData || Taro.getStorageSync('pending_invite_encrypted_data');
const pendingIv = iv || Taro.getStorageSync('pending_invite_iv');
console.log('开始接受邀请, token:', pendingToken);
console.log('请求URL:', `${INVITE_API_URL}/api/_app/developer/invite/accept`); console.log('请求URL:', `${INVITE_API_URL}/api/_app/developer/invite/accept`);
console.log('请求参数:', { token, code: phoneCode ? '***' : null }); console.log('请求参数:', { token: pendingToken, code: pendingPhoneCode ? '***' : null });
const res = await Taro.request({ const res = await Taro.request({
url: `${INVITE_API_URL}/api/_app/developer/invite/accept`, url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
method: 'POST', method: 'POST',
data: { data: {
token, token: pendingToken,
code: phoneCode, code: pendingPhoneCode,
encryptedData, encryptedData: pendingEncryptedData,
iv iv: pendingIv
}, },
header: { header: {
'content-type': 'application/json', 'content-type': 'application/json',
@@ -164,6 +243,10 @@ const InvitePage: React.FC = () => {
console.log('响应数据:', res.data); console.log('响应数据:', res.data);
if (res.data.code === 200 || res.data.code === 0) { if (res.data.code === 200 || res.data.code === 0) {
// 清除 pending 的邀请信息
clearPendingInviteData();
clearInviteParams();
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 }); Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
setTimeout(() => { setTimeout(() => {
// 跳转到应用页面或首页 // 跳转到应用页面或首页
@@ -171,7 +254,33 @@ const InvitePage: React.FC = () => {
}, 1500); }, 1500);
} else { } else {
console.error('接受邀请失败:', res.data.message, 'code:', res.data.code); console.error('接受邀请失败:', res.data.message, 'code:', res.data.code);
Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' }); const errorMsg = res.data.message || '';
// 用户不存在,引导去登录注册
if (errorMsg.includes('用户不存在') || errorMsg.includes('用户创建失败') || errorMsg.includes('请先登录')) {
// 保存当前邀请信息
Taro.setStorageSync('pending_invite_token', pendingToken);
if (pendingPhoneCode) Taro.setStorageSync('pending_invite_phone_code', pendingPhoneCode);
if (pendingEncryptedData) Taro.setStorageSync('pending_invite_encrypted_data', pendingEncryptedData);
if (pendingIv) Taro.setStorageSync('pending_invite_iv', pendingIv);
Taro.showModal({
title: '需要登录',
content: '您尚未注册,请先完成登录或注册后再加入应用',
confirmText: '去登录',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
// 跳转到手机号授权登录页面携带token参数以便登录后返回
Taro.navigateTo({
url: `/passport/phone-auth/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + pendingToken)}`
});
}
}
});
} else {
Taro.showToast({ title: errorMsg || '加入失败', icon: 'none' });
}
} }
} catch (err: any) { } catch (err: any) {
console.error('接受邀请异常:', err); console.error('接受邀请异常:', err);
@@ -181,6 +290,16 @@ const InvitePage: React.FC = () => {
} }
}; };
/**
* 清除 pending 的邀请数据
*/
const clearPendingInviteData = () => {
Taro.removeStorageSync('pending_invite_token');
Taro.removeStorageSync('pending_invite_phone_code');
Taro.removeStorageSync('pending_invite_encrypted_data');
Taro.removeStorageSync('pending_invite_iv');
};
/** /**
* 拒绝邀请 * 拒绝邀请
*/ */