kotlin 对接企业微信(实现消息推送及完整代码示例)

前言

在软件开发过程中,客户提出需求,要求平台能够将消息推送到企业微信,并根据不同场景支持推送不同的消息模板。以文本卡片消息类型为例,平台需要根据不同的业务需求,灵活选择和推送合适的消息模板,从而提高消息的传达效果和用户体验。


注意

下方总结中有优化建议,因时间原因,暂无法进行示例代码优化,感兴趣的可以了解。


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:企业ID
    • corpsecret:凭证密钥
  • 部分示例代码:

    • // 定义一个函数 `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. 总结:

  1. 推送三大参数:corpidcorpsecretagentId 均为客户提供。

  2. **手机号参数:**为数据库中获取。

  3. **推送消息类型:**上方已提供官方文档调试界面,需选择不同消息类型,请详见官方文档

  4. 优化建议:

    1. 异步推送消息:

      为了提高系统的响应性和处理能力,可以将消息推送过程改为异步执行,并支持错误处理和重试机制,避免主线程阻塞。我们可以使用 Kotlin 的 Coroutine 来实现异步操作。

    2. 限制时间获取:

      为了避免过长的等待时间,可以给获取 access_token 或推送消息的操作设置超时限制。如果操作超过一定时间未完成,则抛出超时异常。

    3. 错误处理和重试机制:

      在请求过程中,可能会遇到网络不稳定、服务端异常等问题。可以添加错误处理机制并结合重试策略,确保消息推送的稳定性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白的一叶扁舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值