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