前言
在软件开发过程中,客户提出需求,要求平台能够将消息推送到企业微信,并根据不同场景支持推送不同的消息模板。以文本卡片消息类型为例,平台需要根据不同的业务需求,灵活选择和推送合适的消息模板,从而提高消息的传达效果和用户体验。
- 官方调试界面:企业微信开发工具
- 官方文档:企业微信服务端 API 文档
注意
下方总结
中有优化建议,因时间原因,暂无法进行示例代码优化,感兴趣的可以了解。
1. 进入企业内部应用开发
在企业微信开发平台中,选择“服务端 API”,以下是示例代码中相关参数的说明:
- corpid(企业ID):
corpId
- corpsecret(凭证密钥):
secretKey
- AgentId(企业应用的id):
企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
2. 推送流程
2.1 获取 access_token
更多细节请参见:获取 access_token 文档。
获取 access_token
是进行消息推送的第一步,步骤如下:
-
请求地址:
https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
-
请求方式:
GET
(HTTPS) -
请求参数:
corpid
:企业IDcorpsecret
:凭证密钥
-
部分示例代码:
-
// 定义一个函数 `getAccessToken`,用于通过企业 ID 和安全密钥获取访问令牌 fun getAccessToken(corpId: String?, securityKey: String?): String { // 构建请求 URL,传入企业 ID 和安全密钥,拼接到 URL 中 val url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$corpId&corpsecret=$securityKey" // 发送 GET 请求到指定的 URL,并获取响应内容,返回的是 JSON 格式的字符串 val response = URL(url).readText() // 将响应的 JSON 字符串解析成 Map 数据结构,使用 `objectMapper` 来进行反序列化 val map: MutableMap<String, String> = objectMapper.readValue(response, Map::class.java) as MutableMap<String, String> // 从解析后的 Map 中提取访问令牌(ACCESS_TOKEN),并返回其值 return map[MessageConstant.ACCESS_TOKEN] as String }
-
-
返回结果:
-
{ "errcode": 0, "errmsg": "ok", "access_token": "accesstoken000001", "expires_in": 7200 }
-
-
返回参数说明:
-
参数 说明 errcode
出错返回码,为 0 表示成功,非 0 表示调用失败 errmsg
返回码提示语 access_token
获取到的凭证,最长为 512 字节 expires_in
凭证的有效时间(秒)
-
-
部分相关注意事项:
- 开发者需要缓存
access_token
,用于后续接口的调用(注意:不能频繁调用gettoken
接口,否则会受到频率拦截)。 - 当
access_token
失效或过期时,需要重新获取。 access_token
的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时)。- 每个应用有独立的
secret
,获取到的access_token
只能本应用使用,所以每个应用的access_token
应该分开来获取。
- 开发者需要缓存
2.2 根据手机号获取 userid
更多细节请参见:根据手机号获取 userid。
获取 userid
是推送消息的第二步。通过手机号查询用户ID,步骤如下:
-
请求地址:
https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=ACCESS_TOKEN
-
请求方式:
POST
(HTTPS) -
请求参数:
-
ACCESS_TOKEN
:第一步中获取的access_token
-
**请求体:**包含手机号,以 JSON 格式传递,例如:
{ "mobile": "xxxxxxxxx" }
-
-
部分示例代码:
-
// 定义一个函数 `getUserIdByMobile`,用于根据手机号和访问令牌获取用户 ID fun getUserIdByMobile(accessToken: String, mobile: String): String? { // 构建请求 URL,传入 accessToken,拼接成请求的 URL val url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=$accessToken" // 创建一个 Map 来存储手机号信息 val mobileMap = mutableMapOf<String, String>() // 将手机号作为键值对存入 Map mobileMap["mobile"] = mobile // 将手机号 Map 转换为 JSON 字符串格式 val mobileStr = objectMapper.writeValueAsString(mobileMap) // 打开 URL 连接,开始构建 HTTP 请求 val connection = URL(url).openConnection() // 设置连接为输出模式,以便发送 POST 请求 connection.doOutput = true // 设置请求头,表明请求体的数据类型为 JSON connection.setRequestProperty("Content-Type", "application/json") // 获取连接的输出流,准备发送请求体数据 val outputStream = OutputStreamWriter(connection.getOutputStream()) // 将手机号 JSON 字符串写入输出流 outputStream.write(mobileStr) // 确保消息被立即发送 outputStream.flush() // 读取 API 响应,获取返回的 JSON 字符串 val response = connection.inputStream.bufferedReader().use { it.readText() } // 使用日志记录响应内容,便于调试 Logger.info("获取手机的返回: $response") // 将响应的 JSON 字符串解析成 Map 数据结构 val map: MutableMap<String, String> = objectMapper.readValue(response, Map::class.java) as MutableMap<String, String> // 返回从解析的 Map 中获取的用户 ID(通过常量 MessageConstant.USERID) return map[MessageConstant.USERID] }
-
-
返回结果:
-
{ "errcode": 0, "errmsg": "ok", "userid": "zhangsan" }
-
-
返回参数说明:
-
参数 说明 errcode
返回码 errmsg
对返回码的文本描述内容 userid
成员 UserID。对应管理端的账号,企业内必须唯一。不区分大小写,长度为 1~64 个字节。注意:第三方应用获取的值是密文的 userid
-
-
部分相关注意事项:
- 请确保手机号的正确性,若出错的次数超出企业规模人数的20%,会导致1天不可调用。
2.3 利用userid
发送消息 - 文本卡片消息类型为例
必看:利用 userid 发送消息。
利用userid
发送消息为最后一步。通过手机号查询用户ID,步骤如下:
-
请求地址:
https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN
-
请求方式:
POST
(HTTPS) -
请求参数:
-
ACCESS_TOKEN
:第一步中获取的access_token
-
请求体:包含手机号,以 JSON 格式传递,例如:
{ "mobile": "xxxxxxxxx" }
-
-
部分示例代码:
-
// 定义一个函数 `sendMessage`,用于发送消息到微信企业号 fun sendMessage(accessToken: String, message: String) { // 构建请求 URL,传入 accessToken,拼接成消息发送请求的 URL val url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=$accessToken" // 打开 URL 连接,开始构建 HTTP 请求 val connection = URL(url).openConnection() // 设置连接为输出模式,以便发送 POST 请求 connection.doOutput = true // 设置请求头,表明请求体的数据类型为 JSON connection.setRequestProperty("Content-Type", "application/json") // 获取连接的输出流,准备发送请求体数据 val outputStream = OutputStreamWriter(connection.getOutputStream()) // 将消息字符串写入输出流 outputStream.write(message) // 确保消息被立即发送 outputStream.flush() // 读取 API 响应,获取返回的 JSON 字符串 val response = connection.inputStream.bufferedReader().use { it.readText() } // 使用日志记录响应内容,便于调试 Logger.info("Response: $response") }
-
3. 整体示例代码 - 文本卡片消息类型:
核心配置文件:
qywx.agentId=agentId(企业应用的id)
qywx.corpId=corpid(企业ID)
qywx.securityKey=corpsecret(凭证密钥)
示例代码:
@Service
class QyWxMessageService(val redisTemplate: RedisTemplate<String, String>) {
// 注入配置文件中的企业微信应用相关信息
@Value("\${qywx.agentId}")
val agentId: String? = null
@Value("\${qywx.corpId}")
val corpId: String? = null
@Value("\${qywx.securityKey}")
val securityKey: String? = null
// ObjectMapper 用于 JSON 的序列化和反序列化
val objectMapper = ObjectMapper()
// 发送消息的方法
fun sendMessage(rightPopMap: MutableMap<String, String?>) {
// 检查必要的配置是否为空,如果为空则直接返回
if (agentId.isNullOrEmpty() || corpId.isNullOrEmpty() || securityKey.isNullOrEmpty()) {
return
}
// 从 Redis 获取 accessToken,如果为空则重新获取
var accessToken = redisTemplate.opsForValue().get(MessageConstant.REDIS_KEY_ACCESSTOKEN_QUEUE)
if (accessToken.isNullOrEmpty()) {
// 如果 accessToken 不在 Redis 中,则调用获取 token 的方法
accessToken = getAccessToken(corpId, securityKey)
// 将获取的 accessToken 存储到 Redis,并设置过期时间为 1 小时
redisTemplate.opsForValue().set(MessageConstant.REDIS_KEY_ACCESSTOKEN_QUEUE, accessToken, 1, TimeUnit.HOURS)
}
// 用于存储所有成功获取的用户 ID
val userIdList = mutableListOf<String?>()
// 将手机号按照逗号分隔并处理
val split = rightPopMap["mobile"]!!.split(",") as MutableList<String>
// 遍历手机号列表,根据手机号获取用户 ID
split.forEach {
val userId = getUserIdByMobile(accessToken, it)
if (!userId.isNullOrEmpty()) {
// 如果成功获取到用户 ID,加入列表
userIdList.add(userId)
}
}
// 如果没有成功获取到任何用户 ID,则打印错误日志并退出
if (userIdList.size == 0) {
Logger.error("无法根据手机 ${rightPopMap["mobile"]} 获取到企业微信用户id")
return
}
// 将所有用户 ID 拼接成一个以 "|" 分隔的字符串
val userId: String = userIdList.joinToString("|")
// 根据业务需要构建消息,这里为文本卡片消息类型
val message = typeTextCardData(rightPopMap, userId, agentId!!)
// 将消息转换成 JSON 格式并发送
sendMessage(accessToken, objectMapper.writeValueAsString(message))
}
// 构建文本卡片消息的数据结构
fun typeTextCardData(
rightPopMap: MutableMap<String, String?>,
userId: String,
agentId: String
): MutableMap<String, Any> {
// 创建用于构建消息体的 Map
val bodyMap = mutableMapOf<String, Any>()
val textcardMap = mutableMapOf<String, String>()
// 如果消息没有附件,则填充文本卡片内容
if (rightPopMap[MessageConstant.REDIS_MESSAGE_ATTACH].isNullOrEmpty()) {
// 填充文本卡片的标题、描述、URL 和按钮文本
textcardMap[MessageConstant.REDIS_MESSAGE_TEXTCARD_TITLE] = rightPopMap["title"] as String
textcardMap[MessageConstant.REDIS_MESSAGE_TEXTCARD_DESCRIPTION] = rightPopMap["msg"] as String
textcardMap[MessageConstant.REDIS_MESSAGE_TEXTCARD_URL] = rightPopMap["url"] as String
textcardMap[MessageConstant.REDIS_MESSAGE_TEXTCARD_BTNTXT] = rightPopMap["btnTxt"] as String
// 将消息相关信息填充到 bodyMap 中
bodyMap[MessageConstant.REDIS_MESSAGE_TOUSER] = userId
bodyMap[MessageConstant.REDIS_MESSAGE_MSGTYPE] = MessageConstant.MESSAGE_TYPE_TEXT_CARD
bodyMap[MessageConstant.REDIS_MESSAGE_AGENTID] = agentId
bodyMap[MessageConstant.REDIS_MESSAGE_TEXTCARD] = textcardMap
}
// 设置消息的其他参数
bodyMap[MessageConstant.REDIS_MESSAGE_ENABLE_ID_TRANS] = 0
bodyMap[MessageConstant.REDIS_MESSAGE_ENABLE_DUPLICATE_CHECK] = 0
bodyMap[MessageConstant.REDIS_MESSAGE_DUPLICATE_CHECK_INTERVAL] = 1800
return bodyMap
}
}
通过定时器方式进行发送:
@Component
class MessageTimer(val qyWxMessageService: QyWxMessageService, val redisTemplate: RedisTemplate<String, String>, val pushService: PushService) {
// 用于将数据序列化和反序列化的 ObjectMapper 实例
val objectMapper = ObjectMapper()
// 定时任务,每隔 2 秒执行一次
@Scheduled(cron = "0/2 * * * * ? ")
fun sendMsg() {
// 从 Redis 队列中读取一条消息,使用 rightPop 操作从列表的右侧弹出
val rightPop = redisTemplate.opsForList().rightPop(MessageConstant.REDIS_KEY_MESSAGE_QUEUE)
// 如果没有消息,则直接返回,不做任何处理
if (rightPop.isNullOrEmpty()) {
return
}
// 将从 Redis 中读取的 JSON 字符串反序列化为 MutableMap
val rightPopMap: MutableMap<String, String?> =
objectMapper.readValue(rightPop, Map::class.java) as MutableMap<String, String?>
// 如果消息中包含手机号,则进行企业微信消息发送操作
if (!rightPopMap["mobile"].isNullOrEmpty()) {
qyWxMessageService.sendMessage(rightPopMap)
}
}
}
4. 总结:
-
推送三大参数:
corpid
、corpsecret
、agentId
均为客户提供。 -
**手机号参数:**为数据库中获取。
-
**推送消息类型:**上方已提供
官方文档
及调试界面
,需选择不同消息类型,请详见官方文档
-
优化建议:
-
异步推送消息:
为了提高系统的响应性和处理能力,可以将消息推送过程改为异步执行,并支持错误处理和重试机制,避免主线程阻塞。我们可以使用 Kotlin 的
Coroutine
来实现异步操作。 -
限制时间获取:
为了避免过长的等待时间,可以给获取
access_token
或推送消息的操作设置超时限制。如果操作超过一定时间未完成,则抛出超时异常。 -
错误处理和重试机制:
在请求过程中,可能会遇到网络不稳定、服务端异常等问题。可以添加错误处理机制并结合重试策略,确保消息推送的稳定性。
-