@@ -95,6 +95,19 @@ public abstract class WxPayBaseRequest { | |||
@XStreamAlias("sign") | |||
protected String sign; | |||
/** | |||
* <pre> | |||
* 签名类型 | |||
* sign_type | |||
* 否 | |||
* String(32) | |||
* HMAC-SHA256 | |||
* 签名类型,目前支持HMAC-SHA256和MD5 | |||
* </pre> | |||
*/ | |||
@XStreamAlias("sign_type") | |||
private String signType; | |||
/** | |||
* 将单位为元转换为单位为分 | |||
* | |||
@@ -187,6 +200,14 @@ public abstract class WxPayBaseRequest { | |||
this.subMchId = subMchId; | |||
} | |||
public String getSignType() { | |||
return signType; | |||
} | |||
public void setSignType(String signType) { | |||
this.signType = signType; | |||
} | |||
@Override | |||
public String toString() { | |||
return ToStringUtils.toSimpleString(this); | |||
@@ -230,9 +251,8 @@ public abstract class WxPayBaseRequest { | |||
if (StringUtils.isBlank(getNonceStr())) { | |||
this.setNonceStr(String.valueOf(System.currentTimeMillis())); | |||
} | |||
//设置签名字段的值 | |||
this.setSign(SignUtils.createSign(this, config.getMchKey())); | |||
this.setSign(SignUtils.createSign(this, config.getMchKey(), this.signType)); | |||
} | |||
} |
@@ -0,0 +1,112 @@ | |||
package com.github.binarywang.wxpay.bean.request; | |||
import com.github.binarywang.wxpay.exception.WxPayException; | |||
import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
import me.chanjar.weixin.common.annotation.Required; | |||
/** | |||
* <pre> | |||
* 拉取订单评价数据接口的请求参数封装类 | |||
* Created by BinaryWang on 2017/9/2. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
*/ | |||
@XStreamAlias("xml") | |||
public class WxPayQueryCommentRequest extends WxPayBaseRequest { | |||
/** | |||
* <pre> | |||
* 字段名:开始时间 | |||
* 变量名:begin_time | |||
* 是否必填:是 | |||
* 类型:String(19) | |||
* 示例值:20170724000000 | |||
* 描述:按用户评论时间批量拉取的起始时间,格式为yyyyMMddHHmmss | |||
* </pre> | |||
*/ | |||
@Required | |||
@XStreamAlias("begin_time") | |||
private String beginTime; | |||
/** | |||
* <pre> | |||
* 字段名:结束时间 | |||
* 变量名:end_time | |||
* 是否必填:是 | |||
* 类型:String(19) | |||
* 示例值:20170725000000 | |||
* 描述:按用户评论时间批量拉取的结束时间,格式为yyyyMMddHHmmss | |||
* </pre> | |||
*/ | |||
@Required | |||
@XStreamAlias("end_time") | |||
private String endTime; | |||
/** | |||
* <pre> | |||
* 字段名:位移 | |||
* 变量名:offset | |||
* 是否必填:是 | |||
* 类型:uint(64) | |||
* 示例值:0 | |||
* 描述:指定从某条记录的下一条开始返回记录。接口调用成功时,会返回本次查询最后一条数据的offset。商户需要翻页时,应该把本次调用返回的offset 作为下次调用的入参。注意offset是评论数据在微信支付后台保存的索引,未必是连续的 | |||
* </pre> | |||
*/ | |||
@Required | |||
@XStreamAlias("offset") | |||
private Integer offset; | |||
/** | |||
* <pre> | |||
* 字段名:条数 | |||
* 变量名:limit | |||
* 是否必填:否 | |||
* 类型:uint(32) | |||
* 示例值:100 | |||
* 描述:一次拉取的条数, 最大值是200,默认是200 | |||
* </pre> | |||
*/ | |||
@XStreamAlias("limit") | |||
private Integer limit; | |||
/** | |||
* 检查约束情况 | |||
*/ | |||
@Override | |||
protected void checkConstraints() throws WxPayException { | |||
} | |||
public String getBeginTime() { | |||
return beginTime; | |||
} | |||
public void setBeginTime(String beginTime) { | |||
this.beginTime = beginTime; | |||
} | |||
public String getEndTime() { | |||
return endTime; | |||
} | |||
public void setEndTime(String endTime) { | |||
this.endTime = endTime; | |||
} | |||
public Integer getOffset() { | |||
return offset; | |||
} | |||
public void setOffset(Integer offset) { | |||
this.offset = offset; | |||
} | |||
public Integer getLimit() { | |||
return limit; | |||
} | |||
public void setLimit(Integer limit) { | |||
this.limit = limit; | |||
} | |||
} |
@@ -1,5 +1,7 @@ | |||
package com.github.binarywang.wxpay.constant; | |||
import java.text.SimpleDateFormat; | |||
/** | |||
* <pre> | |||
* 微信支付常量类 | |||
@@ -9,6 +11,12 @@ package com.github.binarywang.wxpay.constant; | |||
* @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
*/ | |||
public class WxPayConstants { | |||
/** | |||
* 拉取订单评价数据接口的参数中日期格式 | |||
*/ | |||
public static final SimpleDateFormat QUERY_COMMENT_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); | |||
/** | |||
* 校验用户姓名选项,企业付款时使用 | |||
*/ | |||
@@ -10,6 +10,7 @@ import com.github.binarywang.wxpay.config.WxPayConfig; | |||
import com.github.binarywang.wxpay.exception.WxPayException; | |||
import java.io.File; | |||
import java.util.Date; | |||
import java.util.Map; | |||
/** | |||
@@ -115,8 +116,8 @@ public interface WxPayService { | |||
throws WxPayException; | |||
/** | |||
* @deprecated use WxPayService#parseOrderNotifyResult(String) instead | |||
* @see WxPayService#parseOrderNotifyResult(String) | |||
* @deprecated use WxPayService#parseOrderNotifyResult(String) instead | |||
*/ | |||
@Deprecated | |||
WxPayOrderNotifyResult getOrderNotifyResult(String xmlData) throws WxPayException; | |||
@@ -389,4 +390,23 @@ public interface WxPayService { | |||
* 获取微信请求数据,方便接口调用方获取处理 | |||
*/ | |||
WxPayApiData getWxApiData(); | |||
/** | |||
* <pre> | |||
* 拉取订单评价数据 | |||
* 商户可以通过该接口拉取用户在微信支付交易记录中针对你的支付记录进行的评价内容。商户可结合商户系统逻辑对该内容数据进行存储、分析、展示、客服回访以及其他使用。如商户业务对评价内容有依赖,可主动引导用户进入微信支付交易记录进行评价。 | |||
* 注意: | |||
* 1. 该内容所有权为提供内容的微信用户,商户在使用内容的过程中应遵从用户意愿 | |||
* 2. 一次最多拉取200条评价数据,可根据时间区间分批次拉取 | |||
* 3. 接口只能拉取最近三个月以内的评价数据 | |||
* 接口链接:https://api.mch.weixin.qq.com/billcommentsp/batchquerycomment | |||
* 是否需要证书:需要 | |||
* 文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_17&index=10 | |||
* </pre> | |||
* @param beginDate 开始时间 | |||
* @param endDate 结束时间 | |||
* @param offset 位移 | |||
* @param limit 条数 | |||
*/ | |||
String queryComment(Date beginDate, Date endDate, Integer offset, Integer limit) throws WxPayException; | |||
} |
@@ -23,10 +23,9 @@ import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.io.File; | |||
import java.util.HashMap; | |||
import java.util.LinkedList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.*; | |||
import static com.github.binarywang.wxpay.constant.WxPayConstants.QUERY_COMMENT_DATE_FORMAT; | |||
/** | |||
* <pre> | |||
@@ -237,7 +236,7 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
configMap.put("appid", appId); | |||
payResult = WxPayAppOrderResult.newBuilder() | |||
.sign(SignUtils.createSign(configMap, this.getConfig().getMchKey())) | |||
.sign(SignUtils.createSign(configMap, this.getConfig().getMchKey(), null)) | |||
.prepayId(prepayId) | |||
.partnerId(partnerId) | |||
.appId(appId) | |||
@@ -256,7 +255,7 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
.signType(SignType.MD5) | |||
.build(); | |||
((WxPayMpOrderResult) payResult) | |||
.setPaySign(SignUtils.createSign(payResult, this.getConfig().getMchKey())); | |||
.setPaySign(SignUtils.createSign(payResult, this.getConfig().getMchKey(), null)); | |||
break; | |||
} | |||
} | |||
@@ -303,7 +302,7 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
configMap.put("noncestr", nonceStr); | |||
configMap.put("appid", appId); | |||
// 此map用于客户端与微信服务器交互 | |||
payInfo.put("sign", SignUtils.createSign(configMap, this.getConfig().getMchKey())); | |||
payInfo.put("sign", SignUtils.createSign(configMap, this.getConfig().getMchKey(), null)); | |||
payInfo.put("prepayId", prepayId); | |||
payInfo.put("partnerId", partnerId); | |||
payInfo.put("appId", appId); | |||
@@ -317,7 +316,7 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
payInfo.put("nonceStr", nonceStr); | |||
payInfo.put("package", "prepay_id=" + prepayId); | |||
payInfo.put("signType", SignType.MD5); | |||
payInfo.put("paySign", SignUtils.createSign(payInfo, this.getConfig().getMchKey())); | |||
payInfo.put("paySign", SignUtils.createSign(payInfo, this.getConfig().getMchKey(), null)); | |||
} | |||
return payInfo; | |||
@@ -364,7 +363,7 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
params.put("time_stamp", String.valueOf(System.currentTimeMillis() / 1000));//这里需要秒,10位数字 | |||
params.put("nonce_str", String.valueOf(System.currentTimeMillis())); | |||
String sign = SignUtils.createSign(params, this.getConfig().getMchKey()); | |||
String sign = SignUtils.createSign(params, this.getConfig().getMchKey(), null); | |||
params.put("sign", sign); | |||
for (String key : params.keySet()) { | |||
@@ -411,15 +410,13 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
String url = this.getPayBaseUrl() + "/pay/downloadbill"; | |||
String responseContent = this.post(url, request.toXML(), false); | |||
if (responseContent.startsWith("<")) { | |||
WxPayCommonResult result = WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class); | |||
result.checkResult(this); | |||
return null; | |||
throw WxPayException.from(WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class)); | |||
} else { | |||
return billInformationDeal(responseContent); | |||
return this.handleBillInformation(responseContent); | |||
} | |||
} | |||
private WxPayBillResult billInformationDeal(String responseContent) { | |||
private WxPayBillResult handleBillInformation(String responseContent) { | |||
WxPayBillResult wxPayBillResult = new WxPayBillResult(); | |||
String listStr = ""; | |||
@@ -597,4 +594,25 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService { | |||
wxApiData.remove(); | |||
} | |||
} | |||
@Override | |||
public String queryComment(Date beginDate, Date endDate, Integer offset, Integer limit) throws WxPayException { | |||
WxPayQueryCommentRequest request = new WxPayQueryCommentRequest(); | |||
request.setBeginTime(QUERY_COMMENT_DATE_FORMAT.format(beginDate)); | |||
request.setEndTime(QUERY_COMMENT_DATE_FORMAT.format(endDate)); | |||
request.setOffset(offset); | |||
request.setLimit(limit); | |||
request.setSignType(SignType.HMAC_SHA256); | |||
request.checkAndSign(this.getConfig()); | |||
String url = this.getPayBaseUrl() + "/billcommentsp/batchquerycomment"; | |||
String responseContent = this.post(url, request.toXML(), true); | |||
if (responseContent.startsWith("<")) { | |||
throw WxPayException.from(WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class)); | |||
} | |||
return responseContent; | |||
} | |||
} |
@@ -1,9 +1,15 @@ | |||
package com.github.binarywang.wxpay.util; | |||
import com.github.binarywang.wxpay.constant.WxPayConstants.SignType; | |||
import me.chanjar.weixin.common.util.BeanUtils; | |||
import org.apache.commons.codec.binary.Hex; | |||
import org.apache.commons.codec.digest.DigestUtils; | |||
import org.apache.commons.lang3.StringUtils; | |||
import javax.crypto.Mac; | |||
import javax.crypto.spec.SecretKeySpec; | |||
import java.security.InvalidKeyException; | |||
import java.security.NoSuchAlgorithmException; | |||
import java.util.Map; | |||
import java.util.SortedMap; | |||
import java.util.TreeMap; | |||
@@ -20,22 +26,24 @@ public class SignUtils { | |||
/** | |||
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3) | |||
* | |||
* @param xmlBean Bean需要标记有XML注解 | |||
* @param signKey 签名Key | |||
* @param xmlBean Bean需要标记有XML注解 | |||
* @param signKey 签名Key | |||
* @param signType 签名类型,如果为空,则默认为MD5 | |||
* @return 签名字符串 | |||
*/ | |||
public static String createSign(Object xmlBean, String signKey) { | |||
return createSign(BeanUtils.xmlBean2Map(xmlBean), signKey); | |||
public static String createSign(Object xmlBean, String signKey, String signType) { | |||
return createSign(BeanUtils.xmlBean2Map(xmlBean), signKey, signType); | |||
} | |||
/** | |||
* 微信公众号支付签名算法(详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3) | |||
* | |||
* @param params 参数信息 | |||
* @param signKey 签名Key | |||
* @param params 参数信息 | |||
* @param signKey 签名Key | |||
* @param signType 签名类型,如果为空,则默认为md5 | |||
* @return 签名字符串 | |||
*/ | |||
public static String createSign(Map<String, String> params, String signKey) { | |||
public static String createSign(Map<String, String> params, String signKey, String signType) { | |||
// if (this.getConfig().useSandbox()) { | |||
// //使用仿真测试环境 | |||
// //TODO 目前测试发现,以下两行代码都会出问题,所以暂不建议使用仿真测试环境 | |||
@@ -48,13 +56,32 @@ public class SignUtils { | |||
StringBuilder toSign = new StringBuilder(); | |||
for (String key : sortedMap.keySet()) { | |||
String value = params.get(key); | |||
if (StringUtils.isNotEmpty(value) && !"sign".equals(key) && !"key".equals(key)) { | |||
if (StringUtils.isNotEmpty(value) | |||
&& !StringUtils.equalsAny(key, "sign", "key", "sign_type")) { | |||
toSign.append(key).append("=").append(value).append("&"); | |||
} | |||
} | |||
toSign.append("key=").append(signKey); | |||
return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); | |||
if (SignType.HMAC_SHA256.equals(signType)) { | |||
return createHMACSha256Sign(toSign.toString(), signKey); | |||
} else { | |||
return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); | |||
} | |||
} | |||
private static String createHMACSha256Sign(String message, String key) { | |||
try { | |||
Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); | |||
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256"); | |||
hmacSHA256.init(secretKeySpec); | |||
byte[] bytes = hmacSHA256.doFinal(message.getBytes()); | |||
return Hex.encodeHexString(bytes).toUpperCase(); | |||
} catch (NoSuchAlgorithmException | InvalidKeyException e) { | |||
e.printStackTrace(); | |||
} | |||
return null; | |||
} | |||
/** | |||
@@ -78,7 +105,7 @@ public class SignUtils { | |||
* @see #checkSign(Map, String) | |||
*/ | |||
public static boolean checkSign(Map<String, String> params, String signKey) { | |||
String sign = createSign(params, signKey); | |||
String sign = createSign(params, signKey, null); | |||
return sign.equals(params.get("sign")); | |||
} | |||
} |
@@ -2,7 +2,6 @@ package com.github.binarywang.wxpay.service.impl; | |||
import com.github.binarywang.utils.qrcode.QrcodeUtils; | |||
import com.github.binarywang.wxpay.bean.coupon.*; | |||
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; | |||
import com.github.binarywang.wxpay.bean.request.*; | |||
import com.github.binarywang.wxpay.bean.result.*; | |||
import com.github.binarywang.wxpay.constant.WxPayConstants; | |||
@@ -16,10 +15,13 @@ import com.github.binarywang.wxpay.testbase.XmlWxPayConfig; | |||
import com.google.inject.Inject; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.testng.annotations.*; | |||
import org.testng.annotations.Guice; | |||
import org.testng.annotations.Test; | |||
import java.nio.file.Files; | |||
import java.nio.file.Path; | |||
import java.util.Calendar; | |||
import java.util.Date; | |||
import java.util.Map; | |||
import static org.testng.Assert.*; | |||
@@ -90,7 +92,7 @@ public class WxPayServiceAbstractImplTest { | |||
@Test | |||
public void testDownloadBill() throws Exception { | |||
WxPayBillResult wxPayBillResult = this.payService.downloadBill("20170101", BillType.ALL, "GZIP", "1111111"); | |||
WxPayBillResult wxPayBillResult = this.payService.downloadBill("20170831", BillType.ALL, null, "1111111"); | |||
//前一天没有账单记录返回null | |||
assertNotNull(wxPayBillResult); | |||
//必填字段为空时,抛出异常 | |||
@@ -313,10 +315,24 @@ public class WxPayServiceAbstractImplTest { | |||
@Test | |||
public void testQueryCouponInfo() throws Exception { | |||
WxPayCouponInfoQueryResult result = this.payService.queryCouponInfo(WxPayCouponInfoQueryRequest.newBuilder() | |||
.openid("onqOjjrXT-776SpHnfexGm1_P7iE") | |||
.openid("ojOQA0y9o-Eb6Aep7uVTdbkJqrP4") | |||
.couponId("11") | |||
.stockId("1121") | |||
.build()); | |||
this.logger.info(result.toString()); | |||
} | |||
/** | |||
* 目前调用接口总报“系统繁忙,清稍后再试”,怀疑根本没法使用 | |||
*/ | |||
@Test | |||
public void testQueryComment() throws Exception { | |||
Calendar calendar = Calendar.getInstance(); | |||
calendar.add(Calendar.DAY_OF_MONTH, -2); | |||
Date beginDate = calendar.getTime(); | |||
calendar.add(Calendar.MONTH, -1); | |||
Date endDate = calendar.getTime(); | |||
this.payService.queryComment(beginDate, endDate, 0, null); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
package com.github.binarywang.wxpay.util; | |||
import com.google.common.base.Splitter; | |||
import org.testng.annotations.Test; | |||
import static com.github.binarywang.wxpay.constant.WxPayConstants.SignType.HMAC_SHA256; | |||
import static org.testng.Assert.assertEquals; | |||
/** | |||
* <pre> | |||
* 测试中使用的测试数据参考的是官方文档,地址: | |||
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3 | |||
* Created by BinaryWang on 2017/9/2. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
*/ | |||
public class SignUtilsTest { | |||
@Test | |||
public void testCreateSign() throws Exception { | |||
String signKey = "192006250b4c09247ec02edce69f6a2d"; | |||
String message = "appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA"; | |||
assertEquals(SignUtils.createSign((Splitter.on("&").withKeyValueSeparator("=").split(message)), signKey, null), | |||
"9A0A8659F005D6984697E2CA0A9CF3B7"); | |||
} | |||
@Test | |||
public void testCreateSign_HMACSHA256() throws Exception { | |||
String signKey = "192006250b4c09247ec02edce69f6a2d"; | |||
final String message = "appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA"; | |||
String sign = SignUtils.createSign(Splitter.on("&").withKeyValueSeparator("=").split(message), | |||
signKey, HMAC_SHA256); | |||
assertEquals(sign, "6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6"); | |||
} | |||
@Test | |||
public void testCheckSign() throws Exception { | |||
} | |||
} |