在企微工作台配置自建H5应用详解

273 阅读6分钟

1、企微工作台配置应用

1.1 准备工作

1)需要找企业管理员开通管理员权限,这样可以登录企业微信后台进行后续操作。

2)发布前端项目,需要域名访问,并且公共网络可访问到。

1.2 配置应用

登录到企业微信的管理后台:work.weixin.qq.com/wework_admi…

打开应用管理,找到自建的部分,点击【创建应用】。

填写应用名称、介绍和可见范围后,进入应用的配置界面。H5应用选择网页类型,网页网址的配置需要根据企微应用开发文档中的介绍进行配置,配置链接规则为:open.weixin.qq.com/connect/oau…

构造网页授权链接文档地址: developer.work.weixin.qq.com/document/pa…

配置的网页回调链接需要配置可信域名,找到开发者接口中的【网页授权及JS-SDK】,设置域名,点击【申请校验域名】,按照提示操作。可信域名中填写网页域名,例如:xxx.wangye.com。

2、企微服务端API

调用所有企微服务端的接口都需要access_token,获取token文档地址:developer.work.weixin.qq.com/document/pa…

调用企微的接口,需要将 调用方的IP地址在应用配置的企业可信IP中进行添加。

这里给出一个使用node.js编写的获取访问用户身份的接口的示例。ps:可以借助AI工具,把企微开发文档地址发给AI工具,让AI生成代码。

const express = require('express');
const axios = require('axios');
const app = express();
const port = process.env.PORT || 3000;

// 中间件
app.use(express.json());

// 企业微信配置信息
const config = {
  corpId: 'YOUR_CORP_ID', // 企业ID
  agentId: 'YOUR_AGENT_ID', // 应用ID
  secret: 'YOUR_SECRET', // 应用Secret
};

// 缓存对象
let cache = {
  accessToken: null,
  accessTokenExpires: 0,
  corpJsapiTicket: null,
  corpJsapiTicketExpires: 0,
  agentJsapiTicket: null,
  agentJsapiTicketExpires: 0
};

/**
 * 获取access_token(带缓存功能)
 * 文档: https://developer.work.weixin.qq.com/document/path/91039
 */
async function getAccessToken() {
  // 检查缓存中是否有未过期的access_token
  if (cache.accessToken && Date.now() < cache.accessTokenExpires) {
    return cache.accessToken;
  }

  try {
    const response = await axios.get(
      `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${config.corpId}&corpsecret=${config.secret}`
    );
    
    if (response.data.errcode === 0) {
      // 缓存access_token,提前5分钟过期
      cache.accessToken = response.data.access_token;
      cache.accessTokenExpires = Date.now() + (response.data.expires_in - 300) * 1000;
      
      return cache.accessToken;
    } else {
      throw new Error(`获取access_token失败: ${response.data.errmsg}`);
    }
  } catch (error) {
    console.error('获取access_token错误:', error);
    throw error;
  }
}

/**
 * 使用code获取用户信息
 * 文档: https://developer.work.weixin.qq.com/document/path/96442
 */
async function getUserInfoByCode(code) {
  try {
    const accessToken = await getAccessToken();
    const response = await axios.get(
      `https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=${accessToken}&code=${code}`
    );
    
    return response.data;
  } catch (error) {
    console.error('获取用户信息错误:', error);
    throw error;
  }
}

/**
 * 获取企业的jsapi_ticket
 * 文档: https://developer.work.weixin.qq.com/document/path/96909#获取企业-jsapi-ticket
 */
async function getCorpJsapiTicket() {
  // 检查缓存中是否有未过期的企业jsapi_ticket
  if (cache.corpJsapiTicket && Date.now() < cache.corpJsapiTicketExpires) {
    return cache.corpJsapiTicket;
  }

  try {
    const accessToken = await getAccessToken();
    const response = await axios.get(
      `https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=${accessToken}`
    );
    
    if (response.data.errcode === 0) {
      // 缓存企业jsapi_ticket,提前5分钟过期
      cache.corpJsapiTicket = response.data.ticket;
      cache.corpJsapiTicketExpires = Date.now() + (response.data.expires_in - 300) * 1000;
      
      return cache.corpJsapiTicket;
    } else {
      throw new Error(`获取企业jsapi_ticket失败: ${response.data.errmsg}`);
    }
  } catch (error) {
    console.error('获取企业jsapi_ticket错误:', error);
    throw error;
  }
}

/**
 * 获取应用的jsapi_ticket
 * 文档: https://developer.work.weixin.qq.com/document/path/96909#获取应用的-jsapi-ticket
 */
