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

15 KiB
Raw Blame History

微信集成文档

概述

本文档详细描述了 MobVue 项目中微信和企业微信的集成方案包括认证流程、API 调用、状态管理等。

微信认证流程

1. 微信公众号认证

认证流程图

用户访问应用
    ↓
检查是否已认证
    ↓
未认证 → 跳转微信授权页面
    ↓
用户授权
    ↓
微信回调携带 code
    ↓
使用 code 换取 openid
    ↓
获取用户信息
    ↓
完成认证

代码实现

// 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 用户系统
    ↓
完成认证

代码实现

// 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)

状态定义

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);
});

核心方法

// 处理微信回调
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)

状态定义

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'; // 默认验证码
});

核心方法

// 设置用户信息
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

// 接口: GET /api/v1/wx/getOpenId
export const getOpenIdApi = (params: { code: string }) => {
  return request<string>({
    url: "/api/v1/wx/getOpenId",
    method: "GET",
    params
  });
};

企业微信登录

// 接口: 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
  });
};

获取余额

// 接口: 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 登录

// 接口: 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
  });
};

配置说明

微信配置

公众号配置

// 微信公众号配置
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`;

企业微信配置

// 企业微信配置
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)

# 微信配置
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

使用示例

1. 页面中使用微信状态

<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. 等待微信回调完成

<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 调用失败

解决方案

// 检查回调域名
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 获取失败

调试方法

// 检查网络请求
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. 余额信息不更新

刷新策略

// 定期刷新余额
const startBalanceRefresh = () => {
  // 每 30 秒刷新一次余额
  setInterval(() => {
    if (wxStore.corpid && wxStore.userid) {
      wxStore.refreshBalance();
    }
  }, 30000);
};

// 在关键操作后刷新余额
const afterOrderCreate = async () => {
  // 创建订单后刷新余额
  await wxStore.refreshBalance();
};

安全考虑

1. Token 安全

存储安全

// 敏感信息加密存储
const encryptToken = (token: string) => {
  return btoa(encodeURIComponent(token));
};

const decryptToken = (encrypted: string) => {
  return decodeURIComponent(atob(encrypted));
};

2. 防 CSRF 攻击

State 参数验证

// 生成随机的 state 参数
const generateState = () => {
  return Math.random().toString(36).substring(2, 15);
};

// 验证 state 参数
const validateState = (receivedState: string, expectedState: string) => {
  return receivedState === expectedState;
};

性能优化

1. 减少不必要的 API 调用

// 缓存用户信息
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. 并行请求优化

// 并行获取用户信息和余额
const loadUserData = async () => {
  const [userInfo, balanceInfo] = await Promise.all([
    getUserInfo(),
    getBalanceInfo()
  ]);

  return { userInfo, balanceInfo };
};

测试指南

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("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. 集成测试

// tests/integration/wx-auth.test.ts
import { describe, it, expect } from "vitest";

describe("微信认证集成测试", () => {
  it("应该完成完整的微信认证流程", async () => {
    // 模拟微信回调
    // 验证状态更新
    // 检查用户信息
  });
});

通过本文档,您可以全面了解 MobVue 项目中微信集成的各个方面包括认证流程、状态管理、API 接口、配置说明和常见问题解决方案。