shop-web/doc/微信集成文档.md

646 lines
15 KiB
Markdown
Raw Permalink Normal View History

# 微信集成文档
## 概述
本文档详细描述了 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 接口、配置说明和常见问题解决方案。