diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json
index a718924..df259eb 100644
--- a/.workbuddy/expert-history.json
+++ b/.workbuddy/expert-history.json
@@ -35,5 +35,5 @@
}
]
},
- "lastUpdated": 1775923978885
+ "lastUpdated": 1775965240928
}
\ No newline at end of file
diff --git a/.workbuddy/memory/2026-04-12.md b/.workbuddy/memory/2026-04-12.md
new file mode 100644
index 0000000..5bcf90a
--- /dev/null
+++ b/.workbuddy/memory/2026-04-12.md
@@ -0,0 +1,84 @@
+# 2026-04-12 工作记录
+
+## 任务:优化邀请加入应用按钮逻辑
+
+### 需求描述
+loginByOpenId 返回有用户数据(已登录)时,不显示手机号授权按钮,直接显示「确认加入」普通按钮;
+loginByOpenId 返回未注册时才走 getPhoneNumber 授权分支。
+
+### 解决方案
+完全重写 `invite/index.tsx`,核心逻辑:
+
+#### 按钮渲染逻辑
+```tsx
+{isLoggedIn ? (
+ // 已登录:普通按钮,直接加入,携带 Authorization 头
+
+) : (
+ // 未注册:手机号授权按钮(兜底,实际大多已被重定向到 login 页)
+
+)}
+```
+
+#### handleJoinApp 统一入口
+- `useToken` 参数:已登录用户,请求头加 `Authorization: Bearer xxx`
+- `phoneCode` 参数:未注册用户,请求体加 code/encryptedData/iv
+
+### 文件修改
+- `src/passport/invite/index.tsx` - 完整重写,区分已登录/未注册两种按钮状态
+
+---
+
+## 任务:未注册用户在邀请页内完成授权注册,不跳登录页
+
+### 需求
+- loginByOpenId 未注册 → 在页面内显示「微信手机号授权」按钮
+- 授权成功 → 调用 `loginByMpWxPhone` 注册/登录 → 自动执行加入应用
+- 不再跳转 passport/login 页面
+
+### 关键逻辑
+1. `checkLoginStatus`:已注册 isLoggedIn=true,未注册 isLoggedIn=false,**两种情况都显示邀请页**
+2. 未注册按钮:`open-type="getPhoneNumber"` → `handleGetPhoneNumber`
+ - 授权码调 `SERVER_API_URL/wx-login/loginByMpWxPhone` 完成注册登录
+ - 保存 token → isLoggedIn=true → 立即调 `doJoinApp`
+3. 已注册按钮:普通 `onClick` → `handleConfirmJoin` → `doJoinApp(access_token)`
+4. `doJoinApp`:统一加入接口,请求头带 `Authorization: Bearer {access_token}`
+
+### 文件修改
+- `src/passport/invite/index.tsx` - 完整重写(彻底移除跳登录页逻辑)
+
+---
+
+## 修复:「授权码不能为空」报错
+
+### 问题
+已登录用户点「确认加入」时,后端报 `授权码不能为空`。
+后端 `/api/_app/developer/invite/accept` 接口不管是否登录,都要求传 `code`(微信授权码)。
+
+### 解决
+统一用一个 `getPhoneNumber` 按钮处理两种场景:
+- **已注册**:文字「确认加入」→ 触发 getPhoneNumber → `doJoinApp(code, accessToken)`
+- **未注册**:文字「微信手机号快速加入」→ 触发 getPhoneNumber → 先 `loginByMpWxPhone` 注册 → 再 `wx.login()` 获取新 code → `doJoinApp(newCode, access_token)`
+
+### doJoinApp 参数
+```ts
+doJoinApp(wxCode: string, accessToken: string)
+// 请求体带 code,请求头带 Authorization: Bearer xxx
+```
+
+---
+
+## 优化:已登录用户不弹手机号授权
+
+### 改动
+- 已登录按钮:普通 `onClick`,文字「确认加入」
+- 未注册按钮:`getPhoneNumber` 授权,文字「微信手机号快速加入」
+
+### 逻辑差异
+| 用户状态 | 按钮类型 | 获取 code 方式 |
+|------|------|------|
+| 已登录 | 普通 onClick | `wx.login()` |
+| 未注册 | getPhoneNumber | 授权回调的 `code` |
+
+### 文件修改
+- `src/passport/invite/index.tsx` - 按钮区分两种类型,已登录用普通 onClick
diff --git a/src/passport/invite/index.tsx b/src/passport/invite/index.tsx
index 564bcf4..cf082c9 100644
--- a/src/passport/invite/index.tsx
+++ b/src/passport/invite/index.tsx
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
import { View, Text, Button, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { loginByOpenId } from '@/api/passport/wx-login';
-import { TenantId } from "@/config/app";
+import { saveStorageByLoginUser, SERVER_API_URL } from '@/utils/server';
+import { TenantId } from '@/config/app';
// 邀请相关接口使用独立的 API 域名
const INVITE_API_URL = 'https://websopy-api.websoft.top';
@@ -11,15 +12,12 @@ const INVITE_API_URL = 'https://websopy-api.websoft.top';
* 邀请加入确认页面
*
* 流程:
- * 1. 扫码进入页面
- * 2. 调用 wx.login() 获取 code
- * 3. 调用 loginByOpenId 判断用户是否已注册
- * 4. 已注册:显示邀请页面,用户点击加入
- * 5. 未注册:跳转到 passport/login 页面完成登录/注册
- * 6. 登录成功后返回,自动执行加入操作
+ * 1. 扫码进入 → 调用 loginByOpenId 判断登录状态
+ * 2. 已注册 → isLoggedIn=true → 显示「确认加入」按钮 → 点击直接加入(带 access_token)
+ * 3. 未注册 → isLoggedIn=false → 显示「微信手机号授权」按钮
+ * → 授权成功 → 注册/登录 → isLoggedIn=true → 自动执行加入
*/
-// 邀请信息类型
interface InviteInfo {
appId: string;
appName: string;
@@ -28,39 +26,39 @@ interface InviteInfo {
roleName: string;
}
-// 协议类型
type AgreementType = 'service' | 'privacy';
-
-// 页面状态
-type PageStatus = 'loading' | 'checking' | 'invite' | 'login' | 'error';
+type PageStatus = 'loading' | 'checking' | 'invite' | 'error';
const InvitePage: React.FC = () => {
const router = useRouter();
+
const [pageStatus, setPageStatus] = useState('loading');
const [inviteInfo, setInviteInfo] = useState(null);
const [authLoading, setAuthLoading] = useState(false);
const [agreementChecked, setAgreementChecked] = useState(false);
const [token, setToken] = useState('');
const [error, setError] = useState('');
+ /**
+ * true = 已注册已登录,按钮文字「确认加入」(仍走 getPhoneNumber 获取 code 传给后端)
+ * false = 未注册,按钮文字「微信手机号快速加入」,授权后先注册登录再加入
+ */
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
- initPage().then();
+ initPage();
}, [router.params]);
- // 页面初始化
+ // ── 初始化 ──────────────────────────────────────────
const initPage = async () => {
- // 从 URL 参数中获取 token
const params = router.params;
let inviteToken = params.scene || params.token || params.qrCodeKey || '';
- console.log(params,'URL参数')
- // 兼容 q 参数(URL 编码的完整 URL)
+
if (params.q && !inviteToken) {
try {
const decodedUrl = decodeURIComponent(params.q);
const url = new URL(decodedUrl);
inviteToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || '';
- } catch (e) {
+ } catch {
inviteToken = decodeURIComponent(params.q);
}
}
@@ -72,196 +70,211 @@ const InvitePage: React.FC = () => {
}
setToken(inviteToken);
- // 保存邀请 token,供登录后使用
Taro.setStorageSync('invite_token', inviteToken);
- // 检查用户登录状态
+ // 先获取邀请信息,再检查登录状态(并行更好,但 inviteInfo 失败要 error)
await checkLoginStatus(inviteToken);
- console.log('检查登录状态完成', inviteToken)
};
- // 检查用户登录状态
+ // ── loginByOpenId 检查登录状态 ──────────────────────
const checkLoginStatus = async (inviteToken: string) => {
setPageStatus('checking');
-
try {
- // 调用 wx.login 获取 code
- const wxLoginRes = await Taro.login();
- console.log('wx.login 结果:', wxLoginRes);
+ const wxRes = await Taro.login();
+ if (!wxRes.code) throw new Error('获取微信登录凭证失败');
- if (!wxLoginRes.code) {
- throw new Error('获取微信登录凭证失败');
- }
-
- // 调用 loginByOpenId 判断用户是否已注册
const loginRes = await loginByOpenId({
- code: wxLoginRes.code,
- tenantId: parseInt(TenantId) || 1
+ code: wxRes.code,
+ tenantId: parseInt(TenantId) || 1,
});
console.log('loginByOpenId 结果:', loginRes);
if (loginRes.success && loginRes.data?.access_token) {
- // 用户已注册,保存登录信息
- Taro.setStorageSync('access_token', loginRes.data.access_token);
- if (loginRes.data.user) {
- Taro.setStorageSync('user_info', JSON.stringify(loginRes.data.user));
- }
+ // ✅ 已注册:保存登录信息,显示「确认加入」
+ saveStorageByLoginUser(loginRes.data.access_token, loginRes.data.user as any);
setIsLoggedIn(true);
- // 获取邀请信息并显示邀请页面
- await fetchInviteInfo(inviteToken);
- setPageStatus('invite');
} else {
- // 用户未注册,跳转到登录页面
- console.log('用户未注册,跳转到登录页面');
- setPageStatus('login');
- // 延迟跳转,让用户看到提示
- setTimeout(() => {
- navigateToLogin(inviteToken);
- }, 500);
+ // ❌ 未注册:显示手机号授权按钮
+ console.log('用户未注册,显示手机号授权按钮');
+ setIsLoggedIn(false);
}
+
+ // 两种情况都需要获取邀请信息
+ await fetchInviteInfo(inviteToken);
+ setPageStatus('invite');
} catch (err: any) {
console.error('检查登录状态失败:', err);
- // 出错时也跳转到登录页面
- setPageStatus('login');
- setTimeout(() => {
- navigateToLogin(inviteToken);
- }, 500);
- }
- };
-
- // 跳转到登录页面
- const navigateToLogin = (inviteToken: string) => {
- Taro.redirectTo({
- url: `/passport/login?redirect=${encodeURIComponent('/passport/invite/index?token=' + inviteToken)}&from=invite`
- });
- };
-
- // 获取邀请信息
- const fetchInviteInfo = async (inviteToken: string) => {
- try {
- console.log('开始获取邀请信息, token:', inviteToken);
-
- const res = await Taro.request({
- url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
- method: 'GET',
- header: {
- 'content-type': 'application/json',
- TenantId
- }
- });
-
- console.log('邀请信息接口响应:', res);
-
- if (res.data.code === 200 || res.data.code === 0) {
- setInviteInfo(res.data.data);
- } else {
- console.error('接口返回错误:', res.data.message);
- setError(res.data.message || '邀请信息获取失败');
- setPageStatus('error');
- }
- } catch (err: any) {
- console.error('获取邀请信息异常:', err);
- setError(err.message || '网络请求失败');
+ setError(err.message || '初始化失败,请重试');
setPageStatus('error');
}
};
- // 处理微信手机号授权
- const handleGetPhoneNumber = async (e: any) => {
- const { code, encryptedData, iv, errMsg } = e.detail;
+ // ── 获取邀请信息 ─────────────────────────────────────
+ const fetchInviteInfo = async (inviteToken: string) => {
+ const res = await Taro.request({
+ url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
+ method: 'GET',
+ header: { 'content-type': 'application/json', TenantId },
+ });
- console.log('handleGetPhoneNumber:', { code, errMsg });
+ console.log('邀请信息接口响应:', res);
- // 检查协议是否勾选
+ if (res.data.code === 200 || res.data.code === 0) {
+ setInviteInfo(res.data.data);
+ } else {
+ throw new Error(res.data.message || '邀请信息获取失败');
+ }
+ };
+
+ /**
+ * 已登录用户:点击「确认加入」
+ * 不弹手机号授权,直接用 wx.login() 获取 code 调加入接口
+ */
+ const handleConfirmJoin = async () => {
if (!agreementChecked) {
Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
return;
}
-
- // 用户拒绝授权
- if (errMsg && errMsg.includes('fail')) {
- Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' });
+ const accessToken = Taro.getStorageSync('access_token');
+ if (!accessToken) {
+ Taro.showToast({ title: '登录状态异常,请刷新重试', icon: 'none' });
return;
}
- if (!code) {
- Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' });
- return;
- }
-
- // 执行加入操作
- await handleJoinApp(code, encryptedData, iv);
- };
-
- // 加入应用
- const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => {
try {
setAuthLoading(true);
-
- const inviteToken = Taro.getStorageSync('invite_token') || token;
-
- console.log('开始接受邀请, token:', inviteToken);
-
- const res = await Taro.request({
- url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
- method: 'POST',
- data: {
- token: inviteToken,
- code: phoneCode,
- encryptedData,
- iv
- },
- header: {
- 'content-type': 'application/json',
- TenantId
- }
- });
-
- console.log('接受邀请接口响应:', res);
-
- if (res.data.code === 200 || res.data.code === 0) {
- // 清理邀请信息
- Taro.removeStorageSync('invite_token');
-
- Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
- setTimeout(() => {
- Taro.switchTab({ url: '/pages/index/index' });
- }, 1500);
- } else {
- console.error('接受邀请失败:', res.data.message);
- Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' });
- }
+ // 用 wx.login() 获取 code(后端需要 code 识别用户身份)
+ const wxRes = await Taro.login();
+ if (!wxRes.code) throw new Error('获取微信登录凭证失败');
+ await doJoinApp(wxRes.code, accessToken);
} catch (err: any) {
- console.error('接受邀请异常:', err);
- Taro.showToast({ title: err.message || '加入失败', icon: 'error' });
+ console.error('确认加入失败:', err);
+ Taro.showToast({ title: err.message || '加入失败,请重试', icon: 'none' });
} finally {
setAuthLoading(false);
}
};
- // 拒绝邀请
- const handleReject = () => {
- Taro.switchTab({ url: '/pages/index/index' });
+ /**
+ * 未注册用户:手机号授权回调
+ * 先用 code 注册/登录,再用新的 wx.login code 调加入接口
+ */
+ const handleGetPhoneNumber = async (e: any) => {
+ const { code, encryptedData, iv, errMsg } = e.detail;
+
+ if (!agreementChecked) {
+ Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
+ return;
+ }
+ if (errMsg?.includes('fail')) {
+ Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' });
+ return;
+ }
+ if (!code) {
+ Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' });
+ return;
+ }
+
+ try {
+ setAuthLoading(true);
+
+ // 1. 用手机号授权码完成注册/登录
+ console.log('未注册用户,先注册/登录再加入');
+ const loginRes = await Taro.request({
+ url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
+ method: 'POST',
+ data: {
+ code,
+ encryptedData,
+ iv,
+ tenantId: parseInt(TenantId) || 1,
+ notVerifyPhone: true,
+ },
+ header: { 'content-type': 'application/json', TenantId },
+ });
+
+ console.log('手机号注册/登录结果:', loginRes);
+
+ if (loginRes.data.code !== 0 && loginRes.data.code !== 200) {
+ throw new Error(loginRes.data.message || '注册/登录失败');
+ }
+
+ const { access_token, user } = loginRes.data.data || {};
+ if (!access_token) throw new Error('登录失败,未获取到 token');
+
+ // 2. 保存登录信息
+ saveStorageByLoginUser(access_token, user);
+ setIsLoggedIn(true);
+
+ // 3. 手机号授权码已被消耗,重新获取 wx.login code 调加入接口
+ console.log('注册成功,重新获取 wx.login code 调加入接口');
+ const wxRes = await Taro.login();
+ if (!wxRes.code) throw new Error('获取微信登录凭证失败');
+
+ await doJoinApp(wxRes.code, access_token);
+ } catch (err: any) {
+ console.error('手机号授权登录失败:', err);
+ Taro.showToast({ title: err.message || '授权失败,请重试', icon: 'none' });
+ } finally {
+ setAuthLoading(false);
+ }
};
- // 打开协议页面
+ /**
+ * 调用加入接口(统一入口)
+ * @param wxCode wx.login() 或 getPhoneNumber 获取的 code(后端用于识别用户)
+ * @param accessToken 登录 token(加到 Authorization 头,双重验证)
+ */
+ const doJoinApp = async (wxCode: string, accessToken: string) => {
+ const inviteToken = Taro.getStorageSync('invite_token') || token;
+ console.log('doJoinApp, inviteToken:', inviteToken, 'wxCode:', wxCode ? '有' : '无', 'accessToken:', accessToken ? '有' : '无');
+
+ const res = await Taro.request({
+ url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
+ method: 'POST',
+ data: {
+ token: inviteToken,
+ code: wxCode,
+ },
+ header: {
+ 'content-type': 'application/json',
+ TenantId,
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ console.log('加入应用接口响应:', res);
+
+ if (res.data.code === 200 || res.data.code === 0) {
+ Taro.removeStorageSync('invite_token');
+ Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
+ setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 1500);
+ } else {
+ throw new Error(res.data.message || '加入失败');
+ }
+ };
+
+ // ── 拒绝 / 打开协议 ─────────────────────────────────
+ const handleReject = () => Taro.switchTab({ url: '/pages/index/index' });
+
const openAgreement = (type: AgreementType) => {
const urlMap = {
service: 'https://websopy.websoft.top/agreement',
privacy: 'https://websopy.websoft.top/privacy',
};
- const targetUrl = encodeURIComponent(urlMap[type]);
- Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` });
+ Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(urlMap[type])}` });
};
- // 加载中状态
+ // ── Loading / Checking 状态 ──────────────────────────
if (pageStatus === 'loading' || pageStatus === 'checking') {
return (
-
+
@@ -273,33 +286,17 @@ const InvitePage: React.FC = () => {
);
}
- // 跳转到登录中状态
- if (pageStatus === 'login') {
- return (
-
-
- 🔐
-
- 请先登录
-
-
- 正在跳转到登录页面...
-
-
-
- );
- }
-
- // 错误状态
+ // ── 错误状态 ─────────────────────────────────────────
if (pageStatus === 'error') {
return (
-
+
❌
{error}
@@ -308,34 +305,69 @@ const InvitePage: React.FC = () => {
);
}
- // 邀请确认页面
+ // ── 邀请确认页面 ──────────────────────────────────────
return (
-
+
- {/* 背景效果 */}
+ {/* 背景网格 */}
- {/* 渐变光晕 */}
+ {/* 渐变光晕 - 左上 */}
- {/* 主内容区域 */}
+ {/* 渐变光晕 - 右下 */}
+
+
+ {/* 动态粒子光点 */}
+ {[
+ { top: '15%', left: '10%', size: 4, delay: '0s' },
+ { top: '20%', left: '85%', size: 3, delay: '0.5s' },
+ { top: '70%', left: '8%', size: 4, delay: '1s' },
+ { top: '75%', left: '88%', size: 3, delay: '1.5s' },
+ ].map((p, i) => (
+
+ ))}
+
+ {/* 扫描线 */}
+
+
+ {/* 主内容 */}
{/* 邀请卡片 */}
- {/* 应用信息 */}
+
+ {/* 应用 Logo + 名称 */}
{
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: '16px', overflow: 'hidden',
}}>
- {inviteInfo?.appLogo ? (
-
- ) : (
- 🚀
- )}
+ {inviteInfo?.appLogo
+ ?
+ : 🚀
+ }
-
+
{inviteInfo?.appName || 'Websopy'}
-
- 邀请你加入应用
-
+ 邀请你加入应用
- {/* 邀请人信息 */}
+ {/* 邀请人 */}
邀请人:{inviteInfo?.inviterName || '某位用户'}
@@ -374,50 +401,71 @@ const InvitePage: React.FC = () => {
{/* 分隔线 */}
{/* 协议勾选 */}
-
- setAgreementChecked(!agreementChecked)}>
-
- {agreementChecked && ✓}
+ {!isLoggedIn && (
+
+ setAgreementChecked(!agreementChecked)}>
+
+ {agreementChecked && ✓}
+
+ 我已阅读并同意
+ openAgreement('service')}>《服务协议》
+ 和
+ openAgreement('privacy')}>《隐私政策》
- 我已阅读并同意
- openAgreement('service')}>《服务协议》
- 和
- openAgreement('privacy')}>《隐私政策》
-
+ )}
- {/* 主按钮 */}
+ {/* ===== 按钮区:根据登录状态切换按钮类型 ===== */}
-
+ {isLoggedIn ? (
+ /* 已登录:普通按钮,不弹手机号授权,用 wx.login() 获取 code */
+
+ ) : (
+ /* 未注册:手机号授权按钮 */
+
+ )}
- {/* 拒绝按钮 */}
-
+ {/* 拒绝 */}
+
暂不加入