项目接入微信公众号生产事故

343 阅读8分钟

前言

  节前放假的前一天在公司值班划水中,然后B2B企业微信群里就有人@我说xxx商户说收不到推送,一开始觉得没啥。后来查询日志就在疯狂的打印error日志

事故经过

查看服务器错误日志提示: {"errcode":45009,"errmsg":"reach max api daily quota limit, could get access_token by getStableAccessToken, more details at mmbizurl.cn/s/JtxxFh33r rid: 6447ae3d-57e8ceff-72c09dfb"}

大概得意思就是
已达到api每日最大配额限制,可通过getStableAccessToken获取access_token,详情请访问mmbizurl.cn/s/JtxxFh33r rid: 6447ae3d-57e8cef -72c09dfb

大概我也知道问题所在啦但是想着量应该没这么大啊,先是解决生产问题 我找到微信开发者文档developers.weixin.qq.com/miniprogram…
首先# 使用AppSecret重置 API 调用次数
.清除命令 curl -X POST 'api.weixin.qq.com/cgi-bin/cle…' 调用完发现生产可以正常发送销售消息啦
随后创建一个企业微信群 把各个项目的负责人全部拉进去 让他们把测试环境获取AccessToken的调用注释掉部署下测试环境

企业微信截图_17116133799405.png

事故重现-配置问题

##发现生产和测试用的appid和secret是一套
##获取AccessToken的api也不是稳定版 ##获取AccessToken的token也没有缓存,每次都是重新调用也有并发问题 看的我这个恶心

crm:
  invitationLink: https://www.cogolinks.com/register?marketingCode=
  wxGetSessionUrl: https://api.weixin.qq.com/sns/jscode2session?
  wxAppId: 
  wxSecret: 
  officialAccountAppId: wxc2b00759d0fee5
  officialAccountSecret: 80b15cb9dee509b0957f02cd
  officialAccountUrl: https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&
  officialAccountSendUrl: https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=
  officialAccountGetOpenIdUrl: https://api.weixin.qq.com/sns/oauth2/access_token?

问题代码

WxSendMessageReq req = new WxSendMessageReq();
req.setSaleId(Long.valueOf(gclUser.getSalespersonId()));
req.setTemplateId("");
req.setFirstData("充值资金入账审核拒绝");
String data1 = String.format("您的客户%s有一笔充值资金%s%s已退回,请及时关注",
        gclMno.getMecName(),enterAccountNotifyReq.getEntryCurrency(),enterAccountNotifyReq.getEntryAmount().toString());
req.setData1(data1);
req.setData2("/");
req.setData3(DateUtils.datetoString(DateUtils.getNowTime(),DateUtils.YYYYMMDDHHMM));
req.setRemarkData("/");
this.wxSendMessageBusiness.sendWxMessage(req);
package com.cogo.business.service.message.business;

import com.cogo.business.config.CrmConfig;
import com.cogo.business.domain.crm.CrmSalesperson;
import com.cogo.business.enums.BusinessExceptionEnum;
import com.cogo.business.message.request.WxSendMessageReq;
import com.cogo.business.message.vo.WxMssDataDto;
import com.cogo.business.message.vo.WxMssVo;
import com.cogo.business.message.vo.WxSession;
import com.cogo.business.service.crm.CrmSalespersonService;
import com.cogo.business.utils.HttpClient;
import com.cogo.common.util.ExceptionHelper;
import com.suixingpay.ace.common.json.JsonUtil;
import com.suixingpay.ace.data.api.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 类描述:TODO
 *
 * @author hjs
 * @date 2022-08-31 17:39
 **/
@Service
@Slf4j
public class WxSendMessageBusiness {


    @Autowired
    private CrmConfig crmConfig;

    @Autowired
    private CrmSalespersonService crmSalespersonService;

