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

646 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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