feat:增加企业微信接口博客
This commit is contained in:
@@ -87,6 +87,7 @@ export const routers = [
|
|||||||
text: '🔄 其他',
|
text: '🔄 其他',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'VitePress使用和简介', link: '/Others/VitePress' },
|
{ text: 'VitePress使用和简介', link: '/Others/VitePress' },
|
||||||
|
{ text: '企业微信工作台开发', link: '/Others/WeCom' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
451
docs/Others/WeCom.md
Normal file
451
docs/Others/WeCom.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
---
|
||||||
|
title: 企业微信工作台开发
|
||||||
|
date: 2026-01-23
|
||||||
|
---
|
||||||
|
|
||||||
|
  接口文档地址:[接口](https://developer.work.weixin.qq.com/document/path/90664)
|
||||||
|
|
||||||
|
# 一、建立工作台
|
||||||
|
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
|
||||||
|
2. 选择自建->创建应用。
|
||||||
|
3. 输入名称、图标等信息,分配可见范围,完成应用创建.
|
||||||
|
4. 点击应用,选择开发者接口->网页授权,配置可信域名和企业可信IP。
|
||||||
|
5. 查看并保存AgentId和Secret。
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
可信域名不能是IP形式,必须是和企业微信备案主体一致的服务器。
|
||||||
|
:::
|
||||||
|
|
||||||
|
# 二、JS-SDK开发
|
||||||
|
## 2.1 接口鉴权
|
||||||
|
  [官方文档](https://developer.work.weixin.qq.com/document/path/90514)
|
||||||
|
  参考第三节企业微信接口开发
|
||||||
|
|
||||||
|
## 2.2 打开默认浏览器
|
||||||
|
  使用系统浏览器打开指定 URL,支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
|
||||||
|
|
||||||
|
# 三、接口开发
|
||||||
|
  本教程以node.js为例。
|
||||||
|
## 3.1 通用企业微信服务端API
|
||||||
|
```js
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const TOKEN_URL = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
|
||||||
|
const JSAPI_TICKET_URL = 'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket'
|
||||||
|
const MESSAGE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
|
||||||
|
|
||||||
|
// 获取access_token
|
||||||
|
async function getAccessTokenApi(corpId, corpSecret) {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${TOKEN_URL}?corpid=${corpId}&corpsecret=${corpSecret}`
|
||||||
|
const response = await axios.get(url);
|
||||||
|
|
||||||
|
if (response.data.errcode === 0) {
|
||||||
|
return {
|
||||||
|
accessToken: response.data.access_token,
|
||||||
|
expires: now + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取access_token错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取jsapi_ticket
|
||||||
|
async function getJsapiTicketApi(accessToken) {
|
||||||
|
try {
|
||||||
|
const url = `${JSAPI_TICKET_URL}?access_token=${accessToken}`
|
||||||
|
const response = await axios.get(url);
|
||||||
|
|
||||||
|
if (response.data.errcode === 0) {
|
||||||
|
return response.data.ticket;
|
||||||
|
} else {
|
||||||
|
throw new Error(`获取jsapi_ticket失败: ${response.data.errmsg}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取jsapi_ticket错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送应用消息
|
||||||
|
async function sendMessageApi(agentid, accessToken, touser, content) {
|
||||||
|
try {
|
||||||
|
const url = `${MESSAGE_URL}?access_token=${accessToken}`
|
||||||
|
const response = await axios.post(url, {
|
||||||
|
touser: touser,
|
||||||
|
msgtype: 'text',
|
||||||
|
agentid: agentid,
|
||||||
|
text: {
|
||||||
|
content: content
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.errcode === 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`发送应用消息失败: ${response.data.errmsg}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送应用消息错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAccessTokenApi,
|
||||||
|
getJsapiTicketApi,
|
||||||
|
sendMessageApi
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.2 自建应用鉴权接口
|
||||||
|
```js
|
||||||
|
const {getAccessTokenApi, getJsapiTicketApi, sendMessageApi} = require("./wecom");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
corpId: '', // 企业id
|
||||||
|
corpSecret: '', // 应用密码
|
||||||
|
agentId: '', // 应用id
|
||||||
|
domain: '', // 跳转域名
|
||||||
|
password: '' // 登录默认密码
|
||||||
|
};
|
||||||
|
|
||||||
|
// 存储access_token和jsapi_ticket
|
||||||
|
let tokenCache = {
|
||||||
|
accessToken: null,
|
||||||
|
jsapiTicket: null,
|
||||||
|
expires: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取access_token
|
||||||
|
async function getRepairAccessToken() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (tokenCache.accessToken && tokenCache.expires > now) {
|
||||||
|
return tokenCache.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getAccessTokenApi(config.corpId, config.corpSecret)
|
||||||
|
tokenCache.accessToken = result.accessToken;
|
||||||
|
tokenCache.expires = result.expires
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取jsapi_ticket
|
||||||
|
async function getRepairJsapiTicket() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (tokenCache.jsapiTicket && tokenCache.expires > now) {
|
||||||
|
return tokenCache.jsapiTicket;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getRepairAccessToken();
|
||||||
|
tokenCache.jsapiTicket = await getJsapiTicketApi(tokenCache.accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送应用消息
|
||||||
|
async function sendRepairMessage(touser, content) {
|
||||||
|
await getRepairAccessToken();
|
||||||
|
await sendMessageApi(config.agentId, tokenCache.accessToken, touser, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepairDomain() {
|
||||||
|
return config.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepairPassword() {
|
||||||
|
return config.password
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getRepairAccessToken,
|
||||||
|
getRepairJsapiTicket,
|
||||||
|
sendRepairMessage,
|
||||||
|
getRepairDomain,
|
||||||
|
getRepairPassword
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 NodeJS API
|
||||||
|
```js
|
||||||
|
const express = require('express');
|
||||||
|
const axios = require('axios');
|
||||||
|
const cors = require('cors');
|
||||||
|
const {generateNonceStr, generateSignature} = require("./utils");
|
||||||
|
const {sendRepairMessage, getRepairJsapiTicket, getRepairAccessToken, getRepairDomain, getRepairPassword} = require("./repair");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const corpId = 'ww48932728388c9e3a'
|
||||||
|
const USER_INFO_URL = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
|
||||||
|
|
||||||
|
// API接口:生成JS-SDK配置
|
||||||
|
app.post('/auth/config/repair', async (req, res) => {
|
||||||
|
await configService(req, res, 'repair')
|
||||||
|
});
|
||||||
|
|
||||||
|
async function configService(req, res, appName) {
|
||||||
|
try {
|
||||||
|
const {url} = req.body;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({errcode: -1, errmsg: '缺少url参数'});
|
||||||
|
}
|
||||||
|
|
||||||
|
let jsapiTicket = ''
|
||||||
|
|
||||||
|
if (appName === 'repair') {
|
||||||
|
jsapiTicket = await getRepairJsapiTicket();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const noncestr = generateNonceStr();
|
||||||
|
const signature = generateSignature(jsapiTicket, noncestr, timestamp, url);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
errcode: 0,
|
||||||
|
errmsg: 'ok',
|
||||||
|
appId: corpId,
|
||||||
|
timestamp,
|
||||||
|
nonceStr: noncestr,
|
||||||
|
signature
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成JS-SDK配置错误:', error);
|
||||||
|
res.status(500).json({errcode: -1, errmsg: '服务器内部错误'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API接口:OAuth回调处理
|
||||||
|
app.get('/auth/callback/repair', async (req, res) => {
|
||||||
|
await callback(req, res, 'repair')
|
||||||
|
});
|
||||||
|
|
||||||
|
async function callback(req, res, appName) {
|
||||||
|
try {
|
||||||
|
const {code, state} = req.query;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
return res.status(400).send('缺少code参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过code获取用户信息
|
||||||
|
let accessToken = ''
|
||||||
|
if (appName === 'repair') {
|
||||||
|
accessToken = await getRepairAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${USER_INFO_URL}?access_token=${accessToken}&code=${code}`
|
||||||
|
const userResponse = await axios.get(url);
|
||||||
|
|
||||||
|
if (userResponse.data.errcode !== 0) {
|
||||||
|
throw new Error(`获取用户信息失败: ${userResponse.data.errmsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = userResponse.data;
|
||||||
|
|
||||||
|
// 重定向到前端页面
|
||||||
|
let redirectUrl = ''
|
||||||
|
if (appName === 'repair') {
|
||||||
|
redirectUrl = `${getRepairDomain()}?username=${userInfo.UserId}&password=${getRepairPassword()}&type=repair`;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(redirectUrl);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OAuth回调处理错误:', error);
|
||||||
|
res.status(500).send(`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>登录失败</h1>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API接口:发现应用消息
|
||||||
|
app.post('/message/repair', async (req, res) => {
|
||||||
|
const {touser, content} = req.body;
|
||||||
|
await sendRepairMessage(touser, content)
|
||||||
|
res.json({
|
||||||
|
errcode: 0,
|
||||||
|
errmsg: 'ok'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`企业微信登录服务运行在端口 ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.4 工具类
|
||||||
|
```js
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// 工具函数:生成随机字符串
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:生成签名
|
||||||
|
function generateSignature(jsapiTicket, noncestr, timestamp, url) {
|
||||||
|
const string = `jsapi_ticket=${jsapiTicket}&noncestr=${noncestr}×tamp=${timestamp}&url=${url}`;
|
||||||
|
return crypto.createHash('sha1').update(string).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateNonceStr,
|
||||||
|
generateSignature
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
# 四、应用主页
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>企业微信登录跳转</title>
|
||||||
|
<!-- 引入新版企业微信JS-SDK -->
|
||||||
|
<script src="https://wwcdn.weixin.qq.com/node/open/js/wecom-jssdk-2.3.3.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">正在跳转,请稍候...</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 配置参数
|
||||||
|
const serverUrl = ''
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
corpId: '', // 企业ID
|
||||||
|
agentId: '', // 应用ID
|
||||||
|
redirectUrl: encodeURIComponent(`${serverUrl}auth/callback/repair`), // OAuth回调地址
|
||||||
|
authUrl: `${serverUrl}auth/config/repair` // 后端授权入口
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
// 获取当前页面URL(用于签名)
|
||||||
|
const currentUrl = window.location.href.split('#')[0];
|
||||||
|
|
||||||
|
// 从后端获取签名信息
|
||||||
|
const signatureData = await getSignatureData(currentUrl);
|
||||||
|
|
||||||
|
const getConfigSignature = function(url) {
|
||||||
|
return {
|
||||||
|
timestamp: signatureData.timestamp,
|
||||||
|
nonceStr: signatureData.nonceStr,
|
||||||
|
signature: signatureData.signature
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用ww.register 进行身份注册
|
||||||
|
await ww.register({
|
||||||
|
corpId: config.corpId,
|
||||||
|
agentId: config.agentId,
|
||||||
|
jsApiList: ['openDefaultBrowser', 'closeWindow'],
|
||||||
|
getConfigSignature: getConfigSignature
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成授权URL
|
||||||
|
const authUrl = await generateAuthUrl();
|
||||||
|
|
||||||
|
const isMobile = /(phone|pad|pod|iphone|ipod|ios|ipad|android|mobile|blackberry|iemobile|mqqbrowser|fennec|wosbrowser|browserng|webos|symbian|windows phone)/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// 在移动端企业微信中直接打开链接
|
||||||
|
window.location.href = authUrl;
|
||||||
|
} else {
|
||||||
|
// 使用新版API调用系统浏览器
|
||||||
|
ww.openDefaultBrowser({
|
||||||
|
url: authUrl,
|
||||||
|
success: (res) => {
|
||||||
|
// console.log('成功调用系统浏览器:', res);
|
||||||
|
ww.closeWindow({})
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
// console.error('打开浏览器失败:', err);
|
||||||
|
document.getElementById('loading').innerHTML = `打开失败: ${err.errMsg || '未知错误'},请点击右上角菜单选择"在浏览器中打开"`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化失败:', error);
|
||||||
|
document.getElementById('loading').innerHTML = '跳转失败,请手动在浏览器中打开: ' + error.errMsg;
|
||||||
|
// window.alert(error.errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取签名数据
|
||||||
|
async function getSignatureData(url) {
|
||||||
|
const response = await fetch(`${config.authUrl}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取签名配置失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// 确保返回的数据包含必要的字段
|
||||||
|
if (!result.timestamp || !result.nonceStr || !result.signature) {
|
||||||
|
throw new Error('签名数据不完整');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成授权URL
|
||||||
|
async function generateAuthUrl() {
|
||||||
|
// 使用URLSearchParams构建参数
|
||||||
|
const authParams = new URLSearchParams({
|
||||||
|
appid: config.corpId,
|
||||||
|
redirect_uri: config.redirectUrl,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'snsapi_base', // 静默授权
|
||||||
|
state: 'wecom_redirect',
|
||||||
|
agentid: config.agentId
|
||||||
|
});
|
||||||
|
|
||||||
|
return `https://open.weixin.qq.com/connect/oauth2/authorize?${authParams.toString()}#wechat_redirect`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
  将该应用主页的URL放在工作台的应用主页中。
|
||||||
|
::: tip
|
||||||
|
需要在前端工程中配置路由参数访问,即通过ip:port/#/login?username=''&password=''的形式访问。
|
||||||
|
:::
|
||||||
Reference in New Issue
Block a user