    public Response sendWxMessage(WxSendMessageReq wxSendMessageReq) {
        log.info("给销售发送微信公众号销售开始:{}",JsonUtil.objectToJson(wxSendMessageReq));
        CrmSalesperson query = new CrmSalesperson();
        query.setId(wxSendMessageReq.getSaleId());
        CrmSalesperson crmSalesperson = crmSalespersonService.findOneById(wxSendMessageReq.getSaleId());
        if (null == crmSalesperson || StringUtils.isBlank(crmSalesperson.getOfficialAccountOpenId())) {
            log.info("销售编号:{}没有绑定微信公众号");
            return Response.failOf("销售没有绑定微信公众号");
        }
        String response = "";
        try {
            String accessToken = getAccessToken();
            WxMssVo wxMssVo = new WxMssVo();
            wxMssVo.setTouser(crmSalesperson.getOfficialAccountOpenId());
            wxMssVo.setTemplate_id(wxSendMessageReq.getTemplateId());
            Map<String, WxMssDataDto> data = new HashMap<>();
            data.put("first", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getFirstData()) ? "" : wxSendMessageReq.getFirstData()).build());
            data.put("keyword1", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getData1()) ? "" : wxSendMessageReq.getData1()).build());
            data.put("keyword2", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getData2()) ? "" : wxSendMessageReq.getData2()).build());
            data.put("keyword3", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getData3()) ? "" : wxSendMessageReq.getData3()).build());
            data.put("keyword4", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getData4()) ? "" : wxSendMessageReq.getData4()).build());
            data.put("remark", WxMssDataDto.builder().value(StringUtils.isEmpty(wxSendMessageReq.getRemarkData()) ? "" : wxSendMessageReq.getRemarkData()).build());
            wxMssVo.setData(data);
//        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken;
            String url = crmConfig.getOfficialAccountSendUrl() + accessToken;
            String dataMsg = JsonUtil.objectToJson(wxMssVo);
            log.info("销售编号:{},销售名称:{}发送消息标题:{}微信公众号消息开始:{}", crmSalesperson.getId(), crmSalesperson.getSalespersonName(), wxSendMessageReq.getFirstData(), dataMsg);
            response = HttpClient.post(url, dataMsg);
        } catch (IOException e) {
            log.error("发送给销售:{}公众号信息失败", crmSalesperson.getSalespersonName(), e);
        }
        log.info("发送微信公众号响应结果:{}", response);
        return Response.success();
    }

    private String getAccessToken() {
//        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wxc2b00759d0fee5cf&secret=80b15cb9dee509b0957f02cd08ba83cb";
        String url = crmConfig.getOfficialAccountUrl() + "appid=" + crmConfig.getOfficialAccountAppId() + "&secret=" + crmConfig.getOfficialAccountSecret();
        String response = null;
        try {
            response = HttpClient.get(url);
        } catch (IOException e) {
            log.error("获取微信公众号Access_token异常");
            throw ExceptionHelper.business(BusinessExceptionEnum.MESSAGE_TOKEN_FAIl);
        }
        log.info("获取微信公众号原始数据:{}", response);
        System.out.println("原始数据:" + response);
        WxSession wxSession = JsonUtil.jsonToObject(response, WxSession.class);
        log.info("获取微信公众号token信息:{}", JsonUtil.objectToJson(wxSession));
        return wxSession.getAccess_token();
    }
}
package com.cogo.business.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description crm配置
 * @ClassName CrmConfig
 * @Author guo_chx@.com
 * @Version 1.0
 **/
@Data
@Component
@ConfigurationProperties(prefix = "crm")
public class CrmConfig {

    private String officialAccountAppId;
    private String officialAccountSecret;
    private String officialAccountUrl;
    private String officialAccountSendUrl;

}
package com.cogo.business.message.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

/**
 * @description:
 * @author: wen_jf@.com
 * @date: Created in 2022/7/13 21:01
 * @version: 1.0
 */
@Builder
@Data
@AllArgsConstructor
public class WxMssDataDto {
    private String value;
    private String color;
}
package com.cogo.business.message.vo;

import lombok.Data;

import java.util.Map;

/**
 * @description:
 * @author: wen_jf.com
 * @date: Created in 2022/7/13 21:00
 * @version: 1.0
 */
@Data
public class WxMssVo {
    private String touser;// 接收者openId
    private String template_id;// 消息模板id
    private Map<String,WxMssDataDto> data;// 消息内容
}

##获取稳定版接口调用凭据

获取稳定版接口调用凭据

接口应在服务器端调用,详细说明参见服务端API

接口说明

接口英文名

getStableAccessToken

功能描述

  • 获取小程序全局后台接口调用凭据,有效期最长为7200s,开发者需要进行妥善保存;
  • 有两种调用模式: 1. 普通模式,access_token 有效期内重复调用该接口不会更新 access_token,绝大部分场景下使用该模式;2. 强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token
  • 该接口调用频率限制为 1万次 每分钟,每天限制调用 50万 次;
  • getAccessToken获取的调用凭证完全隔离,互不影响。该接口仅支持 POST JSON 形式的调用;
  • 如使用云开发,可通过云调用免维护 access_token 调用;
  • 如使用云托管,也可以通过微信令牌/开放接口服务免维护 access_token 调用;

