diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json index decac99..2c5ec6c 100644 --- a/.workbuddy/expert-history.json +++ b/.workbuddy/expert-history.json @@ -22,7 +22,18 @@ "usedAt": 1775908159660, "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": 1775910071224 + "lastUpdated": 1775921440281 } \ No newline at end of file diff --git a/.workbuddy/memory/2026-04-11.md b/.workbuddy/memory/2026-04-11.md new file mode 100644 index 0000000..5f4a066 --- /dev/null +++ b/.workbuddy/memory/2026-04-11.md @@ -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` - 完整重构邀请流程 diff --git a/project.private.config.json b/project.private.config.json index 7628790..ab188a4 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -11,6 +11,13 @@ "scene": null, "launchMode": "default" }, + { + "name": "passport/invite/index", + "pathName": "passport/invite/index", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "passport/invite", "pathName": "passport/invite", diff --git a/src/passport/invite/index.tsx b/src/passport/invite/index.tsx index 6acb211..9d1d083 100644 --- a/src/passport/invite/index.tsx +++ b/src/passport/invite/index.tsx @@ -6,11 +6,20 @@ import Taro, { useRouter } from '@tarojs/taro'; // 注意:使用 /api/_app 前缀表示小程序专用接口(免登录) const INVITE_API_URL = 'https://websopy-api.websoft.top'; 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 [token, setToken] = useState(''); const [error, setError] = useState(''); + const [isLoggedIn, setIsLoggedIn] = useState(false); useEffect(() => { + // 检查用户是否已登录 + const accessToken = Taro.getStorageSync('access_token'); + setIsLoggedIn(!!accessToken); + // 从 URL 参数中获取 token const params = router.params; let inviteToken = params.scene || params.token || params.qrCodeKey || ''; @@ -66,6 +80,12 @@ const InvitePage: React.FC = () => { // 获取邀请信息 if (inviteToken) { + // 保存邀请 token 到本地存储,供登录后使用 + saveInviteParams({ + inviter: inviteToken, + source: 'app_invite', + t: Date.now().toString() + }); fetchInviteInfo(inviteToken); } else { setError('无效的邀请链接'); @@ -73,6 +93,33 @@ const InvitePage: React.FC = () => { } }, [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 { code, encryptedData, iv, errMsg } = detail; @@ -131,28 +181,57 @@ const InvitePage: React.FC = () => { 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); }; /** * 加入应用 + * + * 支持两种调用方式: + * 1. 新用户:通过手机号授权码完成注册并加入(phoneCode 必传) + * 2. 已登录用户:直接加入应用(使用存储的 token) */ - const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => { + const handleJoinApp = async (phoneCode?: string, encryptedData?: string, iv?: string) => { try { 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('请求参数:', { token, code: phoneCode ? '***' : null }); + console.log('请求参数:', { token: pendingToken, code: pendingPhoneCode ? '***' : null }); const res = await Taro.request({ url: `${INVITE_API_URL}/api/_app/developer/invite/accept`, method: 'POST', data: { - token, - code: phoneCode, - encryptedData, - iv + token: pendingToken, + code: pendingPhoneCode, + encryptedData: pendingEncryptedData, + iv: pendingIv }, header: { 'content-type': 'application/json', @@ -164,6 +243,10 @@ const InvitePage: React.FC = () => { console.log('响应数据:', res.data); if (res.data.code === 200 || res.data.code === 0) { + // 清除 pending 的邀请信息 + clearPendingInviteData(); + clearInviteParams(); + Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 }); setTimeout(() => { // 跳转到应用页面或首页 @@ -175,6 +258,12 @@ const InvitePage: React.FC = () => { // 用户不存在,引导去登录注册 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: '您尚未注册,请先完成登录或注册后再加入应用', @@ -182,9 +271,9 @@ const InvitePage: React.FC = () => { cancelText: '取消', success: (modalRes) => { if (modalRes.confirm) { - // 跳转到登录页面,携带token参数以便登录后返回 + // 跳转到手机号授权登录页面,携带token参数以便登录后返回 Taro.navigateTo({ - url: `/passport/login/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + token)}` + url: `/passport/phone-auth/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + pendingToken)}` }); } } @@ -201,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'); + }; + /** * 拒绝邀请 */