async function getAgentJsapiTicket() {
  // 检查缓存中是否有未过期的应用jsapi_ticket
  if (cache.agentJsapiTicket && Date.now() < cache.agentJsapiTicketExpires) {
    return cache.agentJsapiTicket;
  }

  try {
    const accessToken = await getAccessToken();
    const response = await axios.get(
      `https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=${accessToken}&type=agent_config`
    );
    
    if (response.data.errcode === 0) {
      // 缓存应用jsapi_ticket,提前5分钟过期
      cache.agentJsapiTicket = response.data.ticket;
      cache.agentJsapiTicketExpires = Date.now() + (response.data.expires_in - 300) * 1000;
      
      return cache.agentJsapiTicket;
    } else {
      throw new Error(`获取应用jsapi_ticket失败: ${response.data.errmsg}`);
    }
  } catch (error) {
    console.error('获取应用jsapi_ticket错误:', error);
    throw error;
  }
}

// 路由定义

/**
 * 直接通过code获取用户信息的API接口(前端调用)
 */
app.post('/api/wecom/getUserInfo', async (req, res) => {
  try {
    const { code } = req.body;
    
    if (!code) {
      return res.status(400).json({ error: '缺少code参数' });
    }
    
    const userInfo = await getUserInfoByCode(code);
    res.json({ success: true, data: userInfo });
    
  } catch (error) {
    console.error('获取用户信息错误:', error);
    res.status(500).json({ 
      success: false, 
      error: '获取用户信息失败',
      message: error.message 
    });
  }
});

/**
 * 获取企业JSAPI ticket接口
 * 文档: https://developer.work.weixin.qq.com/document/path/96909#获取企业-jsapi-ticket
 */
app.get('/api/wecom/ticket/corp', async (req, res) => {
  try {
    const ticket = await getCorpJsapiTicket();
    res.json({ 
      success: true, 
      data: {
        ticket: ticket,
        expires_in: Math.floor((cache.corpJsapiTicketExpires - Date.now()) / 1000)
      }
    });
  } catch (error) {
    console.error('获取企业JSAPI ticket错误:', error);
    res.status(500).json({ 
      success: false, 
      error: '获取企业JSAPI ticket失败',
      message: error.message 
    });
  }
});

/**
 * 获取应用JSAPI ticket接口
 * 文档: https://developer.work.weixin.qq.com/document/path/96909#获取应用的-jsapi-ticket
 */
app.get('/api/wecom/ticket/agent', async (req, res) => {
  try {
    const ticket = await getAgentJsapiTicket();
    res.json({ 
      success: true, 
      data: {
        ticket: ticket,
        expires_in: Math.floor((cache.agentJsapiTicketExpires - Date.now()) / 1000)
      }
    });
  } catch (error) {
    console.error('获取应用JSAPI ticket错误:', error);
    res.status(500).json({ 
      success: false, 
      error: '获取应用JSAPI ticket失败',
      message: error.message 
    });
  }
});



// 启动服务器
app.listen(port, () => {
  console.log(`服务器运行在端口 ${port}`);
  console.log('可用接口:');
  console.log('POST /api/wecom/getUserInfo - 通过code获取用户信息');
  console.log('GET  /api/wecom/ticket/corp - 获取企业JSAPI ticket');
  console.log('GET  /api/wecom/ticket/agent - 获取应用JSAPI ticket');
});

3、集成企微JS-SDK

如果需要使用企微提供的方法,例如预览图片、内置地图,获取定位等方法,则需要集成企微的JS-SDK。接入方法文档地址:developer.work.weixin.qq.com/document/pa…

3.1 获取企业 / 应用 jsapi_ticket

服务端需要提供获取企业 / 应用 jsapi_ticket的接口,上文中的代码中已给出了示例,企微文档地址:developer.work.weixin.qq.com/document/pa…

3.2 前端注册企微JSSDK

这里给出一个前端集成代码示例,先安装npm install @wecom/jssdk,创建一个weJsSDK.js文件,编写企微的集成方法。注意需要将需要使用的JS接口列表配置到ww.register中的jsApiList中。

// src\utils\weJsSDK.js
import * as ww from '@wecom/jssdk'

// 这两个方法是分别是获取企业jsapi_ticket和获取应用jsapi_ticket的,需要调用上文中的两个接口
import { getJsapiTicket, getJsapiAgentTicket } from '@/api/server'


// 保存注册配置的Promise,避免重复注册
let registrationPromise = null;
/**
 * 初始化企业微信JS-SDK
 * @returns {Promise} 注册结果的Promise
 */
