diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java index c6480e6c..cf5bb8c3 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java @@ -1,8 +1,11 @@ package me.chanjar.weixin.mp.api; +import java.io.File; import java.util.Map; import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.mp.bean.pay.WxEntPayRequest; +import me.chanjar.weixin.mp.bean.pay.WxEntPayResult; import me.chanjar.weixin.mp.bean.pay.WxMpPayCallback; import me.chanjar.weixin.mp.bean.pay.WxMpPayRefundResult; import me.chanjar.weixin.mp.bean.pay.WxMpPayResult; @@ -85,4 +88,17 @@ public interface WxMpPayService { */ WxRedpackResult sendRedpack(WxSendRedpackRequest request) throws WxErrorException; + /** + *
 
+   * 企业付款业务是基于微信支付商户平台的资金管理能力,为了协助商户方便地实现企业向个人付款,针对部分有开发能力的商户,提供通过API完成企业付款的功能。
+   * 比如目前的保险行业向客户退保、给付、理赔。
+   * 企业付款将使用商户的可用余额,需确保可用余额充足。查看可用余额、充值、提现请登录商户平台“资金管理”https://pay.weixin.qq.com/进行操作。
+   * 注意:与商户微信支付收款资金并非同一账户,需要单独充值。
+   * 文档详见:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
+   * 接口链接:https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers
+   * @param keyFile  证书文件对象
+   * 
+ */ + WxEntPayResult entPay(WxEntPayRequest request, File keyFile) throws WxErrorException; + } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java index 965e5df1..79e2c312 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java @@ -1,6 +1,9 @@ package me.chanjar.weixin.mp.api.impl; +import java.io.File; +import java.io.FileInputStream; import java.lang.reflect.Field; +import java.security.KeyStore; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -8,8 +11,19 @@ import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; +import javax.net.ssl.SSLContext; + import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContexts; +import org.apache.http.util.EntityUtils; import org.joor.Reflect; import com.google.common.collect.Lists; @@ -23,6 +37,8 @@ import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.util.xml.XStreamInitializer; import me.chanjar.weixin.mp.api.WxMpPayService; import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.pay.WxEntPayRequest; +import me.chanjar.weixin.mp.bean.pay.WxEntPayResult; import me.chanjar.weixin.mp.bean.pay.WxMpPayCallback; import me.chanjar.weixin.mp.bean.pay.WxMpPayRefundResult; import me.chanjar.weixin.mp.bean.pay.WxMpPayResult; @@ -263,6 +279,23 @@ public class WxMpPayServiceImpl implements WxMpPayService { private void checkParameters(WxUnifiedOrderRequest request) { + checkNotNullParams(request); + + if (!TRADE_TYPES.contains(request.getTradeType())) { + throw new IllegalArgumentException("trade_type目前必须为" + TRADE_TYPES + "其中之一"); + + } + + if ("JSAPI".equals(request.getTradeType()) && request.getOpenid() == null) { + throw new IllegalArgumentException("当 trade_type是'JSAPI'时未指定openid"); + } + + if ("NATIVE".equals(request.getTradeType()) && request.getProductId() == null) { + throw new IllegalArgumentException("当 trade_type是'NATIVE'时未指定product_id"); + } + } + + private void checkNotNullParams(Object request) { List nullFields = Lists.newArrayList(); for (Entry entry : Reflect.on(request).fields() .entrySet()) { @@ -281,20 +314,6 @@ public class WxMpPayServiceImpl implements WxMpPayService { if (!nullFields.isEmpty()) { throw new IllegalArgumentException("必填字段[" + nullFields + "]必须提供值"); } - - if (!TRADE_TYPES.contains(request.getTradeType())) { - throw new IllegalArgumentException("trade_type目前必须为" + TRADE_TYPES + "其中之一"); - - } - - if ("JSAPI".equals(request.getTradeType()) && request.getOpenid() == null) { - throw new IllegalArgumentException("当 trade_type是'JSAPI'时未指定openid"); - } - - if ("NATIVE".equals(request.getTradeType()) - && request.getProductId() == null) { - throw new IllegalArgumentException("当 trade_type是'NATIVE'时未指定product_id"); - } } @Override @@ -332,4 +351,50 @@ public class WxMpPayServiceImpl implements WxMpPayService { return payInfo; } + @Override + public WxEntPayResult entPay(WxEntPayRequest request, File keyFile) throws WxErrorException { + checkNotNullParams(request); + + XStream xstream = XStreamInitializer.getInstance(); + xstream.processAnnotations(WxEntPayRequest.class); + xstream.processAnnotations(WxEntPayResult.class); + + request.setMchAppid(this.wxMpService.getWxMpConfigStorage().getAppId()); + request.setMchId(this.wxMpService.getWxMpConfigStorage().getPartnerId()); + request.setNonceStr(System.currentTimeMillis() + ""); + + String sign = this.createSign(xmlBean2Map(request), this.wxMpService.getWxMpConfigStorage().getPartnerKey()); + request.setSign(sign); + + String url = PAY_BASE_URL + "/mmpaymkttransfers/promotion/transfers"; + + try (FileInputStream instream = new FileInputStream(keyFile)) { + String mchId = request.getMchId(); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(instream, mchId.toCharArray()); + + SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build(); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, + new DefaultHostnameVerifier()); + + try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build()) { + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(new StringEntity(new String(xstream.toXML(request).getBytes("UTF-8"), "ISO-8859-1"))); + + try (CloseableHttpResponse response = httpclient.execute(httpPost)) { + String responseContent = EntityUtils.toString(response.getEntity()); + WxEntPayResult result = (WxEntPayResult) xstream.fromXML(responseContent); + if ("FAIL".equals(result.getResultCode())) { + throw new WxErrorException( + WxError.newBuilder().setErrorMsg(result.getErrCode() + ":" + result.getErrCodeDes()).build()); + } + + return result; + } + } + } catch (Exception e) { + throw new WxErrorException(WxError.newBuilder().setErrorMsg(e.getMessage()).build(), e); + } + } + } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayRequest.java new file mode 100644 index 00000000..9c7253d2 --- /dev/null +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayRequest.java @@ -0,0 +1,253 @@ +package me.chanjar.weixin.mp.bean.pay; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * 企业付款请求对象 + * 注释中各行含义如下: + * 字段名、 变量名、 是否必填、 示例值、 类型、 描述 + * Created by Binary Wang on 2016/10/02. + * @author binarywang (https://github.com/binarywang) + */ +@XStreamAlias("xml") +public class WxEntPayRequest { + /** + * 公众账号appid + * mch_appid + * 是 + * wx8888888888888888 + * String + * 微信分配的公众账号ID(企业号corpid即为此appId) + */ + @XStreamAlias("mch_appid") + private String mchAppid; + + /** + * 商户号 + * mchid + * 是 + * 1900000109 + * String(32) + * 微信支付分配的商户号 + */ + @XStreamAlias("mchid") + private String mchId; + + /** + * 设备号 + * device_info + * 否 + * 13467007045764 + * String(32) + *微信支付分配的终端设备号 + */ + @XStreamAlias("device_info") + private String deviceInfo; + + /** + * 随机字符串 + * nonce_str + *是 + *5K8264ILTKCH16CQ2502SI8ZNMTM67VS + *String(32) + *随机字符串,不长于32位 + */ + @XStreamAlias("nonce_str") + private String nonceStr; + + /** + * 签名 + * sign + * 是 + * C380BEC2BFD727A4B6845133519F3AD6 + * String(32) + *签名,详见签名算法 + */ + @XStreamAlias("sign") + private String sign; + + /** + * 商户订单号 + * partner_trade_no + * 是 + * 10000098201411111234567890 + * String + * 商户订单号, + */ + @XStreamAlias("partner_trade_no") + private String partnerTradeNo; + + /** + * 需保持唯一性 用户openid + * openid + * 是 + * oxTWIuGaIt6gTKsQRLau2M0yL16E + * String + * 商户appid下,某用户的openid + */ + @XStreamAlias("openid") + private String openid; + + /** + * 校验用户姓名选项 + * check_name + * 是 + * OPTION_CHECK + * String + * NO_CHECK:不校验真实姓名  + * FORCE_CHECK:强校验真实姓名(未实名认证的用户会校验失败,无法转账)  + * OPTION_CHECK:针对已实名认证的用户才校验真实姓名(未实名认证用户不校验,可以转账成功) + */ + @XStreamAlias("check_name") + private String checkName; + + /** + * 收款用户姓名 + * re_user_name + * 可选 + * 马花花 + * String + * 收款用户真实姓名。 + * 如果check_name设置为FORCE_CHECK或OPTION_CHECK, 则必填用户真实姓名 + */ + @XStreamAlias("re_user_name") + private String reUserName; + + /** + * 金额 + * amount + * 是 + * 10099 + * int + * 企业付款金额, 单位为分 + */ + @XStreamAlias("amount") + private Integer amount; + + /** + * 企业付款描述信息 + * desc + * 是 + * 理赔 + * String + * 企业付款操作说明信息。必填。 + */ + @XStreamAlias("desc") + private String description; + + /** + * Ip地址 + * spbill_create_ip + * 是 + * 192.168.0.1 + * String(32) + * 调用接口的机器Ip地址 + */ + @XStreamAlias("spbill_create_ip") + private String spbillCreateIp; + + public String getMchAppid() { + return mchAppid; + } + + public void setMchAppid(String mchAppid) { + this.mchAppid = mchAppid; + } + + public String getMchId() { + return mchId; + } + + public void setMchId(String mchId) { + this.mchId = mchId; + } + + public String getDeviceInfo() { + return deviceInfo; + } + + public void setDeviceInfo(String deviceInfo) { + this.deviceInfo = deviceInfo; + } + + public String getNonceStr() { + return nonceStr; + } + + public void setNonceStr(String nonceStr) { + this.nonceStr = nonceStr; + } + + public String getSign() { + return sign; + } + + public void setSign(String sign) { + this.sign = sign; + } + + public String getPartnerTradeNo() { + return partnerTradeNo; + } + + public void setPartnerTradeNo(String partnerTradeNo) { + this.partnerTradeNo = partnerTradeNo; + } + + public String getOpenid() { + return openid; + } + + public void setOpenid(String openid) { + this.openid = openid; + } + + public String getCheckName() { + return checkName; + } + + public void setCheckName(String checkName) { + this.checkName = checkName; + } + + public String getReUserName() { + return reUserName; + } + + public void setReUserName(String reUserName) { + this.reUserName = reUserName; + } + + public Integer getAmount() { + return amount; + } + + public void setAmount(Integer amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSpbillCreateIp() { + return spbillCreateIp; + } + + public void setSpbillCreateIp(String spbillCreateIp) { + this.spbillCreateIp = spbillCreateIp; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } + +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayResult.java new file mode 100644 index 00000000..b2dbb8f1 --- /dev/null +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxEntPayResult.java @@ -0,0 +1,190 @@ +package me.chanjar.weixin.mp.bean.pay; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import com.thoughtworks.xstream.annotations.XStreamAlias; + +/** + * 企业付款返回结果 + * Created by Binary Wang on 2016/10/02. + * @author binarywang (https://github.com/binarywang) + */ +@XStreamAlias("xml") +public class WxEntPayResult { + + /** + * 返回状态码 + */ + @XStreamAlias("return_code") + private String returnCode; + + /** + * 返回信息 + */ + @XStreamAlias("return_msg") + private String returnMsg; + + //############以下字段在return_code为SUCCESS的时候有返回 + + /** + * 商户appid + */ + @XStreamAlias("mch_appid") + private String mchAppid; + + /** + * 商户号 + */ + @XStreamAlias("mchid") + private String mchId; + + /** + * 设备号 + */ + @XStreamAlias("device_info") + private String deviceInfo; + + /** + * 随机字符串 + */ + @XStreamAlias("nonce_str") + private String nonceStr; + + /** + * 业务结果 + */ + @XStreamAlias("result_code") + private String resultCode; + /** + * 错误代码 + */ + @XStreamAlias("err_code") + private String errCode; + + /** + * 错误代码描述 + */ + @XStreamAlias("err_code_des") + private String errCodeDes; + + //############以下字段在return_code 和result_code都为SUCCESS的时候有返回############## + /** + * 商户订单号 + */ + @XStreamAlias("partner_trade_no") + private String partnerTradeNo; + + /** + * 微信订单号 + */ + @XStreamAlias("payment_no") + private String paymentNo; + + /** + * 微信支付成功时间 + */ + @XStreamAlias("payment_time") + private String paymentTime; + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } + + public String getReturnCode() { + return returnCode; + } + + public void setReturnCode(String returnCode) { + this.returnCode = returnCode; + } + + public String getReturnMsg() { + return returnMsg; + } + + public void setReturnMsg(String returnMsg) { + this.returnMsg = returnMsg; + } + + public String getMchAppid() { + return mchAppid; + } + + public void setMchAppid(String mchAppid) { + this.mchAppid = mchAppid; + } + + public String getMchId() { + return mchId; + } + + public void setMchId(String mchId) { + this.mchId = mchId; + } + + public String getDeviceInfo() { + return deviceInfo; + } + + public void setDeviceInfo(String deviceInfo) { + this.deviceInfo = deviceInfo; + } + + public String getNonceStr() { + return nonceStr; + } + + public void setNonceStr(String nonceStr) { + this.nonceStr = nonceStr; + } + + public String getResultCode() { + return resultCode; + } + + public void setResultCode(String resultCode) { + this.resultCode = resultCode; + } + + public String getErrCode() { + return errCode; + } + + public void setErrCode(String errCode) { + this.errCode = errCode; + } + + public String getErrCodeDes() { + return errCodeDes; + } + + public void setErrCodeDes(String errCodeDes) { + this.errCodeDes = errCodeDes; + } + + public String getPartnerTradeNo() { + return partnerTradeNo; + } + + public void setPartnerTradeNo(String partnerTradeNo) { + this.partnerTradeNo = partnerTradeNo; + } + + public String getPaymentNo() { + return paymentNo; + } + + public void setPaymentNo(String paymentNo) { + this.paymentNo = paymentNo; + } + + public String getPaymentTime() { + return paymentTime; + } + + public void setPaymentTime(String paymentTime) { + this.paymentTime = paymentTime; + } +} diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java index 092be38a..6199efeb 100644 --- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java +++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java @@ -1,5 +1,7 @@ package me.chanjar.weixin.mp.api.impl; +import java.io.File; + import org.testng.annotations.Guice; import org.testng.annotations.Test; @@ -8,6 +10,7 @@ import com.google.inject.Inject; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.mp.api.ApiTestModule; import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage; +import me.chanjar.weixin.mp.bean.pay.WxEntPayRequest; import me.chanjar.weixin.mp.bean.pay.WxRedpackResult; import me.chanjar.weixin.mp.bean.pay.WxSendRedpackRequest; import me.chanjar.weixin.mp.bean.pay.WxUnifiedOrderRequest; @@ -74,4 +77,16 @@ public class WxMpPayServiceImplTest { .tradeType("JSAPI").openid("122").outTradeNo("111111").build()); System.err.println(result); } + + /** + * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#entPay(WxEntPayRequest, File)}. + * @throws WxErrorException + */ + @Test + public final void testEntPay() throws WxErrorException { + File keyFile = new File("E:\\dlt.p12"); + WxEntPayRequest request = new WxEntPayRequest(); + System.err.println(this.wxService.getPayService().entPay(request, keyFile)); + } + }