调用方式

HTTPS 调用


POST https://api.weixin.qq.com/cgi-bin/stable_token 

请求参数

属性类型必填说明
grant_typestring填写 client_credential
appidstring账号唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得。(需要已经成为开发者,且账号没有异常状态)
secretstring账号唯一凭证密钥,即 AppSecret,获取方式同 appid
force_refreshboolean默认使用 false。1. force_refresh = false 时为普通调用模式,access_token 有效期内重复调用该接口不会更新 access_token;2. 当force_refresh = true 时为强制刷新模式,会导致上次获取的 access_token 失效,并返回新的 access_token

返回参数

属性类型说明
access_tokenstring获取到的凭证
expires_innumber凭证有效时间,单位:秒。目前是7200秒之内的值。

其他说明

access_token 的存储与更新

  • access_token 的存储空间至少要保留 512 个字符;
  • 建议开发者仅在access_token 泄漏时使用强制刷新模式,该模式限制每天20次。考虑到数据安全,连续使用该模式时,请保证调用时间隔至少为30s,否则不会刷新;
  • 在普通模式调用下,平台会提前5分钟更新access_token,即在有效期倒计时5分钟内发起调用会获取新的access_token。在新旧access_token交接之际,平台会保证在5分钟内,新旧access_token都可用,这保证了用户业务的平滑过渡; 根据此特性可知,任意时刻发起调用获取到的 access_token 有效期至少为 5 分钟,即expires_in >= 300;

最佳实践

  • 在使用getAccessToken时,平台建议开发者使用中控服务来统一获取和刷新access_token。有此成熟方案的开发者依然可以复用方案并通过普通模式来调用本接口,另外请将发起接口调用的时机设置为上次获取到的access_token有效期倒计时5分钟之内即可;
  • 根据以上特性,为减少其他开发者构建中控服务的开发成本,在普通调用模式下,平台建议开发者将每次获取的access_token 在本地建立中心化存储使用,无须考虑并行调用接口时导致意外情况发生,仅须保证至少每5分钟发起一次调用并覆盖本地存储。同时,该方案也支持各业务独立部署使用,即无须中心化存储也可以保证服务正常运行;

access_token 泄漏紧急处理

  • 使用强制刷新模式以间隔30s发起两次调用可将已经泄漏的 access_token立即失效,同时正常的业务请求可能会返回错误码40001(access_token 过期),请妥善使用该策略。其次,需要立即排查泄漏原因,加以修正,必要时可以考虑重置 appsecret

调用示例

示例说明: 普通模式,获取当前有效调用凭证

请求数据示例

POST https://api.weixin.qq.com/cgi-bin/stable_token

请求示例1(不传递force_refresh,默认值为false):

{
    "grant_type": "client_credential",
    "appid": "APPID",
    "secret": "APPSECRET"
}

请求示例2(设置force_refresh为false):

{
    "grant_type": "client_credential",
    "appid": "APPID",
    "secret": "APPSECRET",
    "force_refresh": false
} 

返回数据示例

返回示例1:

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200
}

返回示例2:

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":345 
} 

示例说明: 强制刷新模式,慎用,连续使用需要至少间隔30s

请求数据示例

POST https://api.weixin.qq.com/cgi-bin/stable_token
{
    "grant_type": "client_credential",
    "appid": "APPID",
    "secret": "APPSECRET",
    "force_refresh": true
} 

返回数据示例

{
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200
} 

错误码

错误码错误码取值解决方案
-1system error系统繁忙,此时请开发者稍候再试
0okok
40002invalid grant_type不合法的凭证类型
40013invalid appid不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写
40125invalid appsecret无效的appsecret,请检查appsecret的正确性
40164invalid ip  not in whitelist将ip添加到ip白名单列表即可
41002appid missing缺少 appid 参数
41004appsecret missing缺少 secret 参数
43002require POST method需要 POST 请求
45009reach max api daily quota limit调用超过天级别频率限制。可调用clear_quota接口恢复调用额度。
45011api minute-quota reach limit  mustslower  retry next minuteAPI 调用太频繁,请稍候再试
89503此次调用需要管理员确认,请耐心等候
89506该IP调用求请求已被公众号管理员拒绝,请24小时后再试,建议调用前与管理员沟通确认
89507该IP调用求请求已被公众号管理员拒绝,请1小时后再试,建议调用前与管理员沟通确认

总结

微信AccessToken的获取应该用稳定版api:getStableAccessToken 且需要缓存

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。