23 KiB
23 KiB
微信登录、企业微信登录与Fake登录流程详细文档
文档概述
本文档详细描述了智柜宝系统中三种登录方式的完整流程、参数说明、返回信息及实现逻辑。
一、微信登录(普通微信公众号登录)
1.1 登录流程图
用户访问应用
↓
检查微信环境(micromessenger)
↓
跳转微信授权页面
↓
用户授权
↓
微信回调携带 code 和 state
↓
调用 getOpenIdApi 换取 openid
↓
调用 getBalanceApi 获取余额
↓
完成认证
1.2 核心实现
前端回调处理(src/main.ts 或页面组件)
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)
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
请求参数:
interface GetOpenIdRequestParams {
code: string; // 微信授权后返回的授权码
}
返回数据:
interface ApiResponseData<T> {
code: number; // 状态码:0 表示成功
msg: string; // 消息
data: string; // openid 字符串
}
示例响应:
{
"code": 0,
"msg": "success",
"data": "oMRxw6Eum0DB1IjI_pEX_yrawBHw"
}
获取余额
接口地址: GET /api/v1/payment/getBalance
请求参数:
{
corpid: string; // 企业ID
openid: string; // 微信用户唯一标识
}
返回数据:
interface GetBalanceResponse {
userid: string; // 系统用户ID
corpid: string; // 企业ID
balance: number; // 剩余借呗
useBalance: number; // 已用借呗
balanceLimit: number; // 借呗总额
ab98User: ab98UserDTO; // 汇邦云用户信息
}
示例响应:
{
"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 生成
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: 微信公众号的 AppIDredirect_uri: 授权后回调的网址(需 URL 编码)scope: 授权范围snsapi_base: 静默授权,只能获取 openidsnsapi_userinfo: 需用户授权,可获取用户信息
state: 自定义参数,用于防止 CSRF 攻击
二、企业微信登录
2.1 登录流程图
用户访问应用
↓
检测 URL 中的 corpid 参数
↓
跳转企业微信授权页面
↓
用户授权
↓
企业微信回调携带 corpid、code、state
↓
调用 qyLogin 验证并获取用户信息
↓
调用 getBalanceByQyUserid 获取余额
↓
关联 AB98 用户系统
↓
完成认证
2.2 核心实现
路由守卫处理(src/router/guard.ts:22-31)
// 企业微信登录参数检测
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)
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)
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
请求参数:
interface QyLoginRequestParams {
corpid: string; // 企业ID
code: string; // 企业微信授权后返回的授权码
state?: string; // 状态参数(防CSRF)
}
返回数据:
interface QyLoginDTO {
userid: string; // 系统用户ID
openid: string; // 微信 openid
isCabinetAdmin: number; // 是否为柜子管理员(1是 0否)
qyUserId: number; // 企业微信用户ID
name: string; // 用户姓名
ab98User: ab98UserDTO; // 汇邦云用户信息
}
示例响应:
{
"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
请求参数:
{
corpid: string; // 企业ID
userid: string; // 系统用户ID
}
返回数据: 同 GetBalanceResponse 结构
2.4 企业微信授权 URL 生成
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)
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
请求参数:
{
corpid: string; // 企业ID
userid: string; // 系统用户ID
}
返回数据: 与 QyLoginDTO 相同
示例响应:
{
"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 调用方式
在页面中调用
import { useWxStore } from '@/pinia/stores/wx';
const wxStore = useWxStore();
// 触发 fake 登录
const handleFakeLogin = () => {
wxStore.fakeQyLogin();
};
在组件模板中调用
<template>
<van-button @click="wxStore.fakeQyLogin">
模拟登录
</van-button>
</template>
四、状态管理(Pinia Store)
4.1 WxStore 状态定义(src/pinia/stores/wx.ts:7-38)
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)
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 用户信息
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 || "",
});
};
等待回调完成
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
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 通用响应类型
interface ApiResponseData<T> {
code: number; // 状态码:0表示成功,其他值表示失败
msg: string; // 消息
data: T; // 数据
}
interface ApiResponseMsgData<T> {
code: number; // 状态码
msg: string; // 消息
data: T; // 数据
}
六、环境配置
6.1 环境变量
开发环境(.env.development)
# 微信公众号配置
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)
# 微信公众号配置
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)
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)
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 等待认证完成
在页面组件中使用:
<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 调用失败
解决方案:
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 获取失败
调试方法:
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 日志输出
在关键位置添加日志:
const handleWxCallback = async (params: any) => {
console.log('=== 微信回调开始 ===');
console.log('参数:', params);
console.log('corpid:', corpid.value);
console.log('corpidLogin:', corpidLogin.value);
// ... 处理逻辑
console.log('=== 微信回调完成 ===');
};
九、最佳实践
9.1 安全性
-
CSRF 防护
- 使用
state参数防止 CSRF 攻击 - 生成随机字符串作为
state值
- 使用
-
Token 加密
const encryptToken = (token: string) => { return btoa(encodeURIComponent(token)); }; -
敏感信息存储
- 避免在 localStorage 中存储敏感信息
- 使用加密后存储
9.2 性能优化
-
减少 API 调用
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; }; -
并行请求
const loadUserData = async () => { const [userInfo, balanceInfo] = await Promise.all([ getUserInfo(), getBalanceInfo() ]); return { userInfo, balanceInfo }; };
9.3 用户体验
-
加载状态提示
<template> <van-loading v-if="loading">认证中...</van-loading> <div v-else>页面内容</div> </template> -
错误提示
const handleError = (error: any) => { Toast.fail(error.message || '操作失败'); };
十、测试
10.1 单元测试
// 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 集成测试
// 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 关键要点
- 微信登录:使用
snsapi_userinfo或snsapi_base授权,通过code获取openid - 企业微信登录:需要
corpid参数,通过code获取企业微信用户信息 - Fake登录:仅用于测试环境,直接使用预设的测试账号
- 状态管理:使用 Pinia 管理登录状态和用户信息
- 余额系统:通过
balance、useBalance、balanceLimit管理借呗额度 - AB98集成:三种登录方式最终都会关联到汇邦云用户系统
11.3 开发注意事项
- 确保在微信/企业微信环境下测试
- 正确配置回调域名
- 使用
state参数防止 CSRF 攻击 - 合理处理 API 调用错误
- 在测试环境使用 Fake 登录提高效率
- 遵循状态管理最佳实践
文档版本: v1.0 最后更新: 2025-11-07 维护者: 智柜宝开发团队