活字格集成企业微信JS-SDK指南
在环境集成和工作流程整合方面,企业微信JS-SDK能够实现扫码、定位、语音识别等多种功能,有效提升用户体验和业务维护效率。本文将介绍如何在活字格中集成企业微信JS-SDK,包括环境准备、后端配置、前端调用以及常见问题的解决。
一、环境准备
1. 注册企业微信账号并创建应用
- 访问 企业微信官网 注册账号
- 在管理后台创建自建应用,记录:
- 企业ID (CorpID)
- 应用Secret
- AgentId
2. 配置可信域名、白名单IP和授权回调域
- 在网页授权设置中,配置可信域名为活字格应用的域名
- 在授权登录设置中,配置回调域名
- 在企业可信IP设置中,配置白名单IP
3. 下载并上传企业微信安全提供程序包
- 下载
qyWeixinSecurityProvider.zip
- 在活字格“用户账户管理” - “第三方”中上传
4. 配置活字格应用
- 设置认证模式为“第三方用户集成”
- 配置 CorpID, Secret, AgentId
- 发布应用,配置应用域名
二、Java Web API 后端实现
1. 创建项目
- 使用活字格 Java API 生成器(forguncyJavaExtensionGenerateTool)
- 在 IDEA 中打开,写自定义后端
2. 接口示例:获取 JS-SDK 签名
@Get
public void getJsSdkConfig() throws IOException {
String url = this.getContext().getRequest().getParameter("url");
if (url == null || url.isEmpty()) {
url = this.getContext().getRequest().getHeader("Referer");
}
String nonceStr = WeChatUtils.genNonceStr();
String timestamp = WeChatUtils.genTimestamp();
try {
String jsapiTicket = WeChatUtils.getJsApiTicket();
String signature = WeChatUtils.generateSignature(jsapiTicket, nonceStr, timestamp, url);
JSONObject json = new JSONObject();
json.put("appId", WeChatUtils.getCorpId());
json.put("timestamp", timestamp);
json.put("nonceStr", nonceStr);
json.put("signature", signature);
sendJson(json.toString());
} catch (Exception e) {
JSONObject error = new JSONObject();
error.put("error", e.getMessage());
sendJson(error.toString());
}
}
3. 运行签名逻辑(WeChatUtils.java)
- 获取 access_token
- 获取 jsapi_ticket
- 进行 SHA1 签名
- 返回 appId/timestamp/nonceStr/signature
4. 后端完整代码
以下是 Java Web API 的三个关键模块代码,分别为主接口类、微信工具类和HTTP工具类:
QyWeChatApi.java:后端入口类,定义对外接口
package org.example;
import com.grapecity.forguncy.serverapi.annotation.Get;
import com.grapecity.forguncy.serverapi.entity.ForguncyApi;
import java.io.IOException;
import java.io.PrintWriter;
import org.json.JSONObject;
public class QyWeChatApi extends ForguncyApi {
@Get
public void helloWorld() throws IOException {
this.getContext().getResponse().setContentType("text/plain");
String message = "Hello World QyWeChatApi";
PrintWriter out = this.getContext().getResponse().getWriter();
out.print(message);
out.close();
}
@Get
public void getJsSdkConfig() throws IOException {
String url = this.getContext().getRequest().getParameter("url");
if (url == null || url.isEmpty()) {
url = this.getContext().getRequest().getHeader("Referer");
}
String nonceStr = WeChatUtils.genNonceStr();
String timestamp = WeChatUtils.genTimestamp();
try {
String jsapiTicket = WeChatUtils.getJsApiTicket();
String signature = WeChatUtils.generateSignature(jsapiTicket, nonceStr, timestamp, url);
JSONObject json = new JSONObject();
json.put("appId", WeChatUtils.getCorpId());
json.put("timestamp", timestamp);
json.put("nonceStr", nonceStr);
json.put("signature", signature);
sendJson(json.toString());
} catch (Exception e) {
JSONObject error = new JSONObject();
error.put("error", e.getMessage());
sendJson(error.toString());
}
}
/**
* 快捷 JSON 输出
*/
private void sendJson(String json) throws IOException {
this.getContext().getResponse().setContentType("application/json");
this.getContext().getResponse().setCharacterEncoding("UTF-8");
PrintWriter out = this.getContext().getResponse().getWriter();
out.print(json);
out.flush();
out.close();
}
}
WeChatUtils.java:JS-SDK 签名核心工具类
package org.example;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
import org.json.JSONObject;
/**
* ==================================================
* This class WeChatUtils is responsible for 企业微信 JS-SDK 权限签名接口封装.
*
* @author Darker
* @version 1.0
* ==================================================
*/
public class WeChatUtils {
// 企业微信配置项
private static final String CORP_ID = "企业微信ID";
private static final String CORP_SECRET = "自建应用密钥";
private static final String AGENT_ID = "自建应用ID";
private static final String ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";
private static final String JSAPI_TICKET_URL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket";
private static String accessToken;
private static long tokenExpiryTime;
private static String jsapiTicket;
private static long ticketExpiryTime;
private static final ReentrantLock lock = new ReentrantLock();
/**
* 获取 access_token(带缓存)
*/
public static String getAccessToken() throws IOException {
long currentTime = System.currentTimeMillis();
if (accessToken == null || currentTime >= tokenExpiryTime) {
lock.lock();
try {
if (accessToken == null || currentTime >= tokenExpiryTime) {
String url = ACCESS_TOKEN_URL + "?corpid=" + CORP_ID + "&corpsecret=" + CORP_SECRET;
String response = HttpUtils.httpGet(url);
JSONObject json = new JSONObject(response);
accessToken = json.getString("access_token");
int expiresIn = json.getInt("expires_in");
tokenExpiryTime = currentTime + (expiresIn - 200) * 1000L;
}
} finally {
lock.unlock();
}
}
return accessToken;
}
/**
* 获取 jsapi_ticket(带缓存)
*/
public static String getJsApiTicket() throws IOException {
long currentTime = System.currentTimeMillis();
if (jsapiTicket == null || currentTime >= ticketExpiryTime) {
lock.lock();
try {
if (jsapiTicket == null || currentTime >= ticketExpiryTime) {
String accessToken = getAccessToken();
String url = JSAPI_TICKET_URL + "?access_token=" + accessToken;
String response = HttpUtils.httpGet(url);
JSONObject json = new JSONObject(response);
jsapiTicket = json.getString("ticket");
int expiresIn = json.getInt("expires_in");
ticketExpiryTime = currentTime + (expiresIn - 200) * 1000L;
}
} finally {
lock.unlock();
}
}
return jsapiTicket;
}
/**
* 生成微信 JS-SDK 的签名
*/
public static String generateSignature(String jsapiTicket, String nonceStr, String timestamp, String url)
throws NoSuchAlgorithmException {
String string1 = "jsapi_ticket=" + jsapiTicket +
"&noncestr=" + nonceStr +
"×tamp=" + timestamp +
"&url=" + url;
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes(StandardCharsets.UTF_8));
return byteToHex(crypt.digest());
}
/**
* SHA-1 摘要转 Hex 字符串
*/
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
/** 获取 CorpId */
public static String getCorpId() {
return CORP_ID;
}
/** 获取 AgentId */
public static String getAgentId() {
return AGENT_ID;
}
/** 生成随机 nonceStr */
public static String genNonceStr() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
/** 生成时间戳(秒) */
public static String genTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
}
HttpUtils.java:基础 HTTP 请求工具类
package org.example;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
/**
* ==================================================
* This class HttpUtils is responsible for [功能描述].
*
* @author Darker
* @version 1.0
* ==================================================
*/
public class HttpUtils {
/**
* 发起 HTTP GET 请求,并返回响应结果(纯文本或 JSON)
* @param urlStr 完整的请求地址,例如 https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=xxx&corpsecret=xxx
* @return 响应结果字符串
* @throws IOException 网络请求错误时抛出异常
*/
public static String httpGet(String urlStr) throws IOException {
URI uri = URI.create(urlStr);
URL url = uri.toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
return readResponse(conn);
}
/**
* 发起 HTTP POST 请求,提交字符串类型的请求体,并返回响应结果
*
* @param urlStr 请求地址
* @param body 请求体(一般为 JSON 或 form-urlencoded)
* @param contentType 请求类型,如 "application/json"、"application/x-www-form-urlencoded"
*/
public static String httpPost(String urlStr, String body, String contentType) throws IOException {
URI uri = URI.create(urlStr);
URL url = uri.toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setDoOutput(true); // 启用输出流发送数据
conn.setRequestProperty("Content-Type", contentType);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = body.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
return readResponse(conn);
}
/**
* 从指定的 HttpURLConnection 中读取响应内容(适用于 GET 和 POST)
*
* <p>该方法会以 UTF-8 编码读取响应体的所有内容,适用于企业微信接口返回的 JSON 数据。</p>
*
* @param conn 已建立连接的 HttpURLConnection 对象(必须已调用 getInputStream)
* @return 响应体字符串(一般是 JSON 格式)
* @throws IOException 读取流出错或连接中断时抛出
*/
private static String readResponse(HttpURLConnection conn) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
reader.close();
conn.disconnect();
return result.toString();
}
}
三、前端 JS 调用
1. 初始化 wx.config
var u = navigator.userAgent;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
let innerurl = ''
if (isAndroid) {
innerurl = location.href
}
if (isIOS) {
innerurl = location.href.split('#')[0]
}
var url = Forguncy.ForguncyData.ForguncyRoot + 'customapi/wechatapi/getsignature';
try {
Forguncy.Page.getCell("barURLValue").setValue(url);
Forguncy.Page.getCell("barInnerurlValue").setValue(innerurl);
} catch (e) {
alert(e);
}
var data =
{
"url": innerurl,
};
$.get(url, data, function (result) {
alert("result:" + result);
var valueList = result;
wx.config({
debug: false,
appId: valueList.corpid,
timestamp: valueList.timestamp,
nonceStr: valueList.nonceStr,
signature: valueList.signature,
jsApiList: ["startRecord", "stopRecord", "translateVoice", "scanQRCode", "playVoice", "pauseVoice", 'translateVoice',"stopVoice", "openLocation", "getLocation"] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
wx.ready(function () {
alert("签名信息获取成功!");
});
wx.error(function (res) {
alert("签名信息获取失败:" + res);
});
}).fail(function (xhr, status, error) {
alert("请求失败!"
+ "\n状态:" + status
+ "\n错误信息:" + error
+ "\n响应内容:" + xhr.responseText);
});
2. 扫码功能
wx.scanQRCode({
// 默认为0,扫描结果由微信处理,1则直接返回扫描结果
needResult: 1,
desc: 'scanQRCode desc',
success: function (res) {
//扫码后获取结果参数赋值给Input
var url = res.resultStr;
//商品条形码,取","后面的
if (url.indexOf(",") >= 0) {
var tempArray = url.split(',');
var tempNum = tempArray[1];
try {
Forguncy.Page.getCell("barCodeValue").setValue(tempNum);
} catch (e) {
alert(e);
}
} else {
try {
Forguncy.Page.getCell("barCodeValue").setValue(url);
} catch (e) {
alert(e);
}
}
},
error: function(res) {
if (res.errMsg.indexOf('function_not_exist') > 0) {
alert('版本过低请升级')
}
}
});
3. 定位和打开地图
wx.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
success: function (res) {
var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
var longitude = res.longitude ; // 经度,浮点数,范围为180 ~ -180。
var speed = res.speed; // 速度,以米/每秒计
var accuracy = res.accuracy; // 位置精度
wx.openLocation({
latitude: latitude, // 纬度,浮点数,范围为90 ~ -90
longitude: longitude, // 经度,浮点数,范围为180 ~ -180。
name: '当前位置', // 位置名
address: '', // 地址详情说明
scale: 16, // 地图缩放级别,整形值,范围从1~28。默认为最大
});
}
});
四、常见问题和揭秘
问题:签名失败
- 确保 URL 不包含
#
- 确保 JS-SDK 域名签名同步
问题:access_token/jsapi_ticket 无效
- 确保有效期内缓存成功
- 确保企业ID和Secret正确
问题:界面一直刷新或卡死,可能出现在 JSON 解析位置
如这段代码中:
var valueList = JSON.parse(result);
需要特别注意——有些情况下,result
可能已经是一个 JSON 对象(尤其是在活字格平台的某些 JS 执行环境中可能会自动解析响应),此时再调用 JSON.parse()
会抛出异常,导致页面白屏、报错或不断刷新。
推荐写法(兼容对象/字符串):
var valueList = typeof result === "string" ? JSON.parse(result) : result;
通过类型判断,确保 valueList 始终是有效对象,提升兼容性与健壮性。