评论

微信小微商户/特约商户进件V3版本对接

微信小微商户/特约商户进件V3版本对接

今天我们来讲一下微信小微商户进件V3版本的接口对接。

首先我们来看一下官方的文档:

https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/applyment/chapter3_1.shtml

根据文档提示,小微商户进件对接协议和特约商户是一样的,只是参数不一样。我们这里以小微商户说明。

首先引入JRE包,后面会用到。这里使用的是maven。

上代码

ApplymentBo.java

package com.pay.wechat.bo.small.v3;


import java.io.ByteArrayInputStream;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.util.OrderIDUtil;
import com.pay.wechat.bo.small.v3.util.CertUtil;
import com.pay.wechat.bo.small.v3.util.HttpUrlUtil;
import com.pay.wechat.bo.small.v3.util.RsaEncryptUtil;
import com.pay.contrib.apache.httpclient.util.PemUtil;
import net.sf.json.JSONObject;

/**
 * 小微商户进件V3版本<br>
 * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/applyment/chapter3_1.shtml
 * 
 * @author libaibai
 * @version 1.0 2020年9月3日
 */
@Component
public class ApplymentBo {
	String business_code = OrderIDUtil.getOrderID(null);
	public void exe() throws Exception {
		// 获取微信平台证书 并解析方法在后面
		String certString = CertUtil.getCertStr();
		ByteArrayInputStream stringStream = new ByteArrayInputStream(certString.getBytes());
		// 下面所有加密参数需要的对象
		X509Certificate certx = PemUtil.loadCertificate(stringStream);

		// 超级管理员信息
		Map<String, Object> contact_info = new HashMap<String, Object>();
		String contact_name = RsaEncryptUtil.rsaEncryptOAEP("张三", certx); // 超级管理员姓名
		String contact_id_number = RsaEncryptUtil.rsaEncryptOAEP("000000000000000000", certx); // 超级管理员身份证件号码
		String mobile_phone = RsaEncryptUtil.rsaEncryptOAEP("13600000000", certx);// 联系手机
		String contact_email = RsaEncryptUtil.rsaEncryptOAEP("zhangsan@sina.com", certx);// 联系邮箱
		contact_info.put("contact_name", contact_name);
		contact_info.put("contact_id_number", contact_id_number);
		contact_info.put("mobile_phone", mobile_phone);
		contact_info.put("contact_email", contact_email);

		// 主体资料
		String subject_type = "SUBJECT_TYPE_MICRO"; // 主体类型
		String micro_biz_type = "MICRO_TYPE_STORE"; // 小微经营类型
		String micro_name = "生态园停车"; // 门店名称
		String micro_address_code = "440300"; // 门店省市编码
		String micro_address = "南山区沙河西路科技生态园"; // 门店街道名称
		String store_entrance_pic = "oO5EoYZsdukezw2NXUxEkb9vTU7PgOu5GyMpNVdMVj5aJAwD85_8kNpakg-s4917roa97XFJf0GPdBNHEvkyf0XPzrOjeKjoBYmEL_eSk7I"; // 门店门口照片
		String micro_indoor_copy = "oO5EoYZsdukezw2NXUxEkb9vTU7PgOu5GyMpNVdMVj5aJAwD85_8kNpakg-s4917roa97XFJf0GPdBNHEvkyf0XPzrOjeKjoBYmEL_eSk7I"; // 店内环境照片
		// 证件类型,IDENTIFICATION_TYPE_IDCARD
		String id_doc_type = "IDENTIFICATION_TYPE_IDCARD";
		String id_card_copy = "oO5EoYZsdukezw2NXUxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdafxs"; // 身份证人像面照片
		String id_card_national = "oO5EoYZsdukezwdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddHj3QDW_E"; // 身份证国徽面照片
		String id_card_name = RsaEncryptUtil.rsaEncryptOAEP("张三", certx); // 身份证姓名
		String id_card_number = RsaEncryptUtil.rsaEncryptOAEP("000000000000000000", certx); // 身份证号码
		String card_period_begin = "2026-06-06"; // 身份证有效期开始时间示例值:2026-06-06
		String card_period_end = "2026-06-06"; // 身份证有效期结束时间示例值:2026-06-06

		Map<String, Object> subject_info = new HashMap<String, Object>(); // 主体资料
		Map<String, Object> micro_biz_info = new HashMap<String, Object>(); // 小微商户辅助材料
		Map<String, Object> micro_store_info = new HashMap<String, Object>(); // 门店场所信息
		Map<String, Object> identity_info = new HashMap<String, Object>(); // 经营者身份证件
		Map<String, Object> id_card_info = new HashMap<String, Object>(); // 身份证信息

		micro_store_info.put("micro_name", micro_name);
		micro_store_info.put("micro_address_code", micro_address_code);
		micro_store_info.put("micro_address", micro_address);
		micro_store_info.put("store_entrance_pic", store_entrance_pic);
		micro_store_info.put("micro_indoor_copy", micro_indoor_copy);
		micro_biz_info.put("micro_biz_type", micro_biz_type);
		micro_biz_info.put("micro_store_info", micro_store_info);
		id_card_info.put("id_card_copy", id_card_copy);
		id_card_info.put("id_card_national", id_card_national);
		id_card_info.put("id_card_name", id_card_name);
		id_card_info.put("id_card_number", id_card_number);
		id_card_info.put("card_period_begin", card_period_begin);
		id_card_info.put("card_period_end", card_period_end);
		identity_info.put("id_doc_type", id_doc_type);
		identity_info.put("id_card_info", id_card_info);
		subject_info.put("subject_type", subject_type);
		subject_info.put("micro_biz_info", micro_biz_info);
		subject_info.put("identity_info", identity_info);
		// 经营资料
		String merchant_shortname = "张三停车场"; // 商户简称
		String service_phone = "0755222222"; // 客服电话
		Map<String, Object> business_info = new HashMap<String, Object>();
		business_info.put("merchant_shortname", merchant_shortname);
		business_info.put("service_phone", service_phone);
		// 结算规则
		// 入驻结算规则ID;请选择结算规则ID,详细参见《费率结算规则对照表》 示例值:小微商户:703
		String settlement_id = "703";//
		String qualification_type = "停车缴费"; // 所属行业;请填写所属行业名称,建议参见《费率结算规则对照表》 示例值:餐饮
		Map<String, Object> settlement_info = new HashMap<String, Object>();
		settlement_info.put("settlement_id", settlement_id);
		settlement_info.put("qualification_type", qualification_type);

		// 收款银行卡
		// 账户类型 若主体为小微,可填写:经营者个人银行卡 枚举值:
		// BANK_ACCOUNT_TYPE_PERSONAL:经营者个人银行卡
		// 示例值:BANK_ACCOUNT_TYPE_CORPORATE
		String bank_account_type = "BANK_ACCOUNT_TYPE_PERSONAL";
		String account_name = RsaEncryptUtil.rsaEncryptOAEP("张三", certx); // 开户名称(该字段需进行加密处理)
		String account_bank = "建设银行"; // 开户银行开户银行,详细参见《开户银行对照表》 示例值:工商银行
		String bank_address_code = "440300"; // 开户银行省市编码至少精确到市,详细参见《省市区编号对照表》 示例值:110000

		// 1、“开户银行”为17家直连银行无需填写
		// 2、“开户银行”为其他银行,则开户银行全称(含支行)和开户银行联行号二选一
		// 3、需填写银行全称,如"深圳农村商业银行XXX支行",详细参见《开户银行全称(含支行)对照表》
		// 示例值:施秉县农村信用合作联社城关信用社
		String bank_name = ""; // 开户银行全称(含支行]
		String account_number = RsaEncryptUtil.rsaEncryptOAEP("62122xxxxxxxxxx332", certx); // 银行账号(该字段需进行加密处理)
		Map<String, Object> bank_account_info = new HashMap<String, Object>();
		bank_account_info.put("bank_account_type", bank_account_type);
		bank_account_info.put("account_name", account_name);
		bank_account_info.put("account_bank", account_bank);
		bank_account_info.put("bank_address_code", bank_address_code);
		bank_account_info.put("bank_name", bank_name);
		bank_account_info.put("account_number", account_number);

		Map<String, Object> map = new HashMap<String, Object>();
		map.put("business_code", business_code);
		map.put("contact_info", contact_info);
		map.put("subject_info", subject_info);
		map.put("business_info", business_info);
		map.put("settlement_info", settlement_info);
		map.put("bank_account_info", bank_account_info);
		try {
			String body = JSONObject.fromObject(map).toString();
			String str = HttpUrlUtil.sendPost(body);
			System.out.println(str);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		ApplymentBo t = new ApplymentBo();
		try {
			t.exe();
		} catch (Exception e) {
		}
	}
}

httpUrlUtil.java

package com.pay.wechat.bo.small.v3.util;

import java.security.PrivateKey;
import java.security.Signature;
import java.util.Base64;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
import com.util.Config;
import com.util.UUIDUtil;
import okhttp3.HttpUrl;

/**
 * HttpUrl工具类
 * 
 * @author libaibai
 * @version 1.0 2020年9月4日
 */
public class HttpUrlUtil {
	public static String SCHEMA = "WECHATPAY2-SHA256-RSA2048";
	public static String merchantId = Config.MCHIDSP; // 服务商
	public static String POST = "POST";
	public static String GET = "GET";
	public static String host = "https://api.mch.weixin.qq.com";
	public static String APPLY_PATH = "/v3/applyment4sub/applyment/"; // 申请单url
	public static String CERT_PATH = "/v3/certificates"; // 获取微信平台证书url
	public static String APPLY_QUERY_PATH = "/v3/applyment4sub/applyment/applyment_id/"; // 查询申请状态
	/**
	 * POST请求
	 */
	public static String sendPost(String body) {
		String url = host + APPLY_PATH;
		try {
			// 获取微信平台商户证书序列号
			String wxSerialNo = CertUtil.getCertSerialNo();
			String authorization = getToken(POST, url, body);
			WebClient client = WebClient.create(host);
			client.reset();
			client.header("Content-Type", "application/json; charset=UTF-8");
			client.header("Accept", "application/json");
			client.header("user-agent", "application/json");
			client.header("Wechatpay-Serial", wxSerialNo);
			client.header("Authorization", authorization);
			client.path(APPLY_PATH);
			Response r = client.post(body);
			return r.readEntity(String.class);
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * get请求
	 */
	public static String sendGet() {
		// 请求URL
		String url = host + CERT_PATH;
		try {
			String authorization = getToken(GET, url, "");
			WebClient client = WebClient.create(host);
			client.reset();
			client.header("Content-Type", "application/json; charset=UTF-8");
			client.header("Accept", "application/json");
			client.header("User-Agent", "application/json");
			client.header("Authorization", authorization);
			client.path(CERT_PATH);
			Response r = client.get();
			return r.readEntity(String.class);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * get请求
	 */
	public static String sendGet(String applymentId) {
		// 请求URL
		String url = host + APPLY_QUERY_PATH + applymentId;
		try {
			String authorization = getToken(GET, url, "");
			WebClient client = WebClient.create(host);
			client.reset();
			client.header("Content-Type", "application/json; charset=UTF-8");
			client.header("Accept", "application/json");
			client.header("User-Agent", "application/json");
			client.header("Authorization", authorization);
			client.path(APPLY_QUERY_PATH + applymentId);
			Response r = client.get();
			return r.readEntity(String.class);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 获取加密串
	 * 
	 * @param method
	 * @param url
	 * @param body
	 * @return
	 */
	public static String getToken(String method, String url, String body) {
		String nonceStr = UUIDUtil.getUUID32();
		long timestamp = System.currentTimeMillis() / 1000;
		HttpUrl httpUrl = HttpUrl.parse(url);
		String message = buildMessage(method, httpUrl, timestamp, nonceStr, body);
		String signature = null;
		String certificateSerialNo = null;
		try {
			signature = sign(message.getBytes("utf-8"));
			certificateSerialNo = CertUtil.getSerialNo("");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return SCHEMA + " mchid=\"" + merchantId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\""
				+ certificateSerialNo + "\"," + "signature=\"" + signature + "\"";
	}

	/**
	 * 得到签名字符串
	 */
	public static String sign(byte[] message) throws Exception {
		Signature sign = Signature.getInstance("SHA256withRSA");
		PrivateKey privateKey = CertUtil.getPrivateKey();
		sign.initSign(privateKey);
		sign.update(message);
		return Base64.getEncoder().encodeToString(sign.sign());
	}

	public static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) {
		String canonicalUrl = url.encodedPath();
		if (url.encodedQuery() != null) {
			canonicalUrl += "?" + url.encodedQuery();
		}
		return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n";
	}
}


CertUtil.java

package com.pay.wechat.bo.small.v3.util;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.commons.codec.binary.Base64;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

/**
 * 证书工具类
 * 
 * @author libaibai
 * @version 1.0 2020年9月4日
 */
public class CertUtil {
	// 微信证书私钥路径(从微信商户平台下载,保存在本地)
	public static String APICLIENT_KEY = "G:\\workspace\\dlysw\\src\\main\\resources\\conf\\cert\\apiclient_key.pem";
	// 微信商户证书路径(从微信商户平台下载,保存在本地)
	public static String APICLIENT_CERT = "G:\\workspace\\dlysw\\src\\main\\resources\\conf\\cert\\apiclient_cert.pem";

	/**
	 * 获取私钥。
	 *
	 * @param apiclient_key 私钥文件路径 (required)
	 * @return 私钥对象
	 */
	public static PrivateKey getPrivateKey() throws IOException {
		String content = new String(Files.readAllBytes(Paths.get(APICLIENT_KEY)), StandardCharsets.UTF_8);
		try {
			String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s+", "");
			KeyFactory kf = KeyFactory.getInstance("RSA");
			return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("当前Java环境不支持RSA", e);
		} catch (InvalidKeySpecException e) {
			throw new RuntimeException("无效的密钥格式");
		}
	}

	/**
	 * 获取商户证书。
	 *
	 * @param filename 证书文件路径 (required)
	 * @return X509证书
	 */
	public static X509Certificate getCertificate(String filename) throws IOException {
		InputStream fis = new FileInputStream(APICLIENT_CERT);
		try (BufferedInputStream bis = new BufferedInputStream(fis)) {
			CertificateFactory cf = CertificateFactory.getInstance("X509");
			X509Certificate cert = (X509Certificate) cf.generateCertificate(bis);
			cert.checkValidity();
			return cert;
		} catch (CertificateExpiredException e) {
			throw new RuntimeException("证书已过期", e);
		} catch (CertificateNotYetValidException e) {
			throw new RuntimeException("证书尚未生效", e);
		} catch (CertificateException e) {
			throw new RuntimeException("无效的证书文件", e);
		}
	}

	/**
	 * 获取商户证书序列号
* @param certPath 获取商户证书序列号 传递商号证书路径 apiclient_cert
	 * @return
	 * @throws IOException
	 */
	public static String getSerialNo(String certPath) throws IOException {
		X509Certificate certificate = getCertificate(certPath);
		return certificate.getSerialNumber().toString(16).toUpperCase();
	}

	/**
	 * 获取微信平台证书序列号
* @return
	 * @throws Exception
	 */
	public static String getCertSerialNo() throws Exception {
		try {
			String str = HttpUrlUtil.sendGet();
			System.out.println(str);
			JSONObject json = JSONObject.fromObject(str);
			JSONArray jsonArray = JSONArray.fromObject(json.optString("data"));
			JSONObject jsonObject = jsonArray.getJSONObject(0);
			return jsonObject.optString("serial_no");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 获取微信平台证书
* @return
	 * @throws Exception
	 */
	public static String getCertStr() throws Exception {
		try {
			String str = HttpUrlUtil.sendGet();
			JSONObject json = JSONObject.fromObject(str);
			JSONArray jsonArray = JSONArray.fromObject(json.optString("data"));
			JSONObject jsonObject = jsonArray.getJSONObject(0);
			JSONObject jsonCert = JSONObject.fromObject(jsonObject.optString("encrypt_certificate"));
			String certKeyString = AesUtil.decryptToString(jsonCert.getString("associated_data").getBytes(),
					jsonCert.getString("nonce").getBytes(), jsonCert.getString("ciphertext"));
			return certKeyString;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public static void main(String[] args) {
		try {
			System.out.println(CertUtil.getCertStr());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}


AesUtil.java

package com.dlysw.pay.wechat.bo.small.v3.util;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.dlyspublic.util.Config;

/**
 * 
 * @author libaibai
 * @version 1.0 2020年9月8日
 */
public class AesUtil {

	public static final int TAG_LENGTH_BIT = 128;
	public static byte[] aesKey = Config.AES_KEY_APIV3.getBytes(); // APIv3密钥

	public static String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) throws Exception {
		try {
			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
			SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
			GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
			cipher.init(Cipher.DECRYPT_MODE, key, spec);
			cipher.updateAAD(associatedData);
			return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
		} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
			throw new IllegalStateException(e);
		} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
			throw new IllegalArgumentException(e);
		}
	}
}


ok,直接运行ApplymentBo.java里面的main方法,得到返回结果

{"applyment_id": xxxxxxxxxxxxxx}

就表示进件成功了,我们登陆到商户平台看看。创建员工=API的就是刚才我们申请成功的。

最后,我们也把状态查询的代码也贴一下

ApplymentQueryBo.java

package com.pay.wechat.bo.small.v3;

import org.springframework.stereotype.Component;
import com.pay.wechat.bo.small.v3.util.HttpUrlUtil;

/**
 * 小微商户进件查询
 * 
 * @author libaibai
 * @version 1.0 2020年9月8日
 */
@Component
public class ApplymentQueryBo {

	/**
	 * 执行
* @param applymentId 申请单号
	 * @return
	 */
	public String query(String applymentId) {
		String str = HttpUrlUtil.sendGet(applymentId);
		System.out.println(str);
		return str;
	}

	public static void main(String[] args) {
		String applymentId = "200000xxxxxxxxxx";
		ApplymentQueryBo b = new ApplymentQueryBo();
		b.query(applymentId);
	}
}


返回成功:

{"applyment_id":200000xxxxxxxxxx,"applyment_state":"APPLYMENT_STATE_TO_BE_SIGNED","applyment_state_msg":"请超级管理员使用微信打开返回的“签约链接”,根据页面指引完成签约","audit_detail":[],"business_code":"WEB|1590030703","sign_url":"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQGb7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyeWZaMDl3b3JlUjIxbG9PLU52Y1YAAgRY5VZfAwQAjScA","sub_mchid":"15947111111"}

最后一次编辑于  2020-09-11  
点赞 4
收藏
评论

5 个评论

  • 苏沐晨
    苏沐晨
    2020-09-11

    感谢分享,有GitHub地址吗

    2020-09-11
    赞同 3
    回复
  • 浮光_腾讯生态服务商🌞
    浮光_腾讯生态服务商🌞
    01-14

    小微商户进件发起申请

    可以在公众号:飞付数字科技



    01-14
    赞同
    回复
  • 你好啊
    你好啊
    2022-12-15

    终于在这里找到了小微商户的最新文档

    2022-12-15
    赞同
    回复
  • 花臂Seven
    花臂Seven
    2021-01-12
    请问WebClient、还有 Config类能发下嘛 谢谢了
    
    2021-01-12
    赞同
    回复
  • 高宏宇
    高宏宇
    2020-10-21

    目前用的 还是 v2 也没去看看 v3啥变化

    2020-10-21
    赞同
    回复 1
    • 花臂Seven
      花臂Seven
      2021-01-12
      可以问下 Config 这个是干嘛的嘛
      2021-01-12
      回复
登录 后发表内容