今天我们来讲一下微信小微商户进件V3版本的接口对接。
首先我们来看一下官方的文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/applyment/chapter3_1.shtml
根据文档提示,小微商户进件对接协议和特约商户是一样的,只是参数不一样。我们这里以小微商户说明(主要是公司需要)。
首先引入JRE包,后面会用到。这里使用的是maven。
<groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId>
进件需要我们先上传身份证照片,请参考
微信小微商户进件(二):图片上传
上代码
ApplymentBo.java
package com.pay.wechat.bo.small.v3;
import java.io.ByteArrayInputStream; import java.security.cert.X509Certificate; import java.util.HashMap;
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;
* 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/applyment/chapter3_1.shtml * @author libaibai@chinatenet.com public class ApplymentBo {
* @param contactName 管理员姓名 * @param contactIdNum 管理员身份证号码 * @param contactMobile 管理员手机号码 * @param contactMail 管理员邮箱 * @param microAddressCode * 门店编码,参考:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter4_1.shtml * @param microAddress 门店地址 * @param idCardCopy 身份证正面照片 * @param idCardNational 身份证反面照片 * @param cardPeriodBegin 身份证有效期开始时间,格式:2011-06-24 * @param cardPeriodEnd 身份证有效期结束时间,格式:2011-06-24 * @param servicePhone 客户电话 * @param accountBank 开户行银行名称 * @param bankAddressCode 开户行编码 * @param bankName 开户银行全称(含支行] <pre> * 2、“开户银行”为其他银行,则开户银行全称(含支行)和开户银行联行号二选一 // * 3、需填写银行全称,如"深圳农村商业银行XXX支行",详细参见《开户银行全称(含支行)对照表》 * @param accountNumber 银行卡号 public void exe(String contactName, String contactIdNum, String contactMobile, String contactMail, String microName, String microAddressCode, String microAddress, String idCardCopy, String idCardNational, String cardPeriodBegin, String cardPeriodEnd, String servicePhone, String accountBank, String bankAddressCode, String bankName, String accountNumber) 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(contactName, certx); // 超级管理员姓名 String contact_id_number = RsaEncryptUtil.rsaEncryptOAEP(contactIdNum, certx); // 超级管理员身份证件号码 String mobile_phone = RsaEncryptUtil.rsaEncryptOAEP(contactMobile, certx);// 联系手机 String contact_email = RsaEncryptUtil.rsaEncryptOAEP(contactMail, 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 = microName; // 门店名称 String micro_address_code = microAddressCode; // 门店省市编码 String micro_address = microAddress; // 门店街道名称
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 = idCardCopy; // 身份证人像面照片 // String id_card_national = idCardNational; // 身份证国徽面照片
String id_card_name = RsaEncryptUtil.rsaEncryptOAEP(contactName, certx); // 身份证姓名 String id_card_number = RsaEncryptUtil.rsaEncryptOAEP(contactIdNum, certx); // 身份证号码 // String card_period_begin = "2011-06-24"; // 身份证有效期开始时间示例值:2026-06-06 // String card_period_end = "2021-06-24"; // 身份证有效期结束时间示例值: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", idCardCopy); id_card_info.put("id_card_national", idCardNational); 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", cardPeriodBegin); id_card_info.put("card_period_end", cardPeriodEnd);
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", microName); business_info.put("service_phone", servicePhone);
// 入驻结算规则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(contactName, certx); // 开户名称(该字段需进行加密处理) // String account_bank = "建设银行"; // 开户银行开户银行,详细参见《开户银行对照表》 示例值:工商银行 // String bank_address_code = "440300"; // 开户银行省市编码至少精确到市,详细参见《省市区编号对照表》 示例值:110000
// 2、“开户银行”为其他银行,则开户银行全称(含支行)和开户银行联行号二选一 // 3、需填写银行全称,如"深圳农村商业银行XXX支行",详细参见《开户银行全称(含支行)对照表》 // String bank_name = ""; // 开户银行全称(含支行] String account_number = RsaEncryptUtil.rsaEncryptOAEP(accountNumber, 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", accountBank); bank_account_info.put("bank_address_code", bankAddressCode); bank_account_info.put("bank_name", bankName); bank_account_info.put("account_number", account_number);
String business_code = OrderIDUtil.getOrderID(null); // 申请单号 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); String body = JSONObject.fromObject(map).toString(); String str = HttpUrlUtil.sendPost(body);
public static void main(String[] args) { String contactName = "张**"; // 超级管理员姓名 String contactIdNum = "520201************"; // 身份证号码 String contactMobile = "139*********"; // 手机号码 String contactMail = "3*******@qq.com"; // 联系邮箱 String microName = "联运公司停车场"; // 门店名称(也用于简称)
// 参考:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter4_1.shtml String microAddressCode = "520200"; String microAddress = "**************路52号"; // 门店地址
String idCardCopy = "eYjXW9AMg-***********************************************-w508XSWniUlM"; // 身份证正面照 String idCardNational = "eYjXW9AMg-************************************-RPk8-eZ7PJvD8XBtaTZ1a1YEIN1PSeuRnkM"; // 身份证反面照
String cardPeriodBegin = "2007-08-10"; // 身份证有效期开始时间2011-06-24 String cardPeriodEnd = "2027-08-10";// 身份证有效期结束时间2011-06-24 String servicePhone = "139*********";// 客服电话; 无特殊使用管理员手机号码 String accountBank = "招商银行"; // 开户银行 String bankAddressCode = "520200"; // 开户银行省市编码至少精确到市,详细参见《省市区编号对照表》 示例值:110000;
// 2、“开户银行”为其他银行,则开户银行全称(含支行)和开户银行联行号二选一 // 3、需填写银行全称,如"深圳农村商业银行XXX支行",详细参见《开户银行全称(含支行)对照表》 String bankName = ""; // 开户银行全称(含支行] String accountNumber = "************************"; // 银行账号
ApplymentBo t = new ApplymentBo(); t.exe(contactName, contactIdNum, contactMobile, contactMail, microName, microAddressCode, microAddress, idCardCopy, idCardNational, cardPeriodBegin, cardPeriodEnd, servicePhone, accountBank, bankAddressCode, bankName, accountNumber);
httpUrlUtil.java
package com.pay.wechat.bo.small.v3.util;
import java.security.PrivateKey; import java.security.Signature;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
import com.util.UUIDUtil;
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/"; // 查询申请状态
public static String sendPost(String body) { String url = host + APPLY_PATH; String wxSerialNo = CertUtil.getCertSerialNo(); String authorization = getToken(POST, url, body); WebClient client = WebClient.create(host); 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); Response r = client.post(body); return r.readEntity(String.class);
public static String sendGet() { String url = host + CERT_PATH; String authorization = getToken(GET, url, ""); WebClient client = WebClient.create(host); client.header("Content-Type", "application/json; charset=UTF-8"); client.header("Accept", "application/json"); client.header("User-Agent", "application/json"); client.header("Authorization", authorization); Response r = client.get(); return r.readEntity(String.class);
public static String sendGet(String applymentId) { String url = host + APPLY_QUERY_PATH + applymentId; String authorization = getToken(GET, url, ""); WebClient client = WebClient.create(host); 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);
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 certificateSerialNo = null; signature = sign(message.getBytes("utf-8")); certificateSerialNo = CertUtil.getSerialNo("");
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); 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;
// 微信证书私钥路径(从微信商户平台下载,保存在本地) 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) public static PrivateKey getPrivateKey() throws IOException { String content = new String(Files.readAllBytes(Paths.get(APICLIENT_KEY)), StandardCharsets.UTF_8); 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) 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); } 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 public static String getSerialNo(String certPath) throws IOException { X509Certificate certificate = getCertificate(certPath); return certificate.getSerialNumber().toString(16).toUpperCase();
public static String getCertSerialNo() throws Exception { String str = HttpUrlUtil.sendGet(); JSONObject json = JSONObject.fromObject(str); JSONArray jsonArray = JSONArray.fromObject(json.optString("data")); JSONObject jsonObject = jsonArray.getJSONObject(0); return jsonObject.optString("serial_no");
public static String getCertStr() throws Exception { 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")); //System.out.println(str); //System.out.println(jsonCert); String certKeyString = AesUtil.decryptToString(jsonCert.getString("associated_data").getBytes(), jsonCert.getString("nonce").getBytes(), jsonCert.getString("ciphertext"));
public static void main(String[] args) { System.out.println(CertUtil.getCertStr()); // TODO Auto-generated catch block
AesUtil.java
package com.dlysw.pay.wechat.bo.small.v3.util;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec;
import com.dlyspublic.util.Config;
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 { 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;
public class ApplymentQueryBo {
* @param applymentId 申请单号 public String query(String applymentId) { String str = HttpUrlUtil.sendGet(applymentId);
public static void main(String[] args) { String applymentId = "200000xxxxxxxxxx"; ApplymentQueryBo b = new ApplymentQueryBo();
返回成功:
{"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"}
补充:
package com.pay.wechat.bo.small.v3.util;
import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.X509Certificate;
import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException;
public class RsaEncryptUtil {
* @throws IllegalBlockSizeException public static String rsaEncryptOAEP(String message, X509Certificate certificate) throws IllegalBlockSizeException, IOException { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
byte[] data = message.getBytes("utf-8"); byte[] cipherdata = cipher.doFinal(data); return Base64.getEncoder().encodeToString(cipherdata); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); } catch (InvalidKeyException e) { throw new IllegalArgumentException("无效的证书", e); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
* @throws BadPaddingException public static String rsaDecryptOAEP(String ciphertext, PrivateKey privateKey) throws BadPaddingException, IOException { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] data = Base64.getDecoder().decode(ciphertext); return new String(cipher.doFinal(data), "utf-8"); } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); } catch (InvalidKeyException e) { throw new IllegalArgumentException("无效的私钥", e); } catch (BadPaddingException | IllegalBlockSizeException e) { throw new BadPaddingException("解密失败");
|