docs: 添加微信登录、企业微信登录与Fake登录流程详细文档
新增登录流程详细文档,包含三种登录方式的完整流程、参数说明、返回信息及实现逻辑。文档涵盖核心代码实现、API接口详情、状态管理、错误处理与最佳实践等内容,为开发人员提供全面的参考指南。
This commit is contained in:
parent
77ff54efa3
commit
66d8acc9a9
|
|
@ -0,0 +1,951 @@
|
|||
# 微信登录、企业微信登录与Fake登录流程详细文档
|
||||
|
||||
## 文档概述
|
||||
|
||||
本文档详细描述了智柜宝系统中三种登录方式的完整流程、参数说明、返回信息及实现逻辑。
|
||||
|
||||
## 一、微信登录(普通微信公众号登录)
|
||||
|
||||
### 1.1 登录流程图
|
||||
|
||||
```
|
||||
用户访问应用
|
||||
↓
|
||||
检查微信环境(micromessenger)
|
||||
↓
|
||||
跳转微信授权页面
|
||||
↓
|
||||
用户授权
|
||||
↓
|
||||
微信回调携带 code 和 state
|
||||
↓
|
||||
调用 getOpenIdApi 换取 openid
|
||||
↓
|
||||
调用 getBalanceApi 获取余额
|
||||
↓
|
||||
完成认证
|
||||
```
|
||||
|
||||
### 1.2 核心实现
|
||||
|
||||
#### 前端回调处理(`src/main.ts` 或页面组件)
|
||||
|
||||
```typescript
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
|
||||
if (code) {
|
||||
wxStore.handleWxCallback({ code, state });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Store 处理逻辑(`src/pinia/stores/wx.ts:81-147`)
|
||||
|
||||
```typescript
|
||||
const handleWxCallback = async (params: { corpid?: string; code?: string; state?: string; }) => {
|
||||
isHandleWxCallbackComplete.value = false;
|
||||
|
||||
if (params.code) {
|
||||
code.value = params.code;
|
||||
state.value = params.state || state.value;
|
||||
corpid.value = params.corpid || corpid.value;
|
||||
corpidLogin.value = !!corpid.value;
|
||||
|
||||
try {
|
||||
// 微信公众号登录(无 corpid)
|
||||
if (!corpid.value) {
|
||||
const res = await getOpenIdApi({ code: params.code });
|
||||
if (res && res.code == 0) {
|
||||
openid.value = res.data;
|
||||
}
|
||||
|
||||
// 获取余额信息
|
||||
if (openid.value) {
|
||||
corpid.value = "wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw"; // 默认企业ID
|
||||
const balanceRes = await getBalanceApi(corpid.value, openid.value);
|
||||
if (balanceRes && balanceRes.code == 0) {
|
||||
balance.value = balanceRes.data.balance;
|
||||
useBalance.value = balanceRes.data.useBalance;
|
||||
balanceLimit.value = balanceRes.data.balanceLimit;
|
||||
userid.value = balanceRes.data.userid;
|
||||
if (!ab98User.value && balanceRes.data.ab98User) {
|
||||
setAb98User(balanceRes.data.ab98User);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取 openid 失败:', err);
|
||||
} finally {
|
||||
isHandleWxCallbackComplete.value = true;
|
||||
}
|
||||
} else {
|
||||
isHandleWxCallbackComplete.value = true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 1.3 API 接口详情
|
||||
|
||||
#### 获取 OpenID
|
||||
|
||||
**接口地址**: `GET /api/v1/payment/getOpenId`
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
interface GetOpenIdRequestParams {
|
||||
code: string; // 微信授权后返回的授权码
|
||||
}
|
||||
```
|
||||
|
||||
**返回数据**:
|
||||
```typescript
|
||||
interface ApiResponseData<T> {
|
||||
code: number; // 状态码:0 表示成功
|
||||
msg: string; // 消息
|
||||
data: string; // openid 字符串
|
||||
}
|
||||
```
|
||||
|
||||
**示例响应**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": "oMRxw6Eum0DB1IjI_pEX_yrawBHw"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取余额
|
||||
|
||||
**接口地址**: `GET /api/v1/payment/getBalance`
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
{
|
||||
corpid: string; // 企业ID
|
||||
openid: string; // 微信用户唯一标识
|
||||
}
|
||||
```
|
||||
|
||||
**返回数据**:
|
||||
```typescript
|
||||
interface GetBalanceResponse {
|
||||
userid: string; // 系统用户ID
|
||||
corpid: string; // 企业ID
|
||||
balance: number; // 剩余借呗
|
||||
useBalance: number; // 已用借呗
|
||||
balanceLimit: number; // 借呗总额
|
||||
ab98User: ab98UserDTO; // 汇邦云用户信息
|
||||
}
|
||||
```
|
||||
|
||||
**示例响应**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"userid": "woZ1ZrEgAAV9AEdRt1MGQxSg-KDJrDlA",
|
||||
"corpid": "wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw",
|
||||
"balance": 5000,
|
||||
"useBalance": 2000,
|
||||
"balanceLimit": 7000,
|
||||
"ab98User": {
|
||||
"userid": "123456",
|
||||
"name": "张三",
|
||||
"tel": "13800138000",
|
||||
"sex": "男",
|
||||
"faceImg": "https://example.com/avatar.jpg",
|
||||
"registered": true,
|
||||
"ab98Balance": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 微信授权 URL 生成
|
||||
|
||||
```typescript
|
||||
const generateWxAuthUrl = () => {
|
||||
const appId = import.meta.env.VITE_WX_APP_ID;
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/callback');
|
||||
const scope = 'snsapi_userinfo'; // 或 snsapi_base
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
|
||||
};
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `appid`: 微信公众号的 AppID
|
||||
- `redirect_uri`: 授权后回调的网址(需 URL 编码)
|
||||
- `scope`: 授权范围
|
||||
- `snsapi_base`: 静默授权,只能获取 openid
|
||||
- `snsapi_userinfo`: 需用户授权,可获取用户信息
|
||||
- `state`: 自定义参数,用于防止 CSRF 攻击
|
||||
|
||||
---
|
||||
|
||||
## 二、企业微信登录
|
||||
|
||||
### 2.1 登录流程图
|
||||
|
||||
```
|
||||
用户访问应用
|
||||
↓
|
||||
检测 URL 中的 corpid 参数
|
||||
↓
|
||||
跳转企业微信授权页面
|
||||
↓
|
||||
用户授权
|
||||
↓
|
||||
企业微信回调携带 corpid、code、state
|
||||
↓
|
||||
调用 qyLogin 验证并获取用户信息
|
||||
↓
|
||||
调用 getBalanceByQyUserid 获取余额
|
||||
↓
|
||||
关联 AB98 用户系统
|
||||
↓
|
||||
完成认证
|
||||
```
|
||||
|
||||
### 2.2 核心实现
|
||||
|
||||
#### 路由守卫处理(`src/router/guard.ts:22-31`)
|
||||
|
||||
```typescript
|
||||
// 企业微信登录参数检测
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const corpid = urlParams.get('corpid') || undefined;
|
||||
if (corpid) {
|
||||
return true;
|
||||
}
|
||||
const isAdmin = urlParams.get('isAdmin') || undefined;
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### Store 处理逻辑(`src/pinia/stores/wx.ts:93-109`)
|
||||
|
||||
```typescript
|
||||
if (!corpid.value) {
|
||||
// 微信公众号登录
|
||||
const res = await getOpenIdApi({ code: params.code });
|
||||
if (res && res.code == 0) {
|
||||
openid.value = res.data;
|
||||
}
|
||||
} else {
|
||||
// 企业微信登录
|
||||
const res = await qyLogin({
|
||||
corpid: corpid.value,
|
||||
code: params.code,
|
||||
state: params.state
|
||||
});
|
||||
|
||||
if (res && res.code == 0) {
|
||||
userid.value = res.data.userid; // 系统用户ID
|
||||
openid.value = res.data.openid; // 微信 openid
|
||||
isCabinetAdmin.value = res.data.isCabinetAdmin === 1; // 是否为柜子管理员
|
||||
name.value = res.data.name; // 用户姓名
|
||||
qyUserId.value = res.data.qyUserId; // 企业微信用户ID
|
||||
setAb98User(res.data.ab98User); // 设置 AB98 用户信息
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 余额获取(`src/pinia/stores/wx.ts:66-79`)
|
||||
|
||||
```typescript
|
||||
const refreshBalance = async () => {
|
||||
if (corpid.value && userid.value) {
|
||||
const res = await getBalanceByQyUserid(corpid.value, userid.value);
|
||||
if (res && res.code == 0) {
|
||||
balance.value = res.data.balance;
|
||||
useBalance.value = res.data.useBalance;
|
||||
balanceLimit.value = res.data.balanceLimit;
|
||||
|
||||
if (res.data.ab98User) {
|
||||
setAb98User(res.data.ab98User);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2.3 API 接口详情
|
||||
|
||||
#### 企业微信登录
|
||||
|
||||
**接口地址**: `GET /api/v1/payment/login/qy`
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
interface QyLoginRequestParams {
|
||||
corpid: string; // 企业ID
|
||||
code: string; // 企业微信授权后返回的授权码
|
||||
state?: string; // 状态参数(防CSRF)
|
||||
}
|
||||
```
|
||||
|
||||
**返回数据**:
|
||||
```typescript
|
||||
interface QyLoginDTO {
|
||||
userid: string; // 系统用户ID
|
||||
openid: string; // 微信 openid
|
||||
isCabinetAdmin: number; // 是否为柜子管理员(1是 0否)
|
||||
qyUserId: number; // 企业微信用户ID
|
||||
name: string; // 用户姓名
|
||||
ab98User: ab98UserDTO; // 汇邦云用户信息
|
||||
}
|
||||
```
|
||||
|
||||
**示例响应**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"userid": "woZ1ZrEgAAV9AEdRt1MGQxSg-KDJrDlA",
|
||||
"openid": "oMRxw6Eum0DB1IjI_pEX_yrawBHw",
|
||||
"isCabinetAdmin": 1,
|
||||
"qyUserId": 10001,
|
||||
"name": "张三",
|
||||
"ab98User": {
|
||||
"userid": "123456",
|
||||
"name": "张三",
|
||||
"tel": "13800138000",
|
||||
"sex": "男",
|
||||
"faceImg": "https://example.com/avatar.jpg",
|
||||
"registered": true,
|
||||
"ab98Balance": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 根据企业微信用户ID获取余额
|
||||
|
||||
**接口地址**: `GET /api/v1/payment/getBalanceByQyUserid`
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
{
|
||||
corpid: string; // 企业ID
|
||||
userid: string; // 系统用户ID
|
||||
}
|
||||
```
|
||||
|
||||
**返回数据**: 同 `GetBalanceResponse` 结构
|
||||
|
||||
### 2.4 企业微信授权 URL 生成
|
||||
|
||||
```typescript
|
||||
const generateQyWxAuthUrl = () => {
|
||||
const corpid = import.meta.env.VITE_WX_CORP_ID;
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/callback');
|
||||
const scope = 'snsapi_base'; // 企业微信通常使用 base
|
||||
const state = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${corpid}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Fake登录(模拟企业微信登录)
|
||||
|
||||
### 3.1 应用场景
|
||||
|
||||
Fake登录用于开发测试环境,模拟企业微信登录流程,无需真实的企业微信授权,直接使用预设的测试账号信息。
|
||||
|
||||
### 3.2 登录流程图
|
||||
|
||||
```
|
||||
触发 fake 登录
|
||||
↓
|
||||
设置 fake 登录状态
|
||||
↓
|
||||
设置默认企业ID
|
||||
↓
|
||||
设置默认用户ID
|
||||
↓
|
||||
调用 fakeQyLoginApi
|
||||
↓
|
||||
获取模拟用户信息
|
||||
↓
|
||||
完成认证
|
||||
```
|
||||
|
||||
### 3.3 核心实现(`src/pinia/stores/wx.ts:149-163`)
|
||||
|
||||
```typescript
|
||||
const fakeQyLogin = async () => {
|
||||
isFakeQyLogin.value = true; // 标记为 fake 登录
|
||||
corpid.value = "wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw"; // 默认企业ID
|
||||
corpidLogin.value = true; // 标记为企业微信登录
|
||||
userid.value = "woZ1ZrEgAAV9AEdRt1MGQxSg-KDJrDlA"; // 默认用户ID
|
||||
|
||||
// 调用 fake API
|
||||
const res = await fakeQyLoginApi({ corpid: corpid.value, userid: userid.value });
|
||||
|
||||
if (res && res.code == 0) {
|
||||
userid.value = res.data.userid;
|
||||
openid.value = "oMRxw6Eum0DB1IjI_pEX_yrawBHw"; // 固定的测试 openid
|
||||
isCabinetAdmin.value = res.data.isCabinetAdmin === 1;
|
||||
name.value = res.data.name;
|
||||
qyUserId.value = res.data.qyUserId;
|
||||
setAb98User(res.data.ab98User);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3.4 API 接口详情
|
||||
|
||||
**接口地址**: `GET /api/v1/payment/login/qy/fake`
|
||||
|
||||
**请求参数**:
|
||||
```typescript
|
||||
{
|
||||
corpid: string; // 企业ID
|
||||
userid: string; // 系统用户ID
|
||||
}
|
||||
```
|
||||
|
||||
**返回数据**: 与 `QyLoginDTO` 相同
|
||||
|
||||
**示例响应**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"userid": "woZ1ZrEgAAV9AEdRt1MGQxSg-KDJrDlA",
|
||||
"openid": "oMRxw6Eum0DB1IjI_pEX_yrawBHw",
|
||||
"isCabinetAdmin": 1,
|
||||
"qyUserId": 10001,
|
||||
"name": "测试用户",
|
||||
"ab98User": {
|
||||
"userid": "123456",
|
||||
"name": "测试用户",
|
||||
"tel": "13800138000",
|
||||
"sex": "男",
|
||||
"faceImg": "https://example.com/test-avatar.jpg",
|
||||
"registered": true,
|
||||
"ab98Balance": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 调用方式
|
||||
|
||||
#### 在页面中调用
|
||||
|
||||
```typescript
|
||||
import { useWxStore } from '@/pinia/stores/wx';
|
||||
|
||||
const wxStore = useWxStore();
|
||||
|
||||
// 触发 fake 登录
|
||||
const handleFakeLogin = () => {
|
||||
wxStore.fakeQyLogin();
|
||||
};
|
||||
```
|
||||
|
||||
#### 在组件模板中调用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<van-button @click="wxStore.fakeQyLogin">
|
||||
模拟登录
|
||||
</van-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、状态管理(Pinia Store)
|
||||
|
||||
### 4.1 WxStore 状态定义(`src/pinia/stores/wx.ts:7-38`)
|
||||
|
||||
```typescript
|
||||
export const useWxStore = defineStore("wx", () => {
|
||||
// 授权信息
|
||||
const code = ref<string>(""); // 微信授权 code
|
||||
const state = ref<string>(""); // 防CSRF的state参数
|
||||
|
||||
// 用户标识
|
||||
const openid = ref<string>(""); // 微信用户唯一标识
|
||||
const userid = ref<string>(""); // 系统用户ID
|
||||
const qyUserId = ref<number>(0); // 企业微信用户ID
|
||||
|
||||
// 企业信息
|
||||
const corpid = ref<string>(""); // 企业ID
|
||||
const corpidLogin = ref<boolean>(false);// 是否企业微信登录
|
||||
|
||||
// 用户信息
|
||||
const isCabinetAdmin = ref<boolean>(false); // 是否是柜子管理员
|
||||
const name = ref<string>(""); // 企业微信用户姓名
|
||||
const ab98User = ref<ab98UserDTO | null>(null); // 汇邦云用户信息
|
||||
|
||||
// 余额信息
|
||||
const balance = ref<number>(0); // 剩余借呗
|
||||
const useBalance = ref<number>(0); // 已用借呗
|
||||
const balanceLimit = ref<number>(0); // 借呗总额
|
||||
|
||||
// 登录状态
|
||||
const isFakeQyLogin = ref<boolean>(false); // 是否fake登录
|
||||
const isHandleWxCallbackComplete = ref<boolean>(false); // 回调处理是否完成
|
||||
});
|
||||
```
|
||||
|
||||
### 4.2 AB98用户Store(`src/pinia/stores/ab98-user.ts`)
|
||||
|
||||
```typescript
|
||||
export const useAb98UserStore = defineStore("ab98User", () => {
|
||||
// 用户基本信息
|
||||
const face_img = ref<string>(''); // 用户头像
|
||||
const sex = ref<string>(''); // 性别
|
||||
const name = ref<string>(''); // 姓名
|
||||
const userid = ref<string>(""); // 用户ID
|
||||
const registered = ref<boolean>(false); // 是否已注册
|
||||
const tel = ref<string>(""); // 手机号
|
||||
const token = ref<string>(""); // 认证令牌
|
||||
|
||||
// 登录状态
|
||||
const isLogin = ref<boolean>(false);
|
||||
const tokenLogin = ref<string>("");
|
||||
const loginCode = '1'; // 默认验证码
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 核心方法
|
||||
|
||||
#### 设置 AB98 用户信息
|
||||
|
||||
```typescript
|
||||
const setAb98User = (user: ab98UserDTO) => {
|
||||
ab98User.value = user;
|
||||
const ab98UserStore = useAb98UserStore();
|
||||
ab98UserStore.setUserInfo({
|
||||
face_img: ab98User.value.faceImg || "",
|
||||
success: true,
|
||||
sex: ab98User.value.sex || "",
|
||||
name: ab98User.value.name || "",
|
||||
userid: ab98User.value.userid || "",
|
||||
registered: true,
|
||||
tel: ab98User.value.tel || "",
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 等待回调完成
|
||||
|
||||
```typescript
|
||||
const waitForHandleWxCallbackComplete = async (timeout = 30000): Promise<boolean> => {
|
||||
const startTime = Date.now();
|
||||
while (!isHandleWxCallbackComplete.value) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
console.warn('等待 handleWxCallback 完成超时');
|
||||
return false;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据类型定义
|
||||
|
||||
### 5.1 ab98UserDTO
|
||||
|
||||
```typescript
|
||||
interface ab98UserDTO {
|
||||
/** 主键ID */
|
||||
ab98UserId?: number;
|
||||
/** openid */
|
||||
openid?: string;
|
||||
/** 汇邦云用户唯一ID */
|
||||
userid?: string;
|
||||
/** 真实姓名 */
|
||||
name?: string;
|
||||
/** 手机号码 */
|
||||
tel?: string;
|
||||
/** 身份证号码 */
|
||||
idnum?: string;
|
||||
/** 性别(男 女) */
|
||||
sex?: string;
|
||||
/** 人脸照片地址 */
|
||||
faceImg?: string;
|
||||
/** 身份证正面地址 */
|
||||
idcardFront?: string;
|
||||
/** 身份证背面地址 */
|
||||
idcardBack?: string;
|
||||
/** 身份证登记地址 */
|
||||
address?: string;
|
||||
/** 是否已注册(0未注册 1已注册) */
|
||||
registered?: boolean;
|
||||
/** 借呗余额 单位分 */
|
||||
ab98Balance?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 通用响应类型
|
||||
|
||||
```typescript
|
||||
interface ApiResponseData<T> {
|
||||
code: number; // 状态码:0表示成功,其他值表示失败
|
||||
msg: string; // 消息
|
||||
data: T; // 数据
|
||||
}
|
||||
|
||||
interface ApiResponseMsgData<T> {
|
||||
code: number; // 状态码
|
||||
msg: string; // 消息
|
||||
data: T; // 数据
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、环境配置
|
||||
|
||||
### 6.1 环境变量
|
||||
|
||||
#### 开发环境(`.env.development`)
|
||||
|
||||
```env
|
||||
# 微信公众号配置
|
||||
VITE_WX_APP_ID=your_dev_app_id
|
||||
VITE_WX_CORP_ID=your_dev_corp_id
|
||||
|
||||
# API 基础地址
|
||||
VITE_BASE_URL=https://apifoxmock.com/m1/2930465-2145633-default
|
||||
```
|
||||
|
||||
#### 生产环境(`.env.production`)
|
||||
|
||||
```env
|
||||
# 微信公众号配置
|
||||
VITE_WX_APP_ID=your_prod_app_id
|
||||
VITE_WX_CORP_ID=your_prod_corp_id
|
||||
|
||||
# API 基础地址
|
||||
VITE_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
### 6.2 微信环境检测(`src/common/utils/wx.ts`)
|
||||
|
||||
```typescript
|
||||
function checkInWeChat(targetUrl: string) {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (!ua.includes('micromessenger')) {
|
||||
// 不在微信内,提示用户在微信中打开
|
||||
const weChatUrl = `weixin://dl/business/?t=${encodeURIComponent(targetUrl)}`;
|
||||
window.location.href = weChatUrl;
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.hidden) return;
|
||||
alert("未检测到微信,请手动打开微信并访问链接。");
|
||||
}, 2000);
|
||||
} else {
|
||||
console.log("已在微信内,无需跳转");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、路由守卫与权限控制
|
||||
|
||||
### 7.1 登录检查(`src/router/guard.ts`)
|
||||
|
||||
```typescript
|
||||
router.beforeEach((to, _from) => {
|
||||
// 检测企业微信登录参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const corpid = urlParams.get('corpid') || undefined;
|
||||
if (corpid) return true;
|
||||
|
||||
const isAdmin = urlParams.get('isAdmin') || undefined;
|
||||
if (isAdmin) return true;
|
||||
|
||||
// 检查 AB98 用户登录状态
|
||||
const ab98UserStore = useAb98UserStore();
|
||||
if (!ab98UserStore.isLogin) {
|
||||
if (isWhiteList(to)) return true; // 白名单页面直接访问
|
||||
return "/ab98"; // 重定向到登录页
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 等待认证完成
|
||||
|
||||
在页面组件中使用:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useWxStore } from '@/pinia/stores/wx';
|
||||
|
||||
const wxStore = useWxStore();
|
||||
|
||||
onMounted(async () => {
|
||||
const success = await wxStore.waitForHandleWxCallbackComplete();
|
||||
if (!success) {
|
||||
console.error('微信认证超时');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!wxStore.isHandleWxCallbackComplete">
|
||||
<van-loading>微信认证中...</van-loading>
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 页面内容 -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、错误处理与调试
|
||||
|
||||
### 8.1 常见错误
|
||||
|
||||
#### 1. 微信认证失败
|
||||
|
||||
**可能原因**:
|
||||
- 回调域名未在微信公众平台配置
|
||||
- AppID 或 Secret 错误
|
||||
- 网络问题导致 API 调用失败
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
const retryWxAuth = async (maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await wxStore.handleWxCallback(params);
|
||||
if (wxStore.isHandleWxCallbackComplete) break;
|
||||
} catch (error) {
|
||||
console.error(`微信认证重试 ${i + 1} 失败:`, error);
|
||||
if (i === maxRetries - 1) throw error;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. OpenID 获取失败
|
||||
|
||||
**调试方法**:
|
||||
```typescript
|
||||
const debugWxAuth = async () => {
|
||||
console.log('当前 URL 参数:', window.location.search);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
|
||||
if (!code) {
|
||||
console.error('未获取到微信 code');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('获取到 code:', code);
|
||||
|
||||
try {
|
||||
const response = await getOpenIdApi({ code });
|
||||
console.log('OpenID 响应:', response);
|
||||
} catch (error) {
|
||||
console.error('获取 OpenID 失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 8.2 日志输出
|
||||
|
||||
在关键位置添加日志:
|
||||
|
||||
```typescript
|
||||
const handleWxCallback = async (params: any) => {
|
||||
console.log('=== 微信回调开始 ===');
|
||||
console.log('参数:', params);
|
||||
console.log('corpid:', corpid.value);
|
||||
console.log('corpidLogin:', corpidLogin.value);
|
||||
|
||||
// ... 处理逻辑
|
||||
|
||||
console.log('=== 微信回调完成 ===');
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、最佳实践
|
||||
|
||||
### 9.1 安全性
|
||||
|
||||
1. **CSRF 防护**
|
||||
- 使用 `state` 参数防止 CSRF 攻击
|
||||
- 生成随机字符串作为 `state` 值
|
||||
|
||||
2. **Token 加密**
|
||||
```typescript
|
||||
const encryptToken = (token: string) => {
|
||||
return btoa(encodeURIComponent(token));
|
||||
};
|
||||
```
|
||||
|
||||
3. **敏感信息存储**
|
||||
- 避免在 localStorage 中存储敏感信息
|
||||
- 使用加密后存储
|
||||
|
||||
### 9.2 性能优化
|
||||
|
||||
1. **减少 API 调用**
|
||||
```typescript
|
||||
const cachedUserInfo = ref<ab98UserDTO | null>(null);
|
||||
|
||||
const getUserInfo = async (forceRefresh = false) => {
|
||||
if (cachedUserInfo.value && !forceRefresh) {
|
||||
return cachedUserInfo.value;
|
||||
}
|
||||
const userInfo = await fetchUserInfo();
|
||||
cachedUserInfo.value = userInfo;
|
||||
return userInfo;
|
||||
};
|
||||
```
|
||||
|
||||
2. **并行请求**
|
||||
```typescript
|
||||
const loadUserData = async () => {
|
||||
const [userInfo, balanceInfo] = await Promise.all([
|
||||
getUserInfo(),
|
||||
getBalanceInfo()
|
||||
]);
|
||||
return { userInfo, balanceInfo };
|
||||
};
|
||||
```
|
||||
|
||||
### 9.3 用户体验
|
||||
|
||||
1. **加载状态提示**
|
||||
```vue
|
||||
<template>
|
||||
<van-loading v-if="loading">认证中...</van-loading>
|
||||
<div v-else>页面内容</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
2. **错误提示**
|
||||
```typescript
|
||||
const handleError = (error: any) => {
|
||||
Toast.fail(error.message || '操作失败');
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、测试
|
||||
|
||||
### 10.1 单元测试
|
||||
|
||||
```typescript
|
||||
// tests/stores/wx.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
import { useWxStore } from "@/pinia/stores/wx";
|
||||
|
||||
describe("Wx Store", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("应该处理微信回调", async () => {
|
||||
const wxStore = useWxStore();
|
||||
await wxStore.handleWxCallback({
|
||||
code: "test_code",
|
||||
state: "test_state"
|
||||
});
|
||||
expect(wxStore.code).toBe("test_code");
|
||||
expect(wxStore.state).toBe("test_state");
|
||||
});
|
||||
|
||||
it("应该进行 fake 登录", async () => {
|
||||
const wxStore = useWxStore();
|
||||
await wxStore.fakeQyLogin();
|
||||
expect(wxStore.isFakeQyLogin).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 10.2 集成测试
|
||||
|
||||
```typescript
|
||||
// tests/integration/login.test.ts
|
||||
describe("登录流程集成测试", () => {
|
||||
it("应该完成完整的微信认证流程", async () => {
|
||||
// 1. 模拟微信回调
|
||||
const params = { code: "test_code", state: "test_state" };
|
||||
|
||||
// 2. 处理回调
|
||||
await wxStore.handleWxCallback(params);
|
||||
|
||||
// 3. 验证状态更新
|
||||
expect(wxStore.isHandleWxCallbackComplete).toBe(true);
|
||||
|
||||
// 4. 检查用户信息
|
||||
expect(wxStore.openid).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、总结
|
||||
|
||||
### 11.1 三种登录方式对比
|
||||
|
||||
| 特性 | 微信登录 | 企业微信登录 | Fake登录 |
|
||||
|------|----------|--------------|----------|
|
||||
| 触发方式 | 微信授权回调 | 企业微信授权回调 | 手动调用 |
|
||||
| 依赖 | 微信公众号 | 企业微信 | 无 |
|
||||
| 适用环境 | 生产/测试 | 生产/测试 | 仅测试 |
|
||||
| 获取openid | ✓ | ✓ | ✓(模拟) |
|
||||
| 获取用户信息 | ✓ | ✓ | ✓(模拟) |
|
||||
| 获取余额 | ✓ | ✓ | ✓(模拟) |
|
||||
| 管理员权限 | 根据后端返回 | 根据后端返回 | 根据后端返回 |
|
||||
|
||||
### 11.2 关键要点
|
||||
|
||||
1. **微信登录**:使用 `snsapi_userinfo` 或 `snsapi_base` 授权,通过 `code` 获取 `openid`
|
||||
2. **企业微信登录**:需要 `corpid` 参数,通过 `code` 获取企业微信用户信息
|
||||
3. **Fake登录**:仅用于测试环境,直接使用预设的测试账号
|
||||
4. **状态管理**:使用 Pinia 管理登录状态和用户信息
|
||||
5. **余额系统**:通过 `balance`、`useBalance`、`balanceLimit` 管理借呗额度
|
||||
6. **AB98集成**:三种登录方式最终都会关联到汇邦云用户系统
|
||||
|
||||
### 11.3 开发注意事项
|
||||
|
||||
1. 确保在微信/企业微信环境下测试
|
||||
2. 正确配置回调域名
|
||||
3. 使用 `state` 参数防止 CSRF 攻击
|
||||
4. 合理处理 API 调用错误
|
||||
5. 在测试环境使用 Fake 登录提高效率
|
||||
6. 遵循状态管理最佳实践
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**最后更新**: 2025-11-07
|
||||
**维护者**: 智柜宝开发团队
|
||||
Loading…
Reference in New Issue