文章目录

需求

实现

  • 步骤:获取component_verify_ticket——>获取component_access_token——>获取authorizer_access_token——>调接口发布小程序
  • 1、获取component_verify_ticket
  • controller
  • 实现类
  • 2、获取component_access_token
  • 实现类
  • 3、获取authorizer_access_token
  • 步骤:
  • 获取预授权码pre_auth_code——>微信公众平台管理员授权——>获取authorizer_access_token&authorizer_refresh_token
  • ①获取预授权码pre_auth_code
  • ②微信公众平台管理员授权
  • ③获取authorizer_access_token&authorizer_refresh_token
  • 4、调接口发布小程序
  • 提交代码接口:[上传小程序代码并生成体验版 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/commit.html)
  • 设置用户隐私接口:[配置小程序用户隐私保护指引 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/privacy_config/set_privacy_setting.html)
  • 提交审核接口:[提交审核 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/submit_audit.html)
  • 查询审核结果接口:[查询最新一次提交的审核状态 | 微信开放文档 (qq.com)](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/code/get_latest_auditstatus.html)
  • 最后

需求

根据现有的平台小程序,快速发布一个相同类型的小程序

如果按常规的做法每个小程序都需要经历这样的步骤:在公众平台申请appid——>拉代码到自己的仓库——>打开编译器绑定自己的appId——>上传代码到公众平台——>再提交审核——>在公众平台调整开发设置。

如果用微信开放平台的服务平台开发的步骤:在微信开放平台申请第三方服务平台——>上传小程序模板(只有第一次有)——>调接口上传小程序代码

——>调接口提交审核——>调接口发布小程序。

上传小程序模板的步骤:在公众平台申请appid——>在微信开放平台的第三方服务平台中添加开发小程序——>上传小程序代码(此时会直接上传到了开放平台的草稿箱)——>将草稿箱的代码作为普通模板

这就是服务平台开发平台的便捷,只需上传一次小程序代码,就可以快速的发布小程序。

实现

步骤:获取component_verify_ticket——>获取component_access_token——>获取authorizer_access_token——>调接口发布小程序

1、获取component_verify_ticket

官方文档:验证票据 | 微信开放文档 (qq.com)

因为这个验证票据是微信官方主动推送的,所以需要在第三方平台配置一波

微信开发工具可以打开第三方小程序没_xml

controller
@ApiOperation(value = "微信开放平台:授权事件接收URL,验证票据", notes = "zhuguangliang")
@AnonymousPostMapping("/pushTicket")
public String wechatPlatformEvent(@RequestParam("timestamp") String timestamp,
                          @RequestParam("nonce") String nonce,
                          @RequestParam("msg_signature") String msgSignature,
                          @RequestBody String postData) {
   return openPlatformUtil.parseRequest(timestamp, nonce, msgSignature, postData);
}
实现类

关于存储component_verify_ticket方案,我这里是mysql和redis各存一份。

