node服务器接入微信与企业微信js-sdk 第一篇
目录结构
1.main.config.js
module.exports = {
corp_id: "xxxx",
agent_id: "xxxx",
app_secret: "xxxx",
appid: "xxxx",
wx_secret: "xxxx",
jsApiList: [
"checkJsApi",
"updateAppMessageShareData",
"updateTimelineShareData",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"onMenuShareQQ",
"onMenuShareWeibo",
"hideMenuItems",
"showMenuItems",
"hideAllNonBaseMenuItem",
"showAllNonBaseMenuItem",
"translateVoice",
"startRecord",
"stopRecord",
"onRecordEnd",
"playVoice",
"pauseVoice",
"stopVoice",
"uploadVoice",
"downloadVoice",
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage",
"getNetworkType",
"openLocation",
"getLocation",
"hideOptionMenu",
"showOptionMenu",
"closeWindow",
"scanQRCode",
"chooseWXPay",
"openProductSpecificView",
"addCard",
"chooseCard",
"openCard",
],
}
2.signApi.js
/**
* 获取签名
* @returns:
* 1. appId 必填,公众号的唯一标识
* 2. timestamp 必填,生成签名的时间戳
* 3. nonceStr 必填,生成签名的随机串
* 4. signature 必填,签名
*/
const crypto = require("crypto");
// sha1加密
function sha1(str) {
let shasum = crypto.createHash("sha1");
shasum.update(str);
str = shasum.digest("hex");
return str;
}
/**
* 生成签名的时间戳
* @return {字符串}
*/
function createTimestamp() {
return parseInt(new Date().getTime() / 1000) + "";
}
/**
* 生成签名的随机串
* @return {字符串}
*/
function createNonceStr() {
return Math.random().toString(36).substr(2, 15);
}
/**
* 对参数对象进行字典排序
* @param {对象} args 签名所需参数对象
* @return {字符串} 排序后生成字符串
*/
function raw(args) {
var keys = Object.keys(args);
keys = keys.sort();
var newArgs = {};
keys.forEach(function (key) {
newArgs[key.toLowerCase()] = args[key];
});
var string = "";
for (var k in newArgs) {
string += "&" + k + "=" + newArgs[k];
}
string = string.substr(1);
return string;
}
module.exports = getSign = ({ params, res, otherParams }) => {
/**
* 签名算法
* 签名生成规则如下:
* 参与签名的字段包括noncestr( 随机字符串),
* 有效的jsapi_ticket, timestamp( 时间戳),
* url( 当前网页的URL, 不包含# 及其后面部分)。
* 对所有待签名参数按照字段名的ASCII 码从小到大排序( 字典序) 后,
* 使用URL键值对的格式( 即key1 = value1 & key2 = value2…) 拼接成字符串string1。
* 这里需要注意的是所有参数名均为小写字符。 对string1作sha1加密, 字段名和字段值都采用原始值, 不进行URL 转义。
*/
var ret = {
jsapi_ticket: params.ticket,
nonceStr: createNonceStr(),
timestamp: createTimestamp(),
url: params.url,
};
// console.log(params, ret);
var string = raw(ret);
var resMap = {
...otherParams,
debug: false,
nonceStr: ret.nonceStr,
signature: sha1(string),
timestamp: ret.timestamp,
};
console.log("resMap", resMap);
res.send({
data: resMap,
success: true,
});
};
3.tokens.json
{}
4.wxtokens.json
{}
5.accesstoken.js
const axios = require("axios");
const Config = require("../configs/main.config");
const fs = require("fs");
const path = require("path");
const { debounce } = require("lodash");
let flag = false;
// token 缓存文件存储位置
const token_path = "../temp/tokens.json";
const wxtoken_path = "../temp/wxtokens.json";
// 判断 token 是否过期
let _isExpire = function (create_time, expire_time) {
const current = Math.floor(Date.now() / 1000);
return create_time + expire_time < current;
};
let getSaveWXToken = async function ({ tokens, type, appid, secret }) {
try {
// console.log("getSaveWXToken", tokens, type, appid, secret);
const {
data: { access_token, expires_in },
} = await axios.get("https://api.weixin.qq.com/cgi-bin/token", {
params: {
grant_type: "client_credential",
appid: appid,
secret: secret,
},
});
// 重新写入 access_token
tokens[type] = {};
tokens[type].create_time = Math.floor(Date.now() / 1000);
tokens[type].expire_time = expires_in;
tokens[type].token = access_token;
fs.writeFileSync(
path.join(__dirname, wxtoken_path),
JSON.stringify(tokens),
{
encoding: "utf-8",
}
);
// 返回 access_token
console.log(`获取WX ${type} access_token 成功`, access_token);
return access_token;
} catch (error) {
console.log("error", error);
}
};
const debounceSaveWXToken = debounce(getSaveWXToken, 1000 * 60 * 5);
// 获取 QYWXtoken
let _getQYWXTokenWithType = async function (type) {
let tokens = {};
let corp_id = Config.corp_id;
//
if (!type) {
type = "app";
}
let secret = Config[`${type}_secret`];
// 先尝试从缓存中读取出token
try {
tokens = JSON.parse(
fs.readFileSync(path.join(__dirname, token_path), {
encoding: "utf-8",
})
);
} catch (err) {
// 缓存文件读取失败
console.error(err);
}
if (
!tokens[type] ||
!tokens[type].token ||
_isExpire(tokens[type].create_time, tokens[type].expire_time)
) {
// 如果缓存中没有 token,或者 token 过期
console.log(`重新获取QYWX ${type} access_token`);
// 发起请求,获取 access_token
const {
data: { access_token, expires_in },
} = await axios.get("https://qyapi.weixin.qq.com/cgi-bin/gettoken", {
params: {
corpid: corp_id,
corpsecret: secret,
},
});
// 重新写入 access_token
tokens[type] = {};
tokens[type].create_time = Math.floor(Date.now() / 1000);
tokens[type].expire_time = expires_in;
tokens[type].token = access_token;
fs.writeFileSync(path.join(__dirname, token_path), JSON.stringify(tokens), {
encoding: "utf-8",
});
// 返回 access_token
console.log(`获取QYWX ${type} access_token 成功`, access_token);
return access_token;
} else {
// 从缓存中读取
console.log(`从缓存中读取QYWX ${type} access_token`, tokens[type].token);
return tokens[type].token;
}
};
// 获取 QYWXjsapi_ticket 临时票据
let _getQYWXTicketWithType = async function (type) {
let tokens = {};
let ACCESS_TOKEN = await _getQYWXTokenWithType("app");
//
if (!type) {
type = "ticket";
}
// 先尝试从缓存中读取出token
try {
tokens = JSON.parse(
fs.readFileSync(path.join(__dirname, token_path), {
encoding: "utf-8",
})
);
} catch (err) {
// 缓存文件读取失败
console.error(err);
}
if (
!tokens[type] ||
!tokens[type].ticket ||
_isExpire(tokens[type].create_time, tokens[type].expire_time)
) {
// 如果缓存中没有 token,或者 token 过期
console.log(`重新获取QYWX ${type} access_token`);
// 发起请求,获取 access_token
const {
data: { ticket, expires_in },
} = await axios.get("https://qyapi.weixin.qq.com/cgi-bin/ticket/get", {
params: {
type: "agent_config",
access_token: ACCESS_TOKEN,
},
});
// 重新写入 access_token
tokens[type] = {};
tokens[type].create_time = Math.floor(Date.now() / 1000);
tokens[type].expire_time = expires_in;
tokens[type].ticket = ticket;
fs.writeFileSync(path.join(__dirname, token_path), JSON.stringify(tokens), {
encoding: "utf-8",
});
// 返回 access_token
console.log(`获取QYWX ${type} access_token 成功`, ticket);
return ticket;
} else {
// 从缓存中读取
console.log(`从缓存中读取QYWX ${type} access_token`, tokens[type].ticket);
return tokens[type].ticket;
}
};
// 获取 WXtoken
let _getWXTokenWithType = async function (type) {
let tokens = {};
let appid = Config.appid;
//
if (!type) {
type = "app";
}
let secret = Config[`${type}_secret`];
// 先尝试从缓存中读取出token
try {
tokens = JSON.parse(
fs.readFileSync(path.join(__dirname, wxtoken_path), {
encoding: "utf-8",
})
);
} catch (err) {
// 缓存文件读取失败
console.error(err);
}
if (
!tokens[type] ||
!tokens[type].token ||
_isExpire(tokens[type].create_time, tokens[type].expire_time)
) {
// 如果缓存中没有 token,或者 token 过期
console.log(`重新获取WX ${type} WXToken`);
// 发起请求,获取 access_token
const access_token = await getSaveWXToken({
tokens,
type,
appid,
secret,
});
return access_token;
} else {
// 从缓存中读取
console.log(`从缓存中读取WX ${type} WXToken`, tokens[type].token);
let access_token = tokens[type].token;
const { data } = await axios.get(
`https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=${access_token}`
);
// console.log("data", data);
if (data.errcode && data.errcode == 40001) {
if (!flag) {
flag = true;
access_token = getSaveWXToken({ tokens, type, appid, secret });
return access_token;
}
access_token = debounceSaveWXToken({ tokens, type, appid, secret });
return access_token || tokens[type].token;
}
// getSaveWXToken({ tokens, type, appid, secret });
return tokens[type].token;
}
};
// 获取 WXjsapi_ticket 临时票据
let _getWXTicketWithType = async function (type) {
let tokens = {};
let ACCESS_TOKEN = await _getWXTokenWithType("wx");
//
if (!type) {
type = "app";
}
// 先尝试从缓存中读取出token
try {
tokens = JSON.parse(
fs.readFileSync(path.join(__dirname, wxtoken_path), {
encoding: "utf-8",
})
);
} catch (err) {
// 缓存文件读取失败
console.error(err);
}
if (
!tokens[type] ||
!tokens[type].ticket ||
_isExpire(tokens[type].create_time, tokens[type].expire_time)
) {
// 如果缓存中没有 ticket,或者 ticket 过期
console.log(`重新获取WX ${type} WXTicket_token`);
// 发起请求,获取 access_token
const {
data: { ticket, expires_in },
} = await axios.get("https://api.weixin.qq.com/cgi-bin/ticket/getticket", {
params: {
type: "jsapi",
access_token: ACCESS_TOKEN,
},
});
// 重新写入 access_token
tokens[type] = {};
tokens[type].create_time = Math.floor(Date.now() / 1000);
tokens[type].expire_time = expires_in;
tokens[type].ticket = ticket;
fs.writeFileSync(
path.join(__dirname, wxtoken_path),
JSON.stringify(tokens),
{
encoding: "utf-8",
}
);
// 返回 access_token
console.log(`获取WX ${type} access_token 成功`, ticket);
return ticket;
} else {
// 从缓存中读取
console.log(`从缓存中读取WX ${type} WXTicket_token`, tokens[type].ticket);
return tokens[type].ticket;
}
};
module.exports = {
// 获取普通应用的 QYWXtoken
async getAppToken() {
return await _getQYWXTokenWithType("app");
},
// 获取普通应用的 QYWXjsapi_ticket 临时票据
async getAppTicket() {
return await _getQYWXTicketWithType("ticket");
},
// 获取普通应用的 WXtoken
async getWXAppToken() {
return await _getWXTokenWithType("wx");
},
// 获取普通应用的 WXjsapi_ticket 临时票据
async getWXAppTicket() {
return await _getWXTicketWithType("ticket");
},
};
6.utilController.js
const AccessToken = require("./accesstoken");
const config = require("../configs/main.config");
const signApi = require("../routes/signApi");
module.exports = function (router) {
//获取微信签名
router.get("/wechat-config", async (req, res) => {
try {
const params = {};
console.log(req.query);
params.url = req.query.url;
params.ticket = await AccessToken.getWXAppTicket();
const otherParams = {
appId: config.appid,
jsApiList: config.jsApiList,
};
signApi({ params, res, otherParams });
} catch (error) {
res.send({
success: false,
});
}
});
//获取QYWX签名
router.get("/qywechat-config", async (req, res) => {
try {
const params = {};
console.log(req.query);
params.url = req.query.url;
params.ticket = await AccessToken.getAppTicket();
const otherParams = {
corpid: config.corp_id,
agentid: config.agent_id,
jsApiList: config.jsApiList,
};
signApi({ params, res, otherParams });
} catch (error) {
res.send({
success: false,
});
}
});
}
7.users.js
var express = require("express");
var router = express.Router();
const utilController = require("../server/utilController");
utilController(router);
module.exports = router;
8.app.js
var express = require("express");
var usersRouter = require("./routes/users");
var app = express();
app.use("/apiWx", usersRouter);
module.exports = app;
9.www
var app = require("../app");
var http = require("http");
var port = normalizePort(process.env.PORT || "1024");
app.set("port", port);
var server = http.createServer(app);
server.listen(port);
server.on("listening", () => onListening(server));
function onListening(server) {
var addr = server.address();
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
debug("Listening on " + bind);
}
10.package.json
**启动重点 **"start": "nodemon ./bin/index.js"
{
"name": "internalappsample",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/index.js",
"serve": "cd ./vue && yarn serve",
"build": "cd ./vue && npx yarn install && npx yarn build"
},
"dependencies": {
"axios": "^0.21.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"ejs": "^3.1.8",
"express": "~4.16.1",
"express-fileupload": "^1.2.1",
"express-session": "^1.17.3",
"form-data": "^4.0.0",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"lodash": "^4.17.21",
"morgan": "~1.9.1",
"request": "^2.88.2",
"url-parse": "^1.5.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.16.0",
"typescript": "^4.3.5"
}
}
作者:白马不是马
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签