async function initWeComSDK() {
    // 如果已经在注册中或已注册,返回同一个Promise
    if (registrationPromise) {
        return registrationPromise;
    }
    registrationPromise = (async () => {
        try {
            // 获取当前页面的URL(去除#后面的部分)
            const url = getCurrentUrl();            
            // 注册SDK
            await ww.register({
                corpId: '企业的corpId',
                agentId: '应用的agentId',
                jsApiList: ['getExternalContact', 'openLocation'], // 需要使用的JS接口列表
                getConfigSignature: () => getConfigSignature(url),
                getAgentConfigSignature: () => getAgentConfigSignature(url)                
            
            });
            console.log('企业微信JS-SDK注册成功');
            return true;
        } catch (error) {
            console.error('企业微信JS-SDK注册失败:', error);
            registrationPromise = null; // 重置以便重试
            throw error;
        }
    })();

    return registrationPromise;
}
/**
* 获取当前页面URL(去除#及其后面部分)
*/
function getCurrentUrl() {
    return window.location.href.split('#')[0];
}
async function getConfigSignature(url) {
    // 根据 url 生成企业签名
    const res = await getJsapiTicket(); // 你的获取企业jsapi_ticket的方法
    return generateSignature(res.ticket, url);
}

async function getAgentConfigSignature(url) {
    // 根据 url 生成应用签名,生成方法同上,注意此处使用应用的 jsapi_ticket
    const res = await getJsapiAgentTicket(); // 你的获取企业jsapi_ticket的方法
    return generateSignature(res.ticket, url);
}
/**
* 生成JS-SDK签名
* @param {String} jsapiTicket - 通过接口获取的jsapi_ticket(企业或应用的)
* @param {String} url - 当前网页的URL,不包含#及其后面部分
* @returns {Object} 签名结果对象,包含timestamp, nonceStr, signature
*/
async function generateSignature(jsapiTicket, url) {
    // 1. 生成随机字符串 (16位)
    const nonceStr = generateNonceStr(16);
    // 2. 生成时间戳 (秒级)
    const timestamp = Math.floor(Date.now() / 1000);
    // 3. 参数排序并拼接字符串
    const string1 = generateSortedString(jsapiTicket, nonceStr, timestamp, url);
    // 4. 使用SHA1加密生成签名
    const signature = await sha1Hash(string1);
    return {
        timestamp,
        nonceStr,
        signature
    };
}
/**
* 生成随机字符串
* @param {Number} length - 随机字符串长度
* @returns {String} 随机字符串
*/
function generateNonceStr(length = 16) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let result = '';
    for (let i = 0; i < length; i++) {
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}
/**
 * 生成排序后的参数字符串
 * @param {String} jsapiTicket - jsapi_ticket
 * @param {String} nonceStr - 随机字符串
 * @param {Number} timestamp - 时间戳
 * @param {String} url - 当前网页URL
 * @returns {String} 排序拼接后的字符串
 */
function generateSortedString(jsapiTicket, nonceStr, timestamp, url) {
    // 参数对象(key必须小写)
    const params = {
        jsapi_ticket: jsapiTicket,
        noncestr: nonceStr,
        timestamp: timestamp.toString(),
        url: url
    };
    // 按照key的ASCII码从小到大排序(字典序)
    const sortedKeys = Object.keys(params).sort();
    // 拼接成URL键值对格式
    const string1 = sortedKeys.map(key => `${key}=${params[key]}`).join('&');
    return string1;
}
async function sha1Hash(str) {
    // 将字符串编码为 Uint8Array
    const encoder = new TextEncoder();
    const data = encoder.encode(str);
    // 使用 Web Crypto API 计算 SHA-1 哈希
    const hashBuffer = await window.crypto.subtle.digest('SHA-1', data);
    // 将 ArrayBuffer 转换为十六进制字符串
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}
// 导出SDK实例和初始化方法
export { ww, initWeComSDK };

在前端项目入口文件中,调用初始化方法。

import { ww, initWeComSDK } from '@/utils/weJsSDK';
initWeComSDK()
    .then(() => {
        console.log('企业微信SDK已就绪')
    })
    .catch((error) => {
        console.warn('企业微信SDK初始化失败:', error);
    })

这样完成之后,即可调用企微提供的客户端方法,下面给出一个调用企微内置地图打开位置的一个方法。

const openWeLocation = (data) => {
    // 使用企业微信内置地图打开位置
    ww.openLocation({
        latitude: Number(data.latitude), // 维度
        longitude: Number(data.longitude), // 经度
        name: '位置名称',
        address: '当前地址',
        scale: 18, // 更合适的缩放级别
        success(result:any) {
            console.log('打开地图成功');
        },
        fail(result:any) {
            console.log('打开地图失败');
            showToast('打开地图失败')
        },
        complete(result:any) {
            console.log('打开地图完成');
        }
    })
}