public String parseRequest(String timeStamp, String nonce, String msgSignature, String postData) {
		try {
			if (redisUtils.hasKey(CacheKey.OPEN_PLATFORM_TICKET)) {
				return "success";
			}
            //这个类是微信官网提供的解密类,需要用到消息校验Token 消息加密Key和服务平台appid
			WXBizMsgCrypt pc = new WXBizMsgCrypt(Token, Key, APPID);
			String xml = pc.decryptMsg(msgSignature, timeStamp, nonce, postData);
			Map<String, String> result = WXXmlToMapUtil.xmlToMap(xml);// 将xml转为map
			String componentVerifyTicket = result.get("ComponentVerifyTicket");
			if (StringUtils.isNotEmpty(componentVerifyTicket)) {
				// 存储平台授权票据,保存ticket
				SpiritOpenPlatform spiritOpenPlatform = new SpiritOpenPlatform();
				if (platformMapper.selectByAppId(APPID) == 0) {
					spiritOpenPlatform.setAppid(APPID);
					spiritOpenPlatform.setAppSecret(AppSecret);
					spiritOpenPlatform.setPlatformKey(Key);
					spiritOpenPlatform.setToken(Token);
					spiritOpenPlatform.setComponentVerifyTicket(componentVerifyTicket);
					platformMapper.insert(spiritOpenPlatform);
				} else {
					spiritOpenPlatform.setComponentVerifyTicket(componentVerifyTicket);
					LambdaUpdateWrapper<SpiritOpenPlatform> updateWrapper = new LambdaUpdateWrapper<>();
					updateWrapper.eq(SpiritOpenPlatform::getAppid, APPID);
					platformMapper.update(spiritOpenPlatform, updateWrapper);
				}
				redisUtils.set(CacheKey.OPEN_PLATFORM_TICKET, componentVerifyTicket, 60 * 60 * 12);
				log.info("微信开放平台,第三方平台获取【验证票据】成功");
			} else {
				log.error("微信开放平台,第三方平台获取【验证票据】失败");
			}
		} catch (AesException e) {
			log.error("微信开放平台,第三方平台获取【验证票据】失败,异常信息:" + e.getMessage());
		}
		return "success";
	}

官方提供的解密类:WXBizMsgCrypt.java

import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;

/**
 * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
 * <ol>  * <li>第三方回复加密消息给公众平台</li>  * <li>第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。</li>
 * </ol>
 * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
 * <ol>
 * <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:  *
 * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
 * <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
 * <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
 * <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
 *
 * </ol>
 */
public class WXBizMsgCrypt {
	static Charset CHARSET = Charset.forName("utf-8");
	Base64 base64 = new Base64();
	byte[] aesKey;
	String token;
	String appId;

	/**
	 * 构造函数
	 *
	 * @param token          公众平台上,开发者设置的token
	 * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
	 * @param appId          公众平台appid
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public WXBizMsgCrypt(String token, String encodingAesKey, String appId) throws AesException {
		if (encodingAesKey.length() != 43) {
			throw new AesException(AesException.IllegalAesKey);
		}

		this.token = token;
		this.appId = appId;
		aesKey = Base64.decodeBase64(encodingAesKey + "=");
	}

	// 还原4个字节的网络字节序
	int recoverNetworkBytesOrder(byte[] orderBytes) {
		int sourceNumber = 0;
		for (int i = 0; i < 4; i++) {
			sourceNumber <<= 8;
			sourceNumber |= orderBytes[i] & 0xff;
		}
		return sourceNumber;
	}

	/**
	 * 对密文进行解密.
	 *
	 * @param text 需要解密的密文
	 * @return 解密得到的明文
	 * @throws AesException aes解密失败
	 */
	String decrypt(String text) throws AesException {
		byte[] original;
		try {
			// 设置解密模式为AES的CBC模式
			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
			SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
			IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
			cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

			// 使用BASE64对密文进行解码
			byte[] encrypted = Base64.decodeBase64(text);

			// 解密
			original = cipher.doFinal(encrypted);
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.DecryptAESError);
		}

		String xmlContent, from_appid;
		try {
			// 去除补位字符
			byte[] bytes = PKCS7Encoder.decode(original);

			// 分离16位随机字符串,网络字节序和AppId
			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

			int xmlLength = recoverNetworkBytesOrder(networkOrder);

			xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
			from_appid =
					new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.IllegalBuffer);
		}

		// appid不相同的情况
		if (!from_appid.equals(appId)) {
			throw new AesException(AesException.ValidateSignatureError);
		}
		return xmlContent;

	}

	/**
	 * * 检验消息的真实性,并且获取解密后的明文.
	 * <ol>
	 * <li>利用收到的密文生成安全签名,进行签名验证</li>
	 * <li>若验证通过,则提取xml中的加密消息</li>
	 * <li>对消息进行解密</li>
	 * </ol>
	 *
	 * @param msgSignature 签名串,对应URL参数的msg_signature
	 * @param timeStamp    时间戳,对应URL参数的timestamp
	 * @param nonce        随机串,对应URL参数的nonce
	 * @param postData     密文,对应POST请求的数据
	 * @return 解密后的原文
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public String decryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
			throws AesException {

		// 密钥,公众账号的app secret
		// 提取密文
		Object[] encrypt = extract(postData);

		// 验证安全签名
		String signature = getSHA1(token, timeStamp, nonce, encrypt[1].toString());

		// 和URL中的签名比较是否相等
		// System.out.println("第三方收到URL中的签名:" + msg_sign);
		// System.out.println("第三方校验签名:" + signature);
		if (!signature.equals(msgSignature)) {
			throw new AesException(AesException.ValidateSignatureError);
		}

		// 解密
		String result = decrypt(encrypt[1].toString());
		return result;
	}

	/**
	 * 提取出xml数据包中的加密消息
	 *
	 * @param xmltext 待提取的xml字符串
	 * @return 提取出的加密消息字符串
	 * @throws AesException
	 */
	public static Object[] extract(String xmltext) throws AesException {
		Object[] result = new Object[3];
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
			dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
			dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
			dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
			dbf.setXIncludeAware(false);
			dbf.setExpandEntityReferences(false);
			DocumentBuilder db = dbf.newDocumentBuilder();
			StringReader sr = new StringReader(xmltext);
			InputSource is = new InputSource(sr);
			Document document = db.parse(is);

			Element root = document.getDocumentElement();
			NodeList nodelist1 = root.getElementsByTagName("Encrypt");
			NodeList nodelist2 = root.getElementsByTagName("ToUserName");
			result[0] = 0;
			result[1] = nodelist1.item(0).getTextContent();

			//注意这里,获取ticket中的xml里面没有ToUserName这个元素,官网原示例代码在这里会报空
			//空指针,所以需要处理一下
			if (nodelist2 != null) {
				if (nodelist2.item(0) != null) {
					result[2] = nodelist2.item(0).getTextContent();
				}
			}
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.ParseXmlError);
		}
	}

	/**
	 * 用SHA1算法生成安全签名
	 *
	 * @param token     票据
	 * @param timestamp 时间戳
	 * @param nonce     随机字符串
	 * @param encrypt   密文
	 * @return 安全签名
	 * @throws AesException
	 */
	public static String getSHA1(String token, String timestamp, String nonce, String encrypt)
			throws AesException {
		try {
			String[] array = new String[]{token, timestamp, nonce, encrypt};
			StringBuffer sb = new StringBuffer();
			// 字符串排序
			Arrays.sort(array);
			for (int i = 0; i < 4; i++) {
				sb.append(array[i]);
			}
			String str = sb.toString();
			// SHA1签名生成
			MessageDigest md = MessageDigest.getInstance("SHA-1");
			md.update(str.getBytes());
			byte[] digest = md.digest();

			StringBuffer hexstr = new StringBuffer();
			String shaHex = "";
			for (int i = 0; i < digest.length; i++) {
				shaHex = Integer.toHexString(digest[i] & 0xFF);
				if (shaHex.length() < 2) {
					hexstr.append(0);
				}
				hexstr.append(shaHex);
			}
			return hexstr.toString();
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.ComputeSignatureError);
		}
	}
}

PKCS7Encode.java

import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * 提供基于PKCS7算法的加解密接口.
 */
public class PKCS7Encoder {
	static Charset CHARSET = Charset.forName("utf-8");
	static int BLOCK_SIZE = 32;

	/**
	 * 获得对明文进行补位填充的字节.
	 *
	 * @param count 需要进行填充补位操作的明文字节个数
	 * @return 补齐用的字节数组
	 */
	static byte[] encode(int count) {
		// 计算需要填充的位数
		int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
		if (amountToPad == 0) {
			amountToPad = BLOCK_SIZE;
		}
		// 获得补位所用的字符
		char padChr = chr(amountToPad);
		String tmp = new String();
		for (int index = 0; index < amountToPad; index++) {
			tmp += padChr;
		}
		return tmp.getBytes(CHARSET);
	}

	/**
	 * 删除解密后明文的补位字符
	 *
	 * @param decrypted 解密后的明文
	 * @return 删除补位字符后的明文
	 */
	static byte[] decode(byte[] decrypted) {
		int pad = (int) decrypted[decrypted.length - 1];
		if (pad < 1 || pad > 32) {
			pad = 0;
		}
		return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
	}

	/**
	 * 将数字转化成ASCII码对应的字符,用于对明文进行补码
	 *
	 * @param a 需要转化的数字
	 * @return 转化得到的字符
	 */
	static char chr(int a) {
		byte target = (byte) (a & 0xFF);
		return (char) target;
	}
}

WXXmlToMapUtil.java

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WXXmlToMapUtil {

	private static final Logger logger = LoggerFactory.getLogger(WXXmlToMapUtil.class);

	/**
	 * XML格式字符串转换为Map
	 *
	 * @param xml XML字符串
	 * @return XML数据转换后的Map
	 */
	public static Map<String, String> xmlToMap(String xml) {
		try {
			Map<String, String> data = new HashMap<>();
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			InputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
			org.w3c.dom.Document doc = documentBuilder.parse(stream);
			doc.getDocumentElement().normalize();
			NodeList nodeList = doc.getDocumentElement().getChildNodes();
			for (int idx = 0; idx < nodeList.getLength(); ++idx) {
				Node node = nodeList.item(idx);
				if (node.getNodeType() == Node.ELEMENT_NODE) {
					org.w3c.dom.Element element = (org.w3c.dom.Element) node;
					data.put(element.getNodeName(), element.getTextContent());
				}
			}
			stream.close();
			return data;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 将Map转换为XML格式的字符串
	 *
	 * @param data Map类型数据
	 * @return XML格式的字符串
	 */
	public static String mapToXml(Map<String, String> data) throws Exception {
		try {
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			org.w3c.dom.Document document = documentBuilder.newDocument();
			org.w3c.dom.Element root = document.createElement("xml");
			document.appendChild(root);
			for (String key : data.keySet()) {
				String value = data.get(key);
				if (value == null) {
					value = "";
				}
				value = value.trim();
				org.w3c.dom.Element filed = document.createElement(key);
				filed.appendChild(document.createTextNode(value));
				root.appendChild(filed);
			}
			TransformerFactory tf = TransformerFactory.newInstance();
			Transformer transformer = tf.newTransformer();
			DOMSource source = new DOMSource(document);
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			StringWriter writer = new StringWriter();
			StreamResult result = new StreamResult(writer);
			transformer.transform(source, result);
			String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
			writer.close();
			return output;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * (多层)xml格式字符串转换为map
	 *
	 * @param xml xml字符串
	 * @return 第一个为Root节点,Root节点之后为Root的元素,如果为多层,可以通过key获取下一层Map
	 */
	public static Map<String, Object> multilayerXmlToMap(String xml) {
		Document doc = null;
		try {
			doc = DocumentHelper.parseText(xml);
		} catch (DocumentException e) {
			logger.error("xml字符串解析,失败 --> {}", e);
		}
		Map<String, Object> map = new HashMap<>();
		if (null == doc) {
			return map;
		}
		// 获取根元素
		Element rootElement = doc.getRootElement();
		recursionXmlToMap(rootElement, map);
		return map;
	}

	/**
	 * multilayerXmlToMap核心方法,递归调用
	 *
	 * @param element 节点元素
	 * @param outmap  用于存储xml数据的map
	 */
	private static void recursionXmlToMap(Element element, Map<String, Object> outmap) {
		// 得到根元素下的子元素列表
		List<Element> list = element.elements();
		int size = list.size();
		if (size == 0) {
			// 如果没有子元素,则将其存储进map中
			outmap.put(element.getName(), element.getTextTrim());
		} else {
			// innermap用于存储子元素的属性名和属性值
			Map<String, Object> innermap = new HashMap<>();
			// 遍历子元素
			list.forEach(childElement -> recursionXmlToMap(childElement, innermap));
			outmap.put(element.getName(), innermap);
		}
	}

	/**
	 * (多层)map转换为xml格式字符串
	 *
	 * @param map     需要转换为xml的map
	 * @param isCDATA 是否加入CDATA标识符 true:加入 false:不加入
	 * @return xml字符串
	 */
	public static String multilayerMapToXml(Map<String, Object> map, boolean isCDATA) {
		String parentName = "xml";
		Document doc = DocumentHelper.createDocument();
		doc.addElement(parentName);
		String xml = recursionMapToXml(doc.getRootElement(), parentName, map, isCDATA);
		return formatXML(xml);
	}

	/**
	 * multilayerMapToXml核心方法,递归调用
	 *
	 * @param element    节点元素
	 * @param parentName 根元素属性名
	 * @param map        需要转换为xml的map
	 * @param isCDATA    是否加入CDATA标识符 true:加入 false:不加入
	 * @return xml字符串
	 */
	private static String recursionMapToXml(Element element, String parentName, Map<String, Object> map, boolean isCDATA) {
		Element xmlElement = element.addElement(parentName);
		map.keySet().forEach(key -> {
			Object obj = map.get(key);
			if (obj instanceof Map) {
				recursionMapToXml(xmlElement, key, (Map<String, Object>) obj, isCDATA);
			} else {
				String value = obj == null ? "" : obj.toString();
				if (isCDATA) {
					xmlElement.addElement(key).addCDATA(value);
				} else {
					xmlElement.addElement(key).addText(value);
				}
			}
		});
		return xmlElement.asXML();
	}

	/**
	 * 格式化xml,显示为容易看的XML格式
	 *
	 * @param xml 需要格式化的xml字符串
	 */
	public static String formatXML(String xml) {
		String requestXML = null;
		try {
			// 拿取解析器
			SAXReader reader = new SAXReader();
			Document document = reader.read(new StringReader(xml));
			if (null != document) {
				StringWriter stringWriter = new StringWriter();
				// 格式化,每一级前的空格
				OutputFormat format = new OutputFormat("    ", true);
				// xml声明与内容是否添加空行
				format.setNewLineAfterDeclaration(false);
				// 是否设置xml声明头部
				format.setSuppressDeclaration(false);
				// 是否分行
				format.setNewlines(true);
				XMLWriter writer = new XMLWriter(stringWriter, format);
				writer.write(document);
				writer.flush();
				writer.close();
				requestXML = stringWriter.getBuffer().toString();
			}
			return requestXML;
		} catch (Exception e) {
			logger.error("格式化xml,失败 --> {}", e);
			return null;
		}
	}
}

AesException.java

@SuppressWarnings("serial")
public class AesException extends Exception {

	public final static int OK = 0;
	public final static int ValidateSignatureError = -40001;
	public final static int ParseXmlError = -40002;
	public final static int ComputeSignatureError = -40003;
	public final static int IllegalAesKey = -40004;
	public final static int ValidateCorpidError = -40005;
	public final static int EncryptAESError = -40006;
	public final static int DecryptAESError = -40007;
	public final static int IllegalBuffer = -40008;

	private int code;

	private static String getMessage(int code) {
		switch (code) {
			case ValidateSignatureError:
				return "签名验证错误";
			case ParseXmlError:
				return "xml解析失败";
			case ComputeSignatureError:
				return "sha加密生成签名失败";
			case IllegalAesKey:
				return "SymmetricKey非法";
			case ValidateCorpidError:
				return "corpid校验失败";
			case EncryptAESError:
				return "aes加密失败";
			case DecryptAESError:
				return "aes解密失败";
			case IllegalBuffer:
				return "解密后得到的buffer非法";
			default:
				return null; // cannot be
		}
	}

	public int getCode() {
		return code;
	}

	AesException(int code) {
		super(getMessage(code));
		this.code = code;
	}

}

2、获取component_access_token

官方文档:令牌 | 微信开放文档 (qq.com)

实现类
@Override
public void getComponentAccessToken() {
    String componentAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/component/api_component_token";
    if (redisUtils.hasKey(CacheKey.COMPONENT_ACCESS_TOKEN)) {
        if (redisUtils.getExpire(CacheKey.COMPONENT_ACCESS_TOKEN) > 10 * 60) {
            return;
        }
    }
    JSONObject params = new JSONObject();
    //第三方平台的appid、appsecret
    params.put("component_appid", APPID);
    params.put("component_appsecret", AppSecret);
    String component_verify_ticket;
    if (redisUtils.hasKey(CacheKey.OPEN_PLATFORM_TICKET)) {
        component_verify_ticket = (String) redisUtils.get(CacheKey.OPEN_PLATFORM_TICKET);
    } else {
        //查询数据库中的component_verify_ticket
        component_verify_ticket = platformMapper.selectTicket();
        redisUtils.set(CacheKey.OPEN_PLATFORM_TICKET, component_verify_ticket, 60 * 60 * 12);
    }
    if (StringUtils.isBlank(component_verify_ticket)) {
        throw new BadRequestException("微信开放平台,第三方平台获取【验证票据】失败");
    }
    params.put("component_verify_ticket", component_verify_ticket);
    JSONObject data = JSONObject.parseObject(HttpUtil.post(componentAccessTokenUrl, JSON.toJSONString(params)));
    if (data.containsKey("component_access_token")) {
        String componentAccessToken = data.getString("component_access_token");
        //将component_access_token存入redis中,并设置2个小时的过期时间
        redisUtils.set(CacheKey.COMPONENT_ACCESS_TOKEN, componentAccessToken, 60 * 60 * 2);
        return;
    }
    redisUtils.del(CacheKey.COMPONENT_ACCESS_TOKEN);
    redisUtils.del(CacheKey.OPEN_PLATFORM_TICKET);
    throw new BadRequestException("获取component_access_token失败,请重试,接口返回信息:" + data.toJSONString());
}

3、获取authorizer_access_token

步骤:
获取预授权码pre_auth_code——>微信公众平台管理员授权——>获取authorizer_access_token&authorizer_refresh_token

官方文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html

①获取预授权码pre_auth_code
@Override
public String getPreAuthCode() {
    //先判断component_access_token是否过期,如果过期重新获取
    getComponentAccessToken();
    String preAuthCodeUrl = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=" + redisUtils.get(CacheKey.COMPONENT_ACCESS_TOKEN);
    JSONObject params = new JSONObject();
    params.put("component_appid", APPID);
    JSONObject data = JSONObject.parseObject(HttpUtil.post(preAuthCodeUrl, JSON.toJSONString(params)));
    if (data.containsKey("pre_auth_code")) {
    	return data.getString("pre_auth_code");
    }
    throw new BadRequestException("获取预授权失败,接口返回信息:" + data.toJSONString());
}
②微信公众平台管理员授权

生成授权链接,可以在前端用a标签填入这个接口地址,点击a标签打开新标签页,管理员进行扫码授权

@Override
public void toAuthorization(HttpServletResponse response) throws IOException {
    String authUrl = "https://mp.weixin.qq.com/cgi-bin/componentloginpagecomponent_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=1";
    String redirectUrl = "https://服务端地址/callback";
    String preAuthCode = getPreAuthCode();
    response.setStatus(301);
    response.sendRedirect(String.format(authUrl, APPID, preAuthCode, redirectUrl));
}

授权之后,会回调:“https://服务端地址/callback”,并将授权码auth_code携带到这个接口

@ApiOperation(value = "微信开放平台:用户授权后,回调地址", notes = "zhuguangliang")
	@AnonymousGetMapping("/callback")
	@Notice("2393194918@qq.com")
	public void callback(String auth_code) {
//		openPlatformService.getAuthorizationInfo(auth_code);
	}
③获取authorizer_access_token&authorizer_refresh_token

官方文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html

说明:authorization_code就是授权之后,返回的授权码

如果你使用微信的第三方平台管理工具,可以在数据库中直接获取authorizer_refresh_token,然后根据:

获取/刷新接口调用令牌 | 微信开放文档 (qq.com)这个接口进行刷新authorizer_access_token,下面是实现代码:

@Override//authorizerAppId为授权的appid
public void getAuthorizationInfo(String authorizerAppId) {
    
    String key = CacheKey.AUTH_ACCESS_TOKEN + authorizerAppId;
    //这一步是为如果发现这个token快过期,则刷新
    if (redisUtils.hasKey(key) && redisUtils.getExpire(key) > 5 * 60) {
        return;
    }
    getComponentAccessToken();
    String refreshTokenUrl = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=" + redisUtils.get(CacheKey.COMPONENT_ACCESS_TOKEN);
    JSONObject param = new JSONObject();
    param.put("component_appid", APPID);
    param.put("authorizer_appid", authorizerAppId);
    String refreshToken;
    if (redisUtils.hasKey(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId)) {
        refreshToken = (String) redisUtils.get(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId);
    } else {
        //从微信第三方管理工具的数据库中查询出authorizer_refresh_token
        refreshToken = platformMapper.selectRefreshTokenByAppId(authorizerAppId);
        if (StringUtils.isBlank(refreshToken)) {
            throw new BadRequestException("小程序管理员未授权,请让小程序管理员重新授权");
        }
        //计算刷新令牌的过期时间
         //从微信第三方管理工具的数据库中查询出authorizer_refresh_token的过期时间
        LocalDateTime authTime = platformMapper.selectAuthTimeByAppId(authorizerAppId).toLocalDateTime();
        Duration duration = Duration.between(authTime, LocalDateTime.now());
        redisUtils.set(CacheKey.AUTH_REFRESH_TOKEN + authorizerAppId, refreshToken, 60 * 60 * 24 * 30 - (duration.toMillis() / 1000 + 60));
    }
    param.put("authorizer_refresh_token", refreshToken);
    JSONObject tokenData = JSONObject.parseObject(HttpUtil.post(refreshTokenUrl, JSON.toJSONString(param)));
    if (tokenData.containsKey("authorizer_access_token")) {
        redisUtils.set(CacheKey.AUTH_ACCESS_TOKEN + authorizerAppId, tokenData.getString("authorizer_access_token"), 2 * 60 * 60);
        LambdaUpdateWrapper<SpiritOpenPlatform> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(SpiritOpenPlatform::getAppid, authorizerAppId);
        SpiritOpenPlatform openPlatform = new SpiritOpenPlatform();
        openPlatform.setAuthorizerRefreshToken(refreshToken);
        openPlatform.setAuthorizerAccessToken(tokenData.getString("authorizer_access_token"));
        platformMapper.update(openPlatform, updateWrapper);
        return;
    }
    throw new BadRequestException("获取authorizer_access_token失败,接口返回信息:" + tokenData.toJSONString());
}

到此,微信第三方平台开发中的关键token,authorizer_access_token已经成功获取

4、调接口发布小程序

提交代码接口:上传小程序代码并生成体验版 | 微信开放文档 (qq.com)
设置用户隐私接口:配置小程序用户隐私保护指引 | 微信开放文档 (qq.com)
提交审核接口:提交审核 | 微信开放文档 (qq.com)
查询审核结果接口:查询最新一次提交的审核状态 | 微信开放文档 (qq.com)
  • 项目中可以采用定时任务的策略调用该接口

最后

总结:微信第三方平台开发的流程,涉及的接口有点多,微信的文档其实也写的很详细了,但是在开发过程中,免不了遇见莫名其妙的问题,这个时候可以在微信开放社区里找找或者搜下博客。