646 lines
15 KiB
Markdown
646 lines
15 KiB
Markdown
# 微信集成文档
|
||
|
||
## 概述
|
||
|
||
本文档详细描述了 MobVue 项目中微信和企业微信的集成方案,包括认证流程、API 调用、状态管理等。
|
||
|
||
## 微信认证流程
|
||
|
||
### 1. 微信公众号认证
|
||
|
||
#### 认证流程图
|
||
```
|
||
用户访问应用
|
||
↓
|
||
检查是否已认证
|
||
↓
|
||
未认证 → 跳转微信授权页面
|
||
↓
|
||
用户授权
|
||
↓
|
||
微信回调携带 code
|
||
↓
|
||
使用 code 换取 openid
|
||
↓
|
||
获取用户信息
|
||
↓
|
||
完成认证
|
||
```
|
||
|
||
#### 代码实现
|
||
```typescript
|
||
// src/App.vue - 微信回调处理
|
||
onMounted(() => {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const code = urlParams.get('code');
|
||
const state = urlParams.get('state');
|
||
|
||
if (code) {
|
||
wxStore.handleWxCallback({ code, state });
|
||
}
|
||
});
|
||
```
|
||
|
||
### 2. 企业微信认证
|
||
|
||
#### 认证流程图
|
||
```
|
||
用户访问应用
|
||
↓
|
||
检查企业微信环境
|
||
↓
|
||
跳转企业微信授权
|
||
↓
|
||
用户授权
|
||
↓
|
||
企业微信回调携带 code
|
||
↓
|
||
使用 code 换取用户信息
|
||
↓
|
||
获取企业微信用户详情
|
||
↓
|
||
关联 AB98 用户系统
|
||
↓
|
||
完成认证
|
||
```
|
||
|
||
#### 代码实现
|
||
```typescript
|
||
// src/pinia/stores/wx.ts - 企业微信登录
|
||
const qyLogin = async ({ corpid, code, state }: QyLoginParams) => {
|
||
const res = await qyLoginApi({ corpid, code, state });
|
||
if (res && res.code == 0) {
|
||
userid.value = res.data.userid;
|
||
openid.value = res.data.openid;
|
||
isCabinetAdmin.value = res.data.isCabinetAdmin === 1;
|
||
name.value = res.data.name;
|
||
qyUserId.value = res.data.qyUserId;
|
||
setAb98User(res.data.ab98User);
|
||
}
|
||
};
|
||
```
|
||
|
||
## 状态管理
|
||
|
||
### 微信状态 Store (src/pinia/stores/wx.ts)
|
||
|
||
#### 状态定义
|
||
```typescript
|
||
export const useWxStore = defineStore("wx", () => {
|
||
// 微信授权 code
|
||
const code = ref<string>("");
|
||
|
||
// 防止 CSRF 攻击的 state 参数
|
||
const state = ref<string>("");
|
||
|
||
// 用户 openid
|
||
const openid = ref<string>("");
|
||
|
||
// 用户 userid
|
||
const userid = ref<string>("");
|
||
|
||
// 企业 ID
|
||
const corpid = ref<string>("");
|
||
|
||
// 是否企业微信登录
|
||
const corpidLogin = ref<boolean>(false);
|
||
|
||
// 是否是柜子管理员
|
||
const isCabinetAdmin = ref<boolean>(false);
|
||
|
||
// 企业微信用户姓名
|
||
const name = ref<string>("");
|
||
|
||
// 企业微信用户 ID
|
||
const qyUserId = ref<number>(0);
|
||
|
||
// 汇邦云用户信息
|
||
const ab98User = ref<ab98UserDTO | null>(null);
|
||
|
||
// 余额信息
|
||
const balance = ref<number>(0);
|
||
const useBalance = ref<number>(0);
|
||
const balanceLimit = ref<number>(0);
|
||
|
||
// 处理状态
|
||
const isHandleWxCallbackComplete = ref<boolean>(false);
|
||
});
|
||
```
|
||
|
||
#### 核心方法
|
||
```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 {
|
||
if (!corpid.value) {
|
||
// 微信公众号登录
|
||
const res = await getOpenIdApi({ code: params.code });
|
||
if (res && res.code == 0) {
|
||
openid.value = res.data;
|
||
}
|
||
} else {
|
||
// 企业微信登录
|
||
await qyLogin({
|
||
corpid: corpid.value,
|
||
code: params.code,
|
||
state: params.state
|
||
});
|
||
}
|
||
|
||
// 获取余额信息
|
||
if (openid.value) {
|
||
await refreshBalance();
|
||
}
|
||
} finally {
|
||
isHandleWxCallbackComplete.value = true;
|
||
}
|
||
} else {
|
||
isHandleWxCallbackComplete.value = true;
|
||
}
|
||
};
|
||
|
||
// 刷新余额
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### 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>("");
|
||
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'; // 默认验证码
|
||
});
|
||
```
|
||
|
||
#### 核心方法
|
||
```typescript
|
||
// 设置用户信息
|
||
const setUserInfo = (data: LoginData) => {
|
||
face_img.value = data.face_img;
|
||
localStorage.setItem(STORAGE_KEYS.FACE, encodeURIComponent(data.face_img));
|
||
sex.value = data.sex;
|
||
localStorage.setItem(STORAGE_KEYS.SEX, encodeURIComponent(data.sex));
|
||
name.value = data.name;
|
||
localStorage.setItem(STORAGE_KEYS.NAME, encodeURIComponent(data.name));
|
||
userid.value = data.userid;
|
||
localStorage.setItem(STORAGE_KEYS.USERID, encodeURIComponent(data.userid));
|
||
registered.value = data.registered;
|
||
localStorage.setItem(STORAGE_KEYS.REGISTERED, JSON.stringify(data.registered));
|
||
tel.value = data.tel;
|
||
localStorage.setItem(STORAGE_KEYS.TEL, encodeURIComponent(data.tel));
|
||
localStorage.setItem(STORAGE_KEYS.LOGIN_CODE, encodeURIComponent(loginCode));
|
||
};
|
||
|
||
// Token 登录
|
||
const tokenLogin = async (token: string, userid: string, openid: string) => {
|
||
const res = await tokenLoginApi({ token, userid, openid });
|
||
if (res?.code === 0 && res.data?.success) {
|
||
setTel(res.data.tel);
|
||
setUserInfo(res.data);
|
||
if (!isLogin.value) {
|
||
setIsLogin(true);
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
## API 接口
|
||
|
||
### 微信相关接口 (src/common/apis/shop/)
|
||
|
||
#### 获取 OpenID
|
||
```typescript
|
||
// 接口: GET /api/v1/wx/getOpenId
|
||
export const getOpenIdApi = (params: { code: string }) => {
|
||
return request<string>({
|
||
url: "/api/v1/wx/getOpenId",
|
||
method: "GET",
|
||
params
|
||
});
|
||
};
|
||
```
|
||
|
||
#### 企业微信登录
|
||
```typescript
|
||
// 接口: POST /api/v1/wx/qyLogin
|
||
export const qyLogin = (params: {
|
||
corpid: string;
|
||
code: string;
|
||
state?: string;
|
||
}) => {
|
||
return request<QyLoginResponse>({
|
||
url: "/api/v1/wx/qyLogin",
|
||
method: "POST",
|
||
data: params
|
||
});
|
||
};
|
||
```
|
||
|
||
#### 获取余额
|
||
```typescript
|
||
// 接口: GET /api/v1/balance
|
||
export const getBalanceApi = (corpid: string, openid: string) => {
|
||
return request<GetBalanceResponse>({
|
||
url: "/api/v1/balance",
|
||
method: "GET",
|
||
params: { corpid, openid }
|
||
});
|
||
};
|
||
|
||
// 企业微信用户余额
|
||
// 接口: GET /api/v1/balance/qyUser
|
||
export const getBalanceByQyUserid = (corpid: string, userid: string) => {
|
||
return request<GetBalanceResponse>({
|
||
url: "/api/v1/balance/qyUser",
|
||
method: "GET",
|
||
params: { corpid, userid }
|
||
});
|
||
};
|
||
```
|
||
|
||
### AB98 系统接口 (src/common/apis/ab98/)
|
||
|
||
#### Token 登录
|
||
```typescript
|
||
// 接口: POST /api/v1/ab98/tokenLogin
|
||
export const tokenLogin = (params: {
|
||
token: string;
|
||
userid: string;
|
||
openid: string;
|
||
}) => {
|
||
return request<LoginData>({
|
||
url: "/api/v1/ab98/tokenLogin",
|
||
method: "POST",
|
||
data: params
|
||
});
|
||
};
|
||
```
|
||
|
||
## 配置说明
|
||
|
||
### 微信配置
|
||
|
||
#### 公众号配置
|
||
```typescript
|
||
// 微信公众号配置
|
||
const wxConfig = {
|
||
appId: 'YOUR_APP_ID',
|
||
redirectUri: encodeURIComponent('https://your-domain.com/callback'),
|
||
scope: 'snsapi_userinfo', // 或 snsapi_base
|
||
state: 'STATE_PARAM'
|
||
};
|
||
|
||
// 生成授权 URL
|
||
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${wxConfig.redirectUri}&response_type=code&scope=${wxConfig.scope}&state=${wxConfig.state}#wechat_redirect`;
|
||
```
|
||
|
||
#### 企业微信配置
|
||
```typescript
|
||
// 企业微信配置
|
||
const qywxConfig = {
|
||
corpid: 'YOUR_CORP_ID',
|
||
agentId: 'YOUR_AGENT_ID',
|
||
redirectUri: encodeURIComponent('https://your-domain.com/qy-callback'),
|
||
state: 'STATE_PARAM'
|
||
};
|
||
|
||
// 生成企业微信授权 URL
|
||
const qyAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${qywxConfig.corpid}&redirect_uri=${qywxConfig.redirectUri}&response_type=code&scope=snsapi_base&state=${qywxConfig.state}#wechat_redirect`;
|
||
```
|
||
|
||
### 环境变量配置
|
||
|
||
#### 开发环境 (.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
|
||
```
|
||
|
||
## 使用示例
|
||
|
||
### 1. 页面中使用微信状态
|
||
|
||
```vue
|
||
<template>
|
||
<div class="user-info">
|
||
<!-- 显示用户信息 -->
|
||
<div v-if="ab98UserStore.isLogin">
|
||
<img :src="ab98UserStore.face_img" class="avatar" />
|
||
<div class="info">
|
||
<h3>{{ ab98UserStore.name }}</h3>
|
||
<p>手机号: {{ ab98UserStore.tel }}</p>
|
||
<p>余额: ¥{{ wxStore.balance }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未登录状态 -->
|
||
<div v-else class="login-prompt">
|
||
<p>请先登录</p>
|
||
<button @click="handleLogin">微信登录</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useWxStore } from '@/pinia/stores/wx';
|
||
import { useAb98UserStore } from '@/pinia/stores/ab98-user';
|
||
|
||
const wxStore = useWxStore();
|
||
const ab98UserStore = useAb98UserStore();
|
||
|
||
// 处理登录
|
||
const handleLogin = () => {
|
||
// 跳转到微信授权页面
|
||
const authUrl = generateWxAuthUrl();
|
||
window.location.href = authUrl;
|
||
};
|
||
|
||
// 生成微信授权 URL
|
||
const generateWxAuthUrl = () => {
|
||
const appId = import.meta.env.VITE_WX_APP_ID;
|
||
const redirectUri = encodeURIComponent(window.location.origin + '/callback');
|
||
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=wx_auth#wechat_redirect`;
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 2. 等待微信回调完成
|
||
|
||
```vue
|
||
<template>
|
||
<div class="loading-container" v-if="!wxStore.isHandleWxCallbackComplete">
|
||
<van-loading>微信认证中...</van-loading>
|
||
</div>
|
||
|
||
<div v-else>
|
||
<!-- 正常页面内容 -->
|
||
<ProductList />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useWxStore } from '@/pinia/stores/wx';
|
||
|
||
const wxStore = useWxStore();
|
||
|
||
// 等待微信回调处理完成
|
||
const waitForWxCallback = async () => {
|
||
const success = await wxStore.waitForHandleWxCallbackComplete();
|
||
if (!success) {
|
||
// 处理超时情况
|
||
console.error('微信认证超时');
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
waitForWxCallback();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
### 1. 微信认证失败
|
||
|
||
#### 可能原因
|
||
- 回调域名未配置
|
||
- AppID 或 Secret 错误
|
||
- 网络问题导致 API 调用失败
|
||
|
||
#### 解决方案
|
||
```typescript
|
||
// 检查回调域名
|
||
const checkCallbackDomain = () => {
|
||
const currentDomain = window.location.hostname;
|
||
// 确保当前域名在微信白名单中
|
||
};
|
||
|
||
// 重试机制
|
||
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);
|
||
|
||
// 手动调用获取 OpenID
|
||
try {
|
||
const response = await getOpenIdApi({ code });
|
||
console.log('OpenID 响应:', response);
|
||
} catch (error) {
|
||
console.error('获取 OpenID 失败:', error);
|
||
}
|
||
};
|
||
```
|
||
|
||
### 3. 余额信息不更新
|
||
|
||
#### 刷新策略
|
||
```typescript
|
||
// 定期刷新余额
|
||
const startBalanceRefresh = () => {
|
||
// 每 30 秒刷新一次余额
|
||
setInterval(() => {
|
||
if (wxStore.corpid && wxStore.userid) {
|
||
wxStore.refreshBalance();
|
||
}
|
||
}, 30000);
|
||
};
|
||
|
||
// 在关键操作后刷新余额
|
||
const afterOrderCreate = async () => {
|
||
// 创建订单后刷新余额
|
||
await wxStore.refreshBalance();
|
||
};
|
||
```
|
||
|
||
## 安全考虑
|
||
|
||
### 1. Token 安全
|
||
|
||
#### 存储安全
|
||
```typescript
|
||
// 敏感信息加密存储
|
||
const encryptToken = (token: string) => {
|
||
return btoa(encodeURIComponent(token));
|
||
};
|
||
|
||
const decryptToken = (encrypted: string) => {
|
||
return decodeURIComponent(atob(encrypted));
|
||
};
|
||
```
|
||
|
||
### 2. 防 CSRF 攻击
|
||
|
||
#### State 参数验证
|
||
```typescript
|
||
// 生成随机的 state 参数
|
||
const generateState = () => {
|
||
return Math.random().toString(36).substring(2, 15);
|
||
};
|
||
|
||
// 验证 state 参数
|
||
const validateState = (receivedState: string, expectedState: string) => {
|
||
return receivedState === expectedState;
|
||
};
|
||
```
|
||
|
||
## 性能优化
|
||
|
||
### 1. 减少不必要的 API 调用
|
||
|
||
```typescript
|
||
// 缓存用户信息
|
||
const cachedUserInfo = ref<Ab98UserDTO | null>(null);
|
||
|
||
const getUserInfo = async (forceRefresh = false) => {
|
||
if (cachedUserInfo.value && !forceRefresh) {
|
||
return cachedUserInfo.value;
|
||
}
|
||
|
||
// 调用 API 获取用户信息
|
||
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 };
|
||
};
|
||
```
|
||
|
||
## 测试指南
|
||
|
||
### 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("should handle wx callback", 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");
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2. 集成测试
|
||
|
||
```typescript
|
||
// tests/integration/wx-auth.test.ts
|
||
import { describe, it, expect } from "vitest";
|
||
|
||
describe("微信认证集成测试", () => {
|
||
it("应该完成完整的微信认证流程", async () => {
|
||
// 模拟微信回调
|
||
// 验证状态更新
|
||
// 检查用户信息
|
||
});
|
||
});
|
||
```
|
||
|
||
通过本文档,您可以全面了解 MobVue 项目中微信集成的各个方面,包括认证流程、状态管理、API 接口、配置说明和常见问题解决方案。 |