diff --git a/README.md b/README.md index cef60de..0edd315 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,154 @@ 微信支付 Java SDK ------ -对[微信支付开发者文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)中给出的API进行了封装。 +本项目依托于 [微信支付开发者文档](https://pay.weixin.qq.com/wiki/doc/api/index.html),对文档中的接口进行二次封装,从而为小伙伴们提供一个`拿来即用`的支付sdk工具。 -com.weixin.pay.WXPay类下提供了对应的方法: +## 项目结构 +首先需要简单说明整个 `wxpay-sdk` 的项目结构,主体结构如下所示: + + - wxpay-sdk + - src + - main + - java + - com.weixin.pay + - card // 微信卡券 + - constants // 常量文件 + - redis // redis工具类 + - util // 支付工具类(支付、签名、加密解密) + - xxx class // 支付实体类,基础配置信息 + - test + - controller + - xxx class // 测试的相关类 + - .gitignore + - pom.xml // 引用包 + - README.md + -|方法名 | 说明 | -|--------|--------| -|microPay| 刷卡支付 | -|unifiedOrder | 统一下单| -|orderQuery | 查询订单 | -|reverse | 撤销订单 | -|closeOrder|关闭订单| -|refund|申请退款| -|refundQuery|查询退款| -|downloadBill|下载对账单| -|report|交易保障| -|shortUrl|转换短链接| -|authCodeToOpenid|授权码查询openid| -* 参数为`Map`对象,返回类型也是`Map`。 -* 方法内部会将参数会转换成含有`appid`、`mch_id`、`nonce_str`、`sign\_type`和`sign`的XML; -* 可选HMAC-SHA256算法和MD5算法签名; -* 通过HTTPS请求得到返回数据后会对其做必要的处理(例如验证签名,签名错误则抛出异常)。 -* 对于downloadBill,无论是否成功都返回Map,且都含有`return_code`和`return_msg`。若成功,其中`return_code`为`SUCCESS`,另外`data`对应对账单数据。 +提供微信支付的基础功能,脱胎于微信官方Java-SDK,进行二次封装后,提供一系列的方法; +基础方法主要在 `com.weixin.pay.WXPay` 类下,此项目包含的微信支付功能主要分为以下几个部分: + +### 1. 基础支付功能 + + `com.weixin.pay.WXPay` : + + |方法名 | 说明 | + |--------|--------| + |microPay| 刷卡支付 | + |unifiedOrder | 统一下单| + |chooseWXPayMap | 微信支付二次签名| + |orderQuery | 查询订单 | + |reverse | 撤销订单 | + |closeOrder|关闭订单| + |refund|申请退款| + |refundQuery|查询退款| + |downloadBill|下载对账单| + |report|交易保障| + |shortUrl|转换短链接| + |authCodeToOpenid|授权码查询openid| + + + +### 2. 验收用例 + +支付验收指引:`https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1` + + `controller.TestWXPay` : + + |方法名 | 说明 | + |--------|--------| + |unifiedOrder | 统一下单| + |orderQuery | 查询订单 | + |reverse | 撤销订单 | + |closeOrder|关闭订单| + |refund|申请退款| + |refundQuery|查询退款| + +### 3. 商户平台-现金红包 + + `com.weixin.pay.WXPay` : + + |方法名 | 说明 | + |--------|--------| + |sendRedPack| 企业向指定微信用户的openid发放指定金额红包 | + |getRedPackInfo| 查询红包记录 | + +### 4. 商户平台-代金券或立减优惠 + + `com.weixin.pay.WXPay` : + + |方法名 | 说明 | + |--------|--------| + |sendCoupon| 发放代金券 | + |queryCouponsInfo| 查询代金券信息 | + |queryCouponStock| 查询代金券批次 | + +### 5. 公众平台-微信卡券 + + `com.weixin.pay.util.WXUtils` : + + |方法名 | 说明 | + |--------|--------| + |getAccessToken| 获取微信全局accessToken | + |getMiniBaseUserInfo| 获取小程序静默登录返回信息 | + |getJsapiAccessTokenByCode| 网页授权获取用户信息时用于获取access_token以及openid | + |getJsapiUserinfo| 通过access_token和openid请求获取用户信息 | + |getWxMiniQRImg| 生成带参数的小程序二维码[] | + |getWxCardApiTicket| 获取卡券 api_ticket 的 api | + |getWxApiTicket| 获取卡券 api_ticket 的 api | + +### 6. 公众平台-社交立减金活动 + + `com.weixin.pay.util.WXUtils` : + + |方法名 | 说明 | + |--------|--------| + |getCardList| 根据代金券批次ID得到组合的cardList | + |createCardActivity| 创建支付后领取立减金活动接口 | + + +## 微信支付调用示例 + +```$xslt +public Map saveWxPayUnifiedOrder(Payment payment, User user) throws Exception { + if (payment == null) { + return null; + } + if (user == null) { + return null; + } + + // 1.调用微信统一下单接口 + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + Map resultMap = wxPay.unifiedOrder(...); + + // 1.1.记录付款流水 + ... + + // 下单失败,进行处理 + if (WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RETURN_CODE)) || + WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RESULT_CODE))) { + + // 处理结果返回,无需继续执行 + resultMap.put(WXPayConstants.RESULT_CODE, WXPayConstants.FAIL); + resultMap.put(WXPayConstants.ERR_CODE_DES, resultMap.get(WXPayConstants.RETURN_MSG)); + return resultMap; + } + + // 1.2.获取prepay_id、nonce_str + String prepay_id = resultMap.get("prepay_id"); + String nonce_str = resultMap.get("nonce_str"); + + // 2.根据微信统一下单接口返回数据组装微信支付参数,返回结果 + return wxPay.chooseWXPayMap(prepay_id, nonce_str); +} +``` + + +基础调用方式如上所述,统一返回值为 `Map`,详细信息见实体类,文档会实时更新,尽情期待!!! + + ## License diff --git a/src/main/java/com/weixin/pay/WXPay.java b/src/main/java/com/weixin/pay/WXPay.java index 9f5c305..bd396b2 100755 --- a/src/main/java/com/weixin/pay/WXPay.java +++ b/src/main/java/com/weixin/pay/WXPay.java @@ -1,19 +1,16 @@ package com.weixin.pay; -import com.weixin.pay.util.WXPayUtil; import com.weixin.pay.constants.WXPayConstants; import com.weixin.pay.constants.WXPayConstants.SignType; +import com.weixin.pay.util.DateTimeUtil; +import com.weixin.pay.util.WXPayUtil; +import org.apache.commons.lang3.StringUtils; import java.math.BigDecimal; +import java.util.Date; import java.util.HashMap; import java.util.Map; -/** - * 微信支付对象,使用此对象操作微信支付方法 - * - * @author yclimb - * @date 2018/8/17 - */ public class WXPay { private WXPayConfig config; @@ -34,7 +31,7 @@ public class WXPay { } - public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception{ + public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception { this(config, null, autoReport, useSandbox); } @@ -53,8 +50,7 @@ public class WXPay { this.useSandbox = useSandbox; if (useSandbox) { this.signType = SignType.MD5; // 沙箱环境 - } - else { + } else { this.signType = SignType.MD5; // 此处原来不是MD5!!! } this.wxPayRequest = new WXPayRequest(config); @@ -73,7 +69,7 @@ public class WXPay { if (this.config.getCertStream() == null) { throw new Exception("cert stream in config is empty"); } - if (this.config.getWXPayDomain() == null){ + if (this.config.getWXPayDomain() == null) { throw new Exception("config.getWXPayDomain() is null"); } @@ -90,9 +86,9 @@ public class WXPay { * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign
* 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口 * - * @param reqData - * @return - * @throws Exception + * @param reqData r + * @return map + * @throws Exception e */ public Map fillRequestData(Map reqData) throws Exception { reqData.put("appid", config.getAppID()); @@ -100,20 +96,35 @@ public class WXPay { reqData.put("nonce_str", WXPayUtil.generateNonceStr()); if (SignType.MD5.equals(this.signType)) { reqData.put("sign_type", WXPayConstants.MD5); - } - else if (SignType.HMACSHA256.equals(this.signType)) { + } else if (SignType.HMACSHA256.equals(this.signType)) { reqData.put("sign_type", WXPayConstants.HMACSHA256); } reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); return reqData; } + /** + * 向 Map 中添加 appid、mch_id、nonce_str、sign
+ * 该函数适用于商户适用于适用于红包查询接口,不适用于统一下单接口 + * + * @param reqData r + * @return map + * @throws Exception e + */ + public Map fillRequestDataNotType(Map reqData) throws Exception { + reqData.put("appid", config.getAppID()); + reqData.put("mch_id", config.getMchID()); + reqData.put("nonce_str", WXPayUtil.generateNonceStr()); + reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); + return reqData; + } + /** * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。 * * @param reqData 向wxpay post的请求数据 * @return 签名是否有效 - * @throws Exception + * @throws Exception e */ public boolean isResponseSignatureValid(Map reqData) throws Exception { // 返回数据的签名方式和请求中给定的签名方式是一致的 @@ -132,19 +143,15 @@ public class WXPay { SignType signType; if (signTypeInData == null) { signType = SignType.MD5; - } - else { + } else { signTypeInData = signTypeInData.trim(); if (signTypeInData.length() == 0) { signType = SignType.MD5; - } - else if (WXPayConstants.MD5.equals(signTypeInData)) { + } else if (WXPayConstants.MD5.equals(signTypeInData)) { signType = SignType.MD5; - } - else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) { + } else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) { signType = SignType.HMACSHA256; - } - else { + } else { throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData)); } } @@ -154,10 +161,11 @@ public class WXPay { /** * 不需要证书的请求 - * @param urlSuffix String - * @param reqData 向wxpay post的请求数据 + * + * @param urlSuffix String + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 超时时间,单位是毫秒 - * @param readTimeoutMs 超时时间,单位是毫秒 + * @param readTimeoutMs 超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -173,16 +181,17 @@ public class WXPay { /** * 需要证书的请求 - * @param urlSuffix String - * @param reqData 向wxpay post的请求数据 Map + * + * @param urlSuffix String + * @param reqData 向wxpay post的请求数据 Map * @param connectTimeoutMs 超时时间,单位是毫秒 - * @param readTimeoutMs 超时时间,单位是毫秒 + * @param readTimeoutMs 超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ public String requestWithCert(String urlSuffix, Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { - String msgUUID= reqData.get("nonce_str"); + String msgUUID = reqData.get("nonce_str"); String reqBody = WXPayUtil.mapToXml(reqData); String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport); @@ -191,6 +200,7 @@ public class WXPay { /** * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 + * * @param xmlStr API返回的XML格式数据 * @return Map类型数据 * @throws Exception e @@ -201,6 +211,7 @@ public class WXPay { /** * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 + * * @param xmlStr API返回的XML格式数据 * @param isFlag 是否对返回的数据进行sign校验 * @return Map类型数据 @@ -212,27 +223,47 @@ public class WXPay { Map respData = WXPayUtil.xmlToMap(xmlStr); if (respData.containsKey(RETURN_CODE)) { return_code = respData.get(RETURN_CODE); - } - else { + } else { throw new Exception(String.format("No `return_code` in XML: %s", xmlStr)); } if (return_code.equals(WXPayConstants.FAIL)) { return respData; - } - else if (return_code.equals(WXPayConstants.SUCCESS)) { + } else if (return_code.equals(WXPayConstants.SUCCESS)) { // 如果isFlag为false,则不需要进行sign校验 if (!isFlag) { return respData; } if (this.isResponseSignatureValid(respData)) { return respData; - } - else { + } else { throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr)); } + } else { + throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); } - else { + } + + /** + * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 + * + * @param xmlStr API返回的XML格式数据 + * @return Map类型数据 + * @throws Exception + */ + public Map sendRedPackProcessResponseXml(String xmlStr) throws Exception { + String RETURN_CODE = "return_code"; + String return_code; + Map respData = WXPayUtil.xmlToMap(xmlStr); + if (respData.containsKey(RETURN_CODE)) { + return_code = respData.get(RETURN_CODE); + } else { + throw new Exception(String.format("No `return_code` in XML: %s", xmlStr)); + } + + if (StringUtils.isNotBlank(return_code)) { + return respData; + } else { throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); } } @@ -240,6 +271,7 @@ public class WXPay { /** * 作用:提交刷卡支付
* 场景:刷卡支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -252,9 +284,10 @@ public class WXPay { /** * 作用:提交刷卡支付
* 场景:刷卡支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -262,8 +295,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.MICROPAY_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -273,6 +305,7 @@ public class WXPay { /** * 提交刷卡支付,针对软POS,尽可能做成功 * 内置重试机制,最多60s + * * @param reqData * @return * @throws Exception @@ -284,13 +317,14 @@ public class WXPay { /** * 提交刷卡支付,针对软POS,尽可能做成功 * 内置重试机制,最多60s + * * @param reqData * @param connectTimeoutMs * @return * @throws Exception */ public Map microPayWithPos(Map reqData, int connectTimeoutMs) throws Exception { - int remainingTimeMs = 60*1000; + int remainingTimeMs = 60 * 1000; long startTimestampMs = 0; Map lastResult = null; Exception lastException = null; @@ -307,48 +341,40 @@ public class WXPay { String errCode = lastResult.get("err_code"); if (resultCode.equals("SUCCESS")) { break; - } - else { + } else { // 看错误码,若支付结果未知,则重试提交刷卡支付 if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) { - remainingTimeMs = remainingTimeMs - (int)(WXPayUtil.getCurrentTimestampMs() - startTimestampMs); + remainingTimeMs = remainingTimeMs - (int) (WXPayUtil.getCurrentTimestampMs() - startTimestampMs); if (remainingTimeMs <= 100) { break; - } - else { + } else { WXPayUtil.getLogger().info("microPayWithPos: try micropay again"); - if (remainingTimeMs > 5*1000) { - Thread.sleep(5*1000); - } - else { - Thread.sleep(1*1000); + if (remainingTimeMs > 5 * 1000) { + Thread.sleep(5 * 1000); + } else { + Thread.sleep(1 * 1000); } continue; } - } - else { + } else { break; } } - } - else { + } else { break; } - } - catch (Exception ex) { + } catch (Exception ex) { lastResult = null; lastException = ex; } - } - else { + } else { break; } } if (lastResult == null) { throw lastException; - } - else { + } else { return lastResult; } } @@ -357,6 +383,7 @@ public class WXPay { * 作用:商户平台-现金红包-发放普通红包
* 场景:现金红包发放后会以公众号消息的形式触达用户 * 其他:需要证书 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception e @@ -365,12 +392,11 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_SENDREDPACK_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.SENDREDPACK_URL_SUFFIX; } String respXml = this.requestWithCert(url, this.redPackRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); - return this.processResponseXml(respXml); + return this.sendRedPackProcessResponseXml(respXml); } public Map redPackRequestData(Map reqData) throws Exception { @@ -381,10 +407,90 @@ public class WXPay { return reqData; } + /** + * 作用:商户平台-现金红包-查询红包记录
+ * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 + * 其他:需要证书 + * + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception e + */ + public Map getRedPackInfo(Map reqData) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_GETHBINFO_URL_SUFFIX; + } else { + url = WXPayConstants.GETHBINFO_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + return this.processResponseXml(respXml); + } + + /** + * 作用:商户平台-代金券或立减优惠-发放代金券
+ * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 + * 注意:通过接口发放的代金券不会进入微信卡包 + * 其他:需要证书 + * + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception e + */ + public Map sendCoupon(Map reqData) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_SEND_COUPON_URL_SUFFIX; + } else { + url = WXPayConstants.SEND_COUPON_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + return this.processResponseXml(respXml, false); + } + + /** + * 作用:商户平台-代金券或立减优惠-查询代金券信息
+ * 场景:查询代金券信息 + * + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception e + */ + public Map queryCouponsInfo(Map reqData) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_QUERYCOUPONSINFO_URL_SUFFIX; + } else { + url = WXPayConstants.QUERYCOUPONSINFO_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + return this.processResponseXml(respXml, false); + } + + /** + * 作用:商户平台-代金券或立减优惠-查询代金券批次
+ * 场景:查询代金券批次信息 + * + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception e + */ + public Map queryCouponStock(Map reqData) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_QUERY_COUPON_STOCK_URL_SUFFIX; + } else { + url = WXPayConstants.QUERY_COUPON_STOCK_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + return this.processResponseXml(respXml, false); + } + /** * 作用:商户平台-企业付款-企业向微信用户个人付款
* 场景:企业付款到零钱资金使用商户号余额资金。 * 其他:需要证书 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception e @@ -393,19 +499,18 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_TRANSFERS_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.TRANSFERS_URL_SUFFIX; } String respXml = this.requestWithCert(url, this.transfersRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); - return this.processResponseXml(respXml, false); + return this.sendRedPackProcessResponseXml(respXml); } public Map transfersRequestData(Map reqData) throws Exception { - reqData.put("mch_appid", WXPayConstants.APP_ID_XXX); - reqData.put("mchid", WXPayConstants.MCH_ID_XXX); + reqData.put("mch_appid", config.getAppID()); + reqData.put("mchid", config.getMchID()); reqData.put("nonce_str", WXPayUtil.generateUUID()); - reqData.put("sign", WXPayUtil.generateSignature(reqData, WXPayConstants.API_KEY_XXX, this.signType)); + reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); return reqData; } @@ -413,6 +518,7 @@ public class WXPay { /** * 作用:统一下单
* 场景:公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -425,21 +531,21 @@ public class WXPay { /** * 作用:统一下单
* 场景:公共号支付、扫码支付、APP支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ - public Map unifiedOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + public Map unifiedOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX; } - if(this.notifyUrl != null) { + if (this.notifyUrl != null) { reqData.put("notify_url", this.notifyUrl); } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -450,6 +556,7 @@ public class WXPay { /** * 作用:查询订单
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -462,9 +569,10 @@ public class WXPay { /** * 作用:查询订单
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 - * @param reqData 向wxpay post的请求数据 int + * + * @param reqData 向wxpay post的请求数据 int * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -472,8 +580,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.ORDERQUERY_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -484,6 +591,7 @@ public class WXPay { /** * 作用:撤销订单
* 场景:刷卡支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -497,9 +605,10 @@ public class WXPay { * 作用:撤销订单
* 场景:刷卡支付
* 其他:需要证书 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -507,8 +616,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.REVERSE_URL_SUFFIX; } String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -519,6 +627,7 @@ public class WXPay { /** * 作用:关闭订单
* 场景:公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -531,18 +640,18 @@ public class WXPay { /** * 作用:关闭订单
* 场景:公共号支付、扫码支付、APP支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ - public Map closeOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + public Map closeOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.CLOSEORDER_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -553,6 +662,7 @@ public class WXPay { /** * 作用:申请退款
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -566,9 +676,10 @@ public class WXPay { * 作用:申请退款
* 场景:刷卡支付、公共号支付、扫码支付、APP支付
* 其他:需要证书 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -576,8 +687,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.REFUND_URL_SUFFIX; } String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -588,6 +698,7 @@ public class WXPay { /** * 作用:退款查询
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -600,9 +711,10 @@ public class WXPay { /** * 作用:退款查询
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -610,8 +722,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.REFUNDQUERY_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -622,6 +733,7 @@ public class WXPay { /** * 作用:对账单下载(成功时返回对账单数据,失败时返回XML格式数据)
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -635,10 +747,11 @@ public class WXPay { * 作用:对账单下载
* 场景:刷卡支付、公共号支付、扫码支付、APP支付
* 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data, - * 其中return_code为`SUCCESS`,data为对账单数据。 - * @param reqData 向wxpay post的请求数据 + * 其中return_code为`SUCCESS`,data为对账单数据。 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return 经过封装的API返回数据 * @throws Exception */ @@ -646,8 +759,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX; } String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim(); @@ -655,8 +767,7 @@ public class WXPay { // 出现错误,返回XML数据 if (respStr.indexOf("<") == 0) { ret = WXPayUtil.xmlToMap(respStr); - } - else { + } else { // 正常返回csv数据 ret = new HashMap(); ret.put("return_code", WXPayConstants.SUCCESS); @@ -670,6 +781,7 @@ public class WXPay { /** * 作用:交易保障
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -682,9 +794,10 @@ public class WXPay { /** * 作用:交易保障
* 场景:刷卡支付、公共号支付、扫码支付、APP支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -692,8 +805,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.REPORT_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -704,6 +816,7 @@ public class WXPay { /** * 作用:转换短链接
* 场景:刷卡支付、扫码支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -716,6 +829,7 @@ public class WXPay { /** * 作用:转换短链接
* 场景:刷卡支付、扫码支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -724,8 +838,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.SHORTURL_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -736,6 +849,7 @@ public class WXPay { /** * 作用:授权码查询OPENID接口
* 场景:刷卡支付 + * * @param reqData 向wxpay post的请求数据 * @return API返回数据 * @throws Exception @@ -748,9 +862,10 @@ public class WXPay { /** * 作用:授权码查询OPENID接口
* 场景:刷卡支付 - * @param reqData 向wxpay post的请求数据 + * + * @param reqData 向wxpay post的请求数据 * @param connectTimeoutMs 连接超时时间,单位是毫秒 - * @param readTimeoutMs 读超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 * @return API返回数据 * @throws Exception */ @@ -758,8 +873,7 @@ public class WXPay { String url; if (this.useSandbox) { url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX; - } - else { + } else { url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX; } String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); @@ -773,22 +887,25 @@ public class WXPay { * 是否需要证书:否 * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 * - * @param notify_url 公众号用户openid - * @param body 商品简单描述,该字段请按照规范传递,例:腾讯充值中心-QQ会员充值 - * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 - * @param total_fee 订单总金额,传入参数单位为:元 + * @param notify_url 公众号用户openid + * @param body 商品简单描述,该字段请按照规范传递,例:腾讯充值中心-QQ会员充值 + * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 + * @param total_fee 订单总金额,传入参数单位为:元 * @param spbill_create_ip APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP + * @param goods_tag 订单优惠标记,用于区分订单是否可以享受优惠 + * @param detail 商品详情 ,单品优惠活动该字段必传 + * @param time_start 订单生成时间,格式为yyyyMMddHHmmss + * @param time_expire 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010 * @return API返回数据 * @throws Exception e */ - public Map unifiedOrder(String notify_url, String openid, String body, String out_trade_no, String total_fee, String spbill_create_ip) throws Exception { + public Map unifiedOrder(String notify_url, String openid, String body, String out_trade_no, String total_fee, + String spbill_create_ip, String goods_tag, String detail, + Date time_start, Date time_expire) throws Exception { /** 构造请求参数数据 **/ Map data = new HashMap<>(); - // 微信支付对象 - WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); - // 字段名 变量名 必填 类型 示例值 描述 // 标价币种 fee_type 否 String(16) CNY 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型 data.put("fee_type", WXPayConstants.FEE_TYPE_CNY); @@ -809,20 +926,27 @@ public class WXPay { data.put("spbill_create_ip", spbill_create_ip); /** 以下参数为非必填参数 **/ - /*// 交易起始时间 time_start 否 String(14) 20091225091010 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 - data.put("time_start", null); - // 交易结束时间 time_expire 否 String(14) 20091227091010 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则,建议:最短失效时间间隔大于1分钟 - data.put("time_expire", null); - // 订单优惠标记 goods_tag 否 String(32) WXG 订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠 - data.put("goods_tag", null); - // 商品ID product_id 否 String(32) 12235413214070356458058 trade_type=NATIVE时(即扫码支付),此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 + // 订单优惠标记 goods_tag 否 String(32) WXG 订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠 + if (StringUtils.isNotBlank(goods_tag)) { + data.put("goods_tag", goods_tag); + } + // 商品详情 detail 否 String(6000) 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明” + if (StringUtils.isNotBlank(detail)) { + data.put("detail", detail); + // 接口版本号 新增字段,接口版本号,区分原接口,默认填写1.0。入参新增version后,则支付通知接口也将返回单品优惠信息字段promotion_detail,请确保支付通知的签名验证能通过。 + data.put("version", "1.0"); + } + // 设备号 device_info 否 String(32) 013467007045764 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" + data.put("device_info", "WEB"); + + // 交易起始时间 time_start 否 String(14) 20091225091010 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 + data.put("time_start", DateTimeUtil.getTimeShortString(time_start)); + // 交易结束时间 time_expire 否 String(14) 20091227091010 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则,建议:最短失效时间间隔大于1分钟 + data.put("time_expire", DateTimeUtil.getTimeShortString(time_expire)); + /*// 商品ID product_id 否 String(32) 12235413214070356458058 trade_type=NATIVE时(即扫码支付),此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 data.put("product_id", null); // 指定支付方式 limit_pay 否 String(32) no_credit 上传此参数no_credit--可限制用户不能使用信用卡支付 data.put("limit_pay", null); - // 设备号 device_info 否 String(32) 013467007045764 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" - data.put("device_info", null); - // 商品详情 detail 否 String(6000) 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明” - data.put("detail", null); // 附加数据 attach 否 String(127) 深圳分店 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 data.put("attach", null);*/ @@ -839,7 +963,7 @@ public class WXPay { data.put("sign", sign);*/ // 微信统一下单接口请求地址 - Map resultMap = wxPay.unifiedOrder(data); + Map resultMap = this.unifiedOrder(data); WXPayUtil.getLogger().info("wxPay.unifiedOrder:" + resultMap); @@ -860,7 +984,7 @@ public class WXPay { // 支付方法调用所需参数map Map chooseWXPayMap = new HashMap<>(); - chooseWXPayMap.put("appId", WXPayConstants.APP_ID); + chooseWXPayMap.put("appId", config.getAppID()); chooseWXPayMap.put("timeStamp", String.valueOf(WXPayUtil.getCurrentTimestamp())); chooseWXPayMap.put("nonceStr", nonce_str); chooseWXPayMap.put("package", "prepay_id=" + prepay_id); @@ -869,7 +993,7 @@ public class WXPay { WXPayUtil.getLogger().info("wxPay.chooseWXPayMap:" + chooseWXPayMap.toString()); // 生成支付签名 - String paySign = WXPayUtil.generateSignature(chooseWXPayMap, WXPayConstants.API_KEY); + String paySign = WXPayUtil.generateSignature(chooseWXPayMap, config.getKey()); chooseWXPayMap.put("paySign", paySign); WXPayUtil.getLogger().info("wxPay.paySign:" + paySign); @@ -880,21 +1004,21 @@ public class WXPay { /** * 作用:申请退款
* 场景:当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家, - * 微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 + * 微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4 * - * @param notify_url 回调地址 + * @param notify_url 回调地址 * @param transaction_id 微信生成的订单号,在支付通知中有返回 - * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 - * @param out_refund_no 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 - * @param total_fee 订单总金额,传入参数单位为:元 - * @param refund_fee 退款总金额,订单总金额,传入参数单位为:元 - * @param refund_desc 退款原因,若商户传入,会在下发给用户的退款消息中体现退款原因 + * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 + * @param out_refund_no 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 + * @param total_fee 订单总金额,传入参数单位为:元 + * @param refund_fee 退款总金额,订单总金额,传入参数单位为:元 + * @param refund_desc 退款原因,若商户传入,会在下发给用户的退款消息中体现退款原因 * @return API返回数据 * @throws Exception e */ public Map refund(String notify_url, String transaction_id, String out_trade_no, String out_refund_no, - String total_fee, String refund_fee, String refund_desc) throws Exception { + String total_fee, String refund_fee, String refund_desc) throws Exception { /** 构造请求参数数据 **/ Map data = new HashMap<>(); @@ -937,11 +1061,8 @@ public class WXPay { // 签名 sign 是 String(32) C380BEC2BFD727A4B6845133519F3AD6 通过签名算法计算得出的签名值,详见签名生成算法 data.put("sign", sign);*/ - // 微信支付对象 - WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); - // 微信退款接口 - Map resultMap = wxPay.refund(data); + Map resultMap = this.refund(data); WXPayUtil.getLogger().info("wxPay.refund:" + resultMap); @@ -954,9 +1075,9 @@ public class WXPay { * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 * * @param partner_trade_no 商户订单号 - * @param openid 用户openid - * @param amount 企业付款金额 - * @param desc 企业付款描述信息 + * @param openid 用户openid + * @param amount 企业付款金额 + * @param desc 企业付款描述信息 * @param spbill_create_ip 该IP可传用户端或者服务端的IP * @return API返回数据 * @throws Exception e @@ -986,15 +1107,249 @@ public class WXPay { // 收款用户姓名 re_user_name 可选 王小王 String 收款用户真实姓名。(如果check_name设置为FORCE_CHECK,则必填用户真实姓名) data.put("re_user_name", "xxx");*/ - // 微信支付对象 - WXPay wxPay = new WXPay(XxxWXPayConfigImpl.getInstance()); - // 微信调用接口 - Map resultMap = wxPay.transfers(data); + Map resultMap = this.transfers(data); WXPayUtil.getLogger().info("wxPay.transfers:" + resultMap); return resultMap; } + /** + * 作用:企业向指定微信用户的openid发放指定金额红包
+ * 场景:商户可以通过本平台向微信支付用户发放现金红包。用户领取红包后,资金到达用户微信支付零钱账户,和零钱包的其他资金有一样的使用出口;若用户未领取,资金将会在24小时后退回商户的微信支付账户中。 + * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3 + * + * @param mch_billno 商户订单号 + * @param openid 用户openid + * @param amount 企业付款金额 + * @param act_name 活动名称 + * @param wishing 红包祝福语 + * @param remark 备注 + * @param spbill_create_ip 该IP可传用户端或者服务端的IP + * @return API返回数据 + * @throws Exception e + */ + public Map sendRedPack(String mch_billno, String openid, String amount, String act_name, String wishing, String remark, String spbill_create_ip) throws Exception { + + /** 构造请求参数数据 **/ + Map data = new HashMap<>(); + + // 商户订单号 mch_billno 是 10000098201411111234567890 String(28) 商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z)接口根据商户订单号支持重入,如出现超时可再调用。 + data.put("mch_billno", mch_billno); + // 商户名称 send_name 是 天虹百货 String(32) 红包发送者名称 + data.put("send_name", "悦店"); + // 用户openid re_openid 是 oxTWIuGaIt6gTKsQRLau2M0yL16E String(32) 接受红包的用户openid openid为用户在wxappid下的唯一标识(获取openid参见微信公众平台开发者文档:网页授权获取用户基本信息) + data.put("re_openid", openid); + // 付款金额 total_amount 是 1000 int 付款金额,单位分 + data.put("total_amount", String.valueOf(new BigDecimal(amount).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); + // 红包发放总人数 total_num 是 1 int 红包发放总人数 total_num=1 + data.put("total_num", "1"); + // 红包祝福语 wishing 是 感谢您参加猜灯谜活动,祝您元宵节快乐! String(128) 红包祝福语 + data.put("wishing", wishing); + // Ip地址 client_ip 是 192.168.0.1 String(15) 调用接口的机器Ip地址 + data.put("client_ip", spbill_create_ip); + // 活动名称 act_name 是 猜灯谜抢红包活动 String(32) 活动名称 + data.put("act_name", act_name); + // 备注 remark 是 猜越多得越多,快来抢! String(256) 备注信息 + data.put("remark", remark); + + /** 以下参数为非必填参数 **/ + /* + * 场景id:scene_id 否 PRODUCT_8 String(32) 发放红包使用场景,红包金额大于200或者小于1元时必传 + * PRODUCT_1:商品促销 + * PRODUCT_2:抽奖 + * PRODUCT_3:虚拟物品兑奖 + * PRODUCT_4:企业内部福利 + * PRODUCT_5:渠道分润 + * PRODUCT_6:保险回馈 + * PRODUCT_7:彩票派奖 + * PRODUCT_8:税务刮奖 + */ + //data.put("scene_id", "PRODUCT_1"); + /* + * 活动信息 risk_info 否 posttime%3d123123412%26clientversion%3d234134%26mobile%3d122344545%26deviceid%3dIOS String(128) + * posttime:用户操作的时间戳 + * mobile:业务系统账号的手机号,国家代码-手机号。不需要+号 + * deviceid :mac 地址或者设备唯一标识 + * clientversion :用户操作的客户端版本 把值为非空的信息用key=value进行拼接,再进行urlencode urlencode(posttime=xx& mobile =xx&deviceid=xx) + */ + // 资金授权商户号 consume_mch_id 否 1222000096 String(32) 资金授权商户号 服务商替特约商户发放时使用 + + /** 以下四个参数,在 this.redPackRequestData 方法中会自动赋值 **/ + // 商户号 mch_id 是 10000098 String(32) 微信支付分配的商户号 + // 随机字符串 nonce_str 是 5K8264ILTKCH16CQ2502SI8ZNMTM67VS String(32) 随机字符串,不长于32位 + // 签名 sign 是 C380BEC2BFD727A4B6845133519F3AD6 String(32) 详见签名生成算法 + // 公众账号appid wxappid 是 wx8888888888888888 String(32) 微信分配的公众账号ID(企业号corpid即为此appId)。在微信开放平台(open.weixin.qq.com)申请的移动应用appid无法使用该接口。 + + // 微信调用接口 + Map resultMap = this.sendRedPack(data); + + WXPayUtil.getLogger().info("wxPay.sendRedPack:" + resultMap); + + return resultMap; + } + + /** + * 作用:查询红包记录
+ * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 + * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_6&index=5 + * + * @param mch_billno 商户订单号 + * @return API返回数据 + * @throws Exception e + */ + public Map getRedPackInfo(String mch_billno) throws Exception { + + /** 构造请求参数数据 **/ + Map data = new HashMap<>(); + + // 商户订单号 mch_billno 是 10000098201411111234567890 String(28) 商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z)接口根据商户订单号支持重入,如出现超时可再调用。 + data.put("mch_billno", mch_billno); + // 订单类型 bill_type 是 MCHT String(32) MCHT:通过商户订单号获取红包信息。 + data.put("bill_type", "MCHT"); + + /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ + // 商户号 mch_id 是 10000098 String(32) 微信支付分配的商户号 + // 随机字符串 nonce_str 是 5K8264ILTKCH16CQ2502SI8ZNMTM67VS String(32) 随机字符串,不长于32位 + // 签名 sign 是 C380BEC2BFD727A4B6845133519F3AD6 String(32) 详见签名生成算法 + // 公众账号appid appid 是 wx8888888888888888 String(32) 微信分配的公众账号ID(企业号corpid即为此appId)。在微信开放平台(open.weixin.qq.com)申请的移动应用appid无法使用该接口。 + + // 微信调用接口 + Map resultMap = this.getRedPackInfo(data); + + WXPayUtil.getLogger().info("wxPay.getRedPackInfo:" + resultMap); + + return resultMap; + } + + /** + * 作用:商户平台-代金券或立减优惠-发放代金券
+ * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 + * 注意:通过接口发放的代金券不会进入微信卡包 + * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_3&index=4 + * + * @param coupon_stock_id 代金券批次id + * @param partner_trade_no 商户单据号 + * @param openid 用户openid + * @return API返回数据 + * @throws Exception e + */ + public Map sendCoupon(String coupon_stock_id, String partner_trade_no, String openid) throws Exception { + + /** 构造请求参数数据 **/ + Map data = new HashMap<>(); + + // 代金券批次id coupon_stock_id 是 1757 String 代金券批次id + data.put("coupon_stock_id", coupon_stock_id); + // openid记录数 openid_count 是 1 int openid记录数(目前支持num=1) + data.put("openid_count", "1"); + // 商户单据号 partner_trade_no 是 1000009820141203515766 String 商户此次发放凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性 + data.put("partner_trade_no", partner_trade_no); + // 用户openid openid 是 onqOjjrXT-776SpHnfexGm1_P7iE String Openid信息,用户在appid下的唯一标识 + data.put("openid", openid); + + /** 以下参数为非必填参数 **/ + // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 + // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 + // 协议版本 version 否 1.0 String(32) 默认1.0 + // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 + + + /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ + // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 + // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 + // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 + // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 + + // 微信调用接口 + Map resultMap = this.sendCoupon(data); + + WXPayUtil.getLogger().info("wxPay.sendCoupon:" + resultMap); + + return resultMap; + } + + /** + * 作用:商户平台-代金券或立减优惠-查询代金券信息
+ * 场景:查询代金券信息 + * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_5&index=6 + * + * @param coupon_id 代金券id + * @param stock_id 批次号 + * @param openid 用户openid + * @return API返回数据 + * @throws Exception e + */ + public Map queryCouponsInfo(String coupon_id, String stock_id, String openid) throws Exception { + + /** 构造请求参数数据 **/ + Map data = new HashMap<>(); + + // 代金券id coupon_id 是 1565 String 代金券id + data.put("coupon_id", coupon_id); + // 用户openid openid 是 onqOjjrXT-776SpHnfexGm1_P7iE String Openid信息,用户在appid下的唯一标识 + data.put("openid", openid); + // 批次号 stock_id 是 58818 String(32) 代金劵对应的批次号 + data.put("stock_id", stock_id); + + /** 以下参数为非必填参数 **/ + // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 + // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 + // 协议版本 version 否 1.0 String(32) 默认1.0 + // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 + + + /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ + // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 + // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 + // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 + // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 + + // 微信调用接口 + Map resultMap = this.queryCouponsInfo(data); + + WXPayUtil.getLogger().info("wxPay.queryCouponsInfo:" + resultMap); + + return resultMap; + } + + /** + * 作用:商户平台-代金券或立减优惠-查询代金券批次
+ * 场景:查询代金券批次信息 + * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_4&index=5 + * + * @param coupon_stock_id 代金券批次id + * @return API返回数据 + * @throws Exception e + */ + public Map queryCouponStock(String coupon_stock_id) throws Exception { + + /** 构造请求参数数据 **/ + Map data = new HashMap<>(); + + // 代金券批次id coupon_stock_id 是 1757 String 代金券批次id + data.put("coupon_stock_id", coupon_stock_id); + + /** 以下参数为非必填参数 **/ + // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 + // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 + // 协议版本 version 否 1.0 String(32) 默认1.0 + // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 + + + /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ + // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 + // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 + // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 + // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 + + // 微信调用接口 + Map resultMap = this.queryCouponStock(data); + + WXPayUtil.getLogger().info("wxPay.queryCouponStock:" + resultMap); + + return resultMap; + } + } // end class diff --git a/src/main/java/com/weixin/pay/card/CardBgColorEnum.java b/src/main/java/com/weixin/pay/card/CardBgColorEnum.java new file mode 100644 index 0000000..6019c02 --- /dev/null +++ b/src/main/java/com/weixin/pay/card/CardBgColorEnum.java @@ -0,0 +1,91 @@ +package com.weixin.pay.card; + +/** + * 微信卡券背景颜色枚举类 + * + * @author yclimb + * @date 2018/9/18 + */ +public enum CardBgColorEnum { + + /** + * 淡绿色 + */ + COLOR_010("Color010", "#63b359"), + /** + * 深绿色 + */ + COLOR_020("Color020", "#2c9f67"), + /** + * 淡蓝色 + */ + COLOR_030("Color030", "#509fc9"), + /** + * 深蓝色 + */ + COLOR_040("Color040", "#5885cf"), + /** + * 淡紫色 + */ + COLOR_050("Color050", "#9062c0"), + /** + * 土黄色 + */ + COLOR_060("Color060", "#d09a45"), + /** + * 淡黄色 + */ + COLOR_070("Color070", "#e4b138"), + /** + * 橘黄色 + */ + COLOR_080("Color080", "#ee903c"), + /** + * 橘黄色 plus + */ + COLOR_081("Color081", "#f08500"), + /** + * 青色 + */ + COLOR_082("Color082", "#a9d92d"), + /** + * 淡红色 + */ + COLOR_090("Color090", "#dd6549"), + /** + * 深红色 + */ + COLOR_100("Color100", "#cc463d"), + /** + * 玫红色 + */ + COLOR_101("Color101", "#cf3e36"), + /** + * 深灰色 + */ + COLOR_102("Color102", "#5E6671") + ; + + /** + * 背景颜色名称 + */ + private String bgName; + + /** + * 色值 + */ + private String bgVal; + + CardBgColorEnum(String bgName, String bgVal) { + this.bgName = bgName; + this.bgVal = bgVal; + } + + public String getBgName() { + return bgName; + } + + public String getBgVal() { + return bgVal; + } +} diff --git a/src/main/java/com/weixin/pay/constants/WXConstants.java b/src/main/java/com/weixin/pay/constants/WXConstants.java index 048e0aa..58619a5 100644 --- a/src/main/java/com/weixin/pay/constants/WXConstants.java +++ b/src/main/java/com/weixin/pay/constants/WXConstants.java @@ -29,6 +29,11 @@ public class WXConstants { */ public static final String OAUTH_STATE = "xxx"; + /** + * 小程序获取 access token code + */ + public static final String WX_MINI_PROGRAM_CODE = "xxxx"; + /** * 微信全局accessToken */ diff --git a/src/main/java/com/weixin/pay/constants/WXPayConstants.java b/src/main/java/com/weixin/pay/constants/WXPayConstants.java index 75559b9..f6ad60b 100755 --- a/src/main/java/com/weixin/pay/constants/WXPayConstants.java +++ b/src/main/java/com/weixin/pay/constants/WXPayConstants.java @@ -4,9 +4,6 @@ import org.apache.http.client.HttpClient; /** * 微信支付SDK常量 - * - * @author yclimb - * @date 2018/8/17 */ public class WXPayConstants { @@ -24,7 +21,7 @@ public class WXPayConstants { * 微信签名枚举类型 */ public enum SignType { - MD5, HMACSHA256 + MD5, HMACSHA256, SHA1 } /** @@ -64,9 +61,9 @@ public class WXPayConstants { * JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里 * MICROPAY--刷卡支付,刷卡支付有单独的支付接口,不调用统一下单接口 */ - public static String TRADE_TYPE = "JSAPI"; - public static String TRADE_TYPE_APP = "APP"; - public static String TRADE_TYPE_NATIVE = "NATIVE"; + public static final String TRADE_TYPE = "JSAPI"; + public static final String TRADE_TYPE_APP = "APP"; + public static final String TRADE_TYPE_NATIVE = "NATIVE"; /** * 微信 - API域名地址 @@ -133,6 +130,29 @@ public class WXPayConstants { * 其他:需要证书 */ public static final String SENDREDPACK_URL_SUFFIX = "/mmpaymkttransfers/sendredpack"; + /** + * 作用:商户平台-现金红包-查询红包记录
+ * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 + * 其他:需要证书 + */ + public static final String GETHBINFO_URL_SUFFIX = "/mmpaymkttransfers/gethbinfo"; + /** + * 作用:商户平台-代金券或立减优惠-发放代金券
+ * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 + * 注意:通过接口发放的代金券不会进入微信卡包 + * 其他:请求需要双向证书 + */ + public static final String SEND_COUPON_URL_SUFFIX = "/mmpaymkttransfers/send_coupon"; + /** + * 作用:商户平台-代金券或立减优惠-查询代金券信息
+ * 场景:查询代金券信息。 + */ + public static final String QUERYCOUPONSINFO_URL_SUFFIX = "/mmpaymkttransfers/querycouponsinfo"; + /** + * 作用:商户平台-代金券或立减优惠-查询代金券批次
+ * 场景:查询代金券批次信息。 + */ + public static final String QUERY_COUPON_STOCK_URL_SUFFIX = "/mmpaymkttransfers/query_coupon_stock"; /** * 作用:提交刷卡支付
* 场景:刷卡支付 @@ -210,7 +230,7 @@ public class WXPayConstants { public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery"; public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse"; public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder"; - public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/secapi/pay/refund"; + public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/pay/refund"; public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery"; public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill"; public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report"; @@ -218,6 +238,11 @@ public class WXPayConstants { public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid"; public static final String SANDBOX_SENDREDPACK_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/sendredpack"; public static final String SANDBOX_TRANSFERS_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/promotion/transfers"; + public static final String SANDBOX_GETHBINFO_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/promotion/gethbinfo"; + public static final String SANDBOX_SEND_COUPON_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/send_coupon"; + public static final String SANDBOX_QUERYCOUPONSINFO_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/querycouponsinfo"; + public static final String SANDBOX_QUERY_COUPON_STOCK_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/query_coupon_stock"; + } diff --git a/src/main/java/com/weixin/pay/constants/WeChatURL.java b/src/main/java/com/weixin/pay/constants/WeChatURL.java new file mode 100644 index 0000000..c005dcf --- /dev/null +++ b/src/main/java/com/weixin/pay/constants/WeChatURL.java @@ -0,0 +1,109 @@ +package com.weixin.pay.constants; + +/** + * 微信公众号相关接口 + * + * @author yclimb + * @date 2018/11/1 + */ +public interface WeChatURL { + + /** + * 请求URL之获取jsapi_ticket + */ + String PAGE_URL_SIGN = "jsapi_ticket={0}&noncestr={1}×tamp={2}&url={3}"; + /** + * 请求URL之获取access_token + */ + String BASE_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"; + /** + * 请求URL之获取jsapi_ticket + */ + String BASE_JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi"; + /** + * 请求URL之创建菜单 + */ + String MENU_CREATE = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}"; + /** + * 请求URL之查询菜单 + */ + String MENU_QUERY = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}"; + /** + * 请求URL之删除菜单 + */ + String MENU_DELETE = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}"; + + /** + * 页面授权获取code地址 + */ + String OAUTH_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=" + WXConstants.OAUTH_STATE + "#wechat_redirect"; + + /** + * 通过code换取网页授权access_token + */ + String OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code"; + + /** + * 页面授权获取指定微信号的基础信息 + */ + String OAUTH_GET_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}&lang=zh_CN"; + + /** + * 获取指定微信号的基础信息 通过全局access_token + */ + String GET_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"; + + /** + * 微信模板消息发送 + */ + String WX_TEMPLATE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}"; + + /** + * 微信客户消息发送 + */ + String WX_CUSTMOER_SERVICE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={}"; + + /*** + * 微信创建二维码ticket + */ + String WX_TICKET_CREATE = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}"; + + + /** + * 小程序登录校验 + */ + String WX_MINI_LOGIN = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; + + /** + * 小程序模板信息 + */ + String WX_MINI_TEMPLATE_MSG = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token={ACCESS_TOKEN}"; + + /** + * 获取小程序二维码,通过该接口生成的小程序码,永久有效,数量暂无限制 + */ + String WX_MINI_QR_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={0}"; + + /** + * 创建支付后领取立减金活动接口 + * 通过此接口创建立减金活动。 + * 将已创建的代金券cardid、跳转小程序appid、发起支付的商户号等信息通过此接口创建立减金活动,成功返回活动id即为创建成功。 + * 接口地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21515658940X5pIn + * + * 协议:https + * http请求方式: POST + * 请求URL:https://api.weixin.qq.com/card/mkt/activity/create?access_token=ACCESS_TOKEN + * POST数据格式:JSON + */ + String WX_CARD_ACTIVITY_CREATE_URL = "https://api.weixin.qq.com/card/mkt/activity/create?access_token={0}"; + + /** + * 卡券签名和JSSDK的签名完全独立,两者的算法和意义完全不同,请不要混淆。 + * JSSDK的签名是使用所有JS接口都需要走的一层鉴权,用以标识调用者的身份,和卡券本身并无关系。 + * 其次,卡券的签名考虑到协议的扩展性和简单的防数据擅改,设计了一套独立的签名协议。 + * 另外由于历史原因,卡券的JS接口先于JSSDK出现,当时的JSAPI并没有鉴权体系,所以在卡券的签名里也加上了appsecret/api_ticket这些身份信息,希望开发者理解。 + * 卡券 api_ticket 是用于调用卡券相关接口的临时票据,有效期为 7200 秒,通过 access_token 来获取。这里要注意与 jsapi_ticket 区分开来。 + * 由于获取卡券 api_ticket 的 api 调用次数非常有限,频繁刷新卡券 api_ticket 会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存卡券 api_ticket 。 + */ + String BASE_API_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=wx_card"; +} diff --git a/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java b/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java index bfe8a97..c749237 100644 --- a/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java +++ b/src/main/java/com/weixin/pay/redis/RedisKeyEnum.java @@ -11,7 +11,15 @@ public enum RedisKeyEnum { /** * 生成带参数的小程序二维码KEY */ - XXX_MINI_WX_CODE(RedisKeyUtil.KEY_PREFIX, "mini", "getwxacodeunlimit", "生成永久无限制微信二维码") + XXX_MINI_WX_CODE(RedisKeyUtil.KEY_PREFIX, "mini", "getwxacodeunlimit", "生成永久无限制微信二维码"), + /** + * 获取卡券api_ticket + */ + IMALL_WXCARD_APITICKET(RedisKeyUtil.KEY_PREFIX, "jsapi", "getWxCardApiTicket", "获取卡券api_ticket的api"), + /** + * 获取卡券api_ticket + */ + IMALL_WX_APITICKET(RedisKeyUtil.KEY_PREFIX, "jsapi", "getWxApiTicket", "获取api_ticket的api") ; diff --git a/src/main/java/com/weixin/pay/util/DateTimeUtil.java b/src/main/java/com/weixin/pay/util/DateTimeUtil.java new file mode 100644 index 0000000..2ddd4d5 --- /dev/null +++ b/src/main/java/com/weixin/pay/util/DateTimeUtil.java @@ -0,0 +1,159 @@ +package com.weixin.pay.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * 时间处理工具类 + * + * @author yclimb + * @date 2018/11/1 + */ +public class DateTimeUtil { + + public static final String TIME_FORMAT_SHORT = "yyyyMMddHHmmss"; + public static final String TIME_FORMAT_SHORT_HOUR = "yyyyMMddHH"; + public static final String TIME_FORMAT_YMD = "yyyy/MM/dd HH:mm:ss"; + public static final String TIME_FORMAT_NORMAL = "yyyy-MM-dd HH:mm:ss"; + public static final String TIME_FORMAT_ENGLISH = "MM/dd/yyyy HH:mm:ss"; + public static final String TIME_FORMAT_CHINA = "yyyy年MM月dd日 HH时mm分ss秒"; + public static final String TIME_FORMAT_CHINA_M = "yyyy年MM月dd日 HH时mm分"; + public static final String TIME_FORMAT_CHINA_S = "yyyy年M月d日 H时m分s秒"; + public static final String TIME_FORMAT_SHORT_S = "HH:mm:ss"; + + public static final String DATE_FORMAT_SHORT = "yyyyMMdd"; + public static final String DATE_FORMAT_NORMAL = "yyyy-MM-dd"; + public static final String DATE_FORMAT_ENGLISH = "MM/dd/yyyy"; + public static final String DATE_FORMAT_CHINA = "yyyy年MM月dd日"; + public static final String DATE_FORMAT_CHINA_YEAR_MONTH = "yyyy年MM月"; + public static final String MONTH_FORMAT = "yyyyMM"; + public static final String YEAR_MONTH_FORMAT = "yyyy-MM"; + public static final String DATE_FORMAT_MINUTE = "yyyyMMddHHmm"; + public static final String MONTH_DAY_FORMAT = "MM-dd"; + public static final String YEAR_FORMAT = "yyyy"; + public static final String TIME_FORMAT_TIME = "yyyy/MM/dd HH:mm"; + private static final SimpleDateFormat sdf = new SimpleDateFormat( + DATE_FORMAT_NORMAL); + + private static final SimpleDateFormat sdfTime = new SimpleDateFormat( + TIME_FORMAT_NORMAL); + + private static final SimpleDateFormat sdfTimes = new SimpleDateFormat( + "yyyyMMddHHmmssSSS"); + + private static final SimpleDateFormat sdfTChina = new SimpleDateFormat( + TIME_FORMAT_CHINA); + + /** + * 把日期字符串转换为日期类型 + * + * @param dateStr 日期字符串 + * @return 日期 + * @since 0.1 + */ + public static Date convertAsDate(String dateStr) { + if (dateStr == null || "".equals(dateStr)) { + return null; + } + DateFormat fmt = null; + if (dateStr.matches("\\d{14}")) { + fmt = new SimpleDateFormat(TIME_FORMAT_SHORT); + } else if (dateStr + .matches("\\d{4}-\\d{1,2}-\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}")) { + fmt = new SimpleDateFormat(TIME_FORMAT_NORMAL); + } else if (dateStr + .matches("\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{1,2}:\\d{1,2}")) { + fmt = new SimpleDateFormat(TIME_FORMAT_ENGLISH); + } else if (dateStr + .matches("\\d{4}年\\d{1,2}月\\d{1,2}日 \\d{1,2}时\\d{1,2}分\\d{1,2}秒")) { + fmt = new SimpleDateFormat(TIME_FORMAT_CHINA); + } else if (dateStr.matches("\\d{8}")) { + fmt = new SimpleDateFormat(DATE_FORMAT_SHORT); + } else if (dateStr.matches("\\d{4}-\\d{1,2}-\\d{1,2}")) { + fmt = new SimpleDateFormat(DATE_FORMAT_NORMAL); + } else if (dateStr.matches("\\d{1,2}/\\d{1,2}/\\d{4}")) { + fmt = new SimpleDateFormat(DATE_FORMAT_ENGLISH); + } else if (dateStr.matches("\\d{4}年\\d{1,2}月\\d{1,2}日")) { + fmt = new SimpleDateFormat(DATE_FORMAT_CHINA); + } else if (dateStr.matches("\\d{4}\\d{1,2}\\d{1,2}\\d{1,2}\\d{1,2}")) { + fmt = new SimpleDateFormat(DATE_FORMAT_MINUTE); + } else if (dateStr.matches("\\d{1,2}:\\d{1,2}:\\d{1,2}")) { + fmt = new SimpleDateFormat(TIME_FORMAT_SHORT_S); + } + try { + return fmt.parse(dateStr); + } catch (ParseException e) { + throw new IllegalArgumentException( + "Date or Time String is invalid."); + } + } + + /** + * 得到时间字符串,格式为 yyyyMMddHHmmss + * @return 返回当前时间的字符串 + * + * @author yclimb + * @date 2018/11/1 + */ + public static String getTimeShortString(Date date) { + return new SimpleDateFormat(TIME_FORMAT_SHORT).format(date); + } + + /** + * 得到十位数的时间戳 + * @param date 时间对象 + * @return long + * + * @author yclimb + * @date 2018/9/18 + */ + public static long getTenTimeByDate(Date date) { + return date.getTime() / 1000; + } + + /** + * 得到十位数的时间戳 + * @param dateStr 时间字符串 + * @return long + * + * @author yclimb + * @date 2018/9/18 + */ + public static long getTenTimeByDate(String dateStr) { + return convertAsDate(dateStr).getTime() / 1000; + } + + + /** + * Description: 比较两个字符串格式的时间大小
+ * 如果第二个时间大于第一个时间返回true,否则返回false + * + * @param strFirst 第一个时间 + * @param strSecond 第二个时间 + * @param strFormat 时间格式化方式 eg:"yyyy-MM-dd HH:mm:ss"," yyyy-MM-dd" + * @return true-第二个时间晚于第一个时间,false-第二个时间不晚于第一个时间 + * + * @author yclimb + * @date 2018/11/1 + */ + public static boolean latterThan(String strFirst, String strSecond, + String strFormat) { + SimpleDateFormat ft = new SimpleDateFormat(strFormat); + try { + Date date1 = ft.parse(strFirst); + Date date2 = ft.parse(strSecond); + long quot = date2.getTime() - date1.getTime(); + if (0 < quot) { + return true; + } else { + return false; + } + } catch (ParseException e) { + e.printStackTrace(); + } + return false; + } + +} diff --git a/src/main/java/com/weixin/pay/util/WXPayUtil.java b/src/main/java/com/weixin/pay/util/WXPayUtil.java index 6b31c63..db897b1 100755 --- a/src/main/java/com/weixin/pay/util/WXPayUtil.java +++ b/src/main/java/com/weixin/pay/util/WXPayUtil.java @@ -116,7 +116,7 @@ public class WXPayUtil { * @return 含有sign字段的XML */ public static String generateSignedXml(final Map data, String key) throws Exception { - return generateSignedXml(data, key, SignType.MD5); + return generateSignedXml(data, key, WXPayConstants.SignType.MD5); } /** @@ -238,6 +238,44 @@ public class WXPayUtil { return new String(nonceChars); } + /** + * 根据字典排序 + * @param type 1:根据key键排序;2:根据value值排序 + * @param data d + * @return sb + */ + public static String dictionaryOrder(final Map data, int type) { + Set keySet = data.keySet(); + String[] keyArray = keySet.toArray(new String[keySet.size()]); + StringBuilder sb = new StringBuilder(); + if (type == 2) { + String[] valArray = new String[keySet.size()]; + for (int i = 0; i < keySet.size(); i++) { + valArray[i] = data.get(keyArray[i]); + } + Arrays.sort(valArray); + for (String v : valArray) { + // 参数值为空,则不参与签名 + if (v.trim().length() > 0) { + sb.append(v); + } + } + } else { + Arrays.sort(keyArray); + for (int i = 0; i < keyArray.length; i++) { + // 参数值为空,则不参与签名 + if (data.get(keyArray[i]).trim().length() > 0) { + if (i == keyArray.length - 1) { + sb.append(keyArray[i]).append("=").append(data.get(keyArray[i]).trim()); + } else { + sb.append(keyArray[i]).append("=").append(data.get(keyArray[i]).trim()).append("&"); + } + } + } + } + return sb.toString(); + } + /** * 生成 MD5 @@ -260,7 +298,7 @@ public class WXPayUtil { * @param data 待处理数据 * @param key 密钥 * @return 加密结果 - * @throws Exception + * @throws Exception e */ public static String HMACSHA256(String data, String key) throws Exception { Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); @@ -274,9 +312,35 @@ public class WXPayUtil { return sb.toString().toUpperCase(); } + /** + * SHA1 安全加密算法 + * @param data 待处理数据 + * @return str + * @throws Exception e + */ + public static String SHA1(String data) throws Exception { + //指定sha1算法 + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update(data.getBytes("UTF-8")); + //获取字节数组 + byte messageDigest[] = digest.digest(); + // Create Hex String + StringBuilder hexString = new StringBuilder(); + // 字节数组转换为 十六进制 数 + for (int i = 0; i < messageDigest.length; i++) { + String shaHex = Integer.toHexString(messageDigest[i] & 0xFF); + if (shaHex.length() < 2) { + hexString.append(0); + } + hexString.append(shaHex); + } + return hexString.toString(); + + } + /** * 日志 - * @return + * @return log */ public static Logger getLogger() { return LoggerFactory.getLogger("wxpay java sdk"); @@ -284,15 +348,15 @@ public class WXPayUtil { /** * 获取当前时间戳,单位秒 - * @return + * @return long */ public static long getCurrentTimestamp() { - return System.currentTimeMillis()/1000; + return System.currentTimeMillis() / 1000; } /** * 获取当前时间戳,单位毫秒 - * @return + * @return long */ public static long getCurrentTimestampMs() { return System.currentTimeMillis(); @@ -300,10 +364,22 @@ public class WXPayUtil { /** * 生成 uuid, 即用来标识一笔单,也用做 nonce_str - * @return + * @return str */ public static String generateUUID() { return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); } + /** + * 通用商户单号(每个单号必须唯一)28位 + * 组成:mch_id+yyyyMMddHHmmss+4位随机数 + * + * @author yclimb + * @date 2018/9/18 + */ + public static String getPayNo() { + String yyyyMMddHHmmss = DateTimeUtil.getTimeShortString(new Date()); + int str4 = (int) (Math.random() * 9000) + 1000; + return WXPayConstants.MCH_ID + yyyyMMddHHmmss + str4; + } } diff --git a/src/main/java/com/weixin/pay/util/WXUtils.java b/src/main/java/com/weixin/pay/util/WXUtils.java index e9fdf77..9878b92 100644 --- a/src/main/java/com/weixin/pay/util/WXUtils.java +++ b/src/main/java/com/weixin/pay/util/WXUtils.java @@ -1,10 +1,13 @@ package com.weixin.pay.util; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; +import com.weixin.pay.card.CardBgColorEnum; import com.weixin.pay.constants.WXConstants; import com.weixin.pay.constants.WXPayConstants; import com.weixin.pay.constants.WXURL; +import com.weixin.pay.constants.WeChatURL; import com.weixin.pay.redis.RedisKeyEnum; import com.weixin.pay.redis.RedisKeyUtil; import lombok.extern.slf4j.Slf4j; @@ -21,6 +24,7 @@ import javax.annotation.Resource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; @@ -274,4 +278,346 @@ public class WXUtils { return null; } + /** + * 创建支付后领取立减金活动接口 + * 通过此接口创建立减金活动。 + * 将已创建的代金券cardid、跳转小程序appid、发起支付的商户号等信息通过此接口创建立减金活动,成功返回活动id即为创建成功。 + * 接口地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21515658940X5pIn + * + * @param begin_time 活动开始时间,精确到秒 + * @param end_time 活动结束时间,精确到秒 + * @param gift_num 单个礼包社交立减金数量(3-15个) + * @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50,默认为1 + * @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50,默认为1 + * @param card_id 卡券ID + * @param min_amt 最少支付金额,单位是元 + * @param membership_appid 奖品指定的会员卡appid。如用户标签有选择商户会员,则需要填写会员卡appid,该appid需要跟所有发放商户号有绑定关系。 + * @param new_tinyapp_user 可以指定为是否小程序新用户(membership_appid为空、new_tinyapp_user为false时,指定为所有用户) + * @return json + * @author yclimb + * @date 2018/9/18 + */ + public JSONObject createCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act, + int max_partic_times_one_day, String card_id, String min_amt, + String membership_appid, boolean new_tinyapp_user) { + try { + + // 创建活动接口之前的验证 + String msg = checkCardActivity(begin_time, end_time, gift_num, max_partic_times_act, max_partic_times_one_day, min_amt); + if (null != msg) { + JSONObject resultJson = new JSONObject(2); + resultJson.put("errcode", "1"); + resultJson.put("errmsg", msg); + return resultJson; + } + + // 获取[爱上悦店]公众号的 access_token + String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE); + + // 调用接口传入参数 + JSONObject paramJson = new JSONObject(1); + + // info 包含 basic_info、card_info_list、custom_info + JSONObject info = new JSONObject(3); + + // 基础信息对象 + JSONObject basic_info = new JSONObject(8); + // activity_bg_color 是 活动封面的背景颜色,可参考:选取卡券背景颜色 + basic_info.put("activity_bg_color", CardBgColorEnum.COLOR_090.getBgName()); + // activity_tinyappid 是 用户点击链接后可静默添加到列表的小程序appid; + basic_info.put("activity_tinyappid", WXPayConstants.APP_ID); + // mch_code 是 支付商户号 + basic_info.put("mch_code", WXPayConstants.MCH_ID); + // begin_time 是 活动开始时间,精确到秒(unix时间戳) + basic_info.put("begin_time", DateTimeUtil.getTenTimeByDate(begin_time)); + // end_time 是 活动结束时间,精确到秒(unix时间戳) + basic_info.put("end_time", DateTimeUtil.getTenTimeByDate(end_time)); + // gift_num 是 单个礼包社交立减金数量(3-15个) + basic_info.put("gift_num", gift_num); + // max_partic_times_act 否 每个用户活动期间最大领取次数,最大为50,不填默认为1 + basic_info.put("max_partic_times_act", max_partic_times_act); + // max_partic_times_one_day 否 每个用户活动期间单日最大领取次数,最大为50,不填默认为1 + basic_info.put("max_partic_times_one_day", max_partic_times_one_day); + + // card_info_list 是 可以配置两种发放规则:小程序新老用户、新老会员 + JSONArray card_info_list = new JSONArray(1); + JSONObject card_info = new JSONObject(3); + // card_id 是 卡券ID + card_info.put("card_id", card_id); + // min_amt 是 最少支付金额,单位是分 + card_info.put("min_amt", String.valueOf(new BigDecimal(min_amt).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); + /* + * membership_appid 是 奖品指定的会员卡appid。如用户标签有选择商户会员,则需要填写会员卡appid,该appid需要跟所有发放商户号有绑定关系。 + * new_tinyapp_user 是 可以指定为是否小程序新用户 + * total_user 是 可以指定为所有用户 + * membership_appid、new_tinyapp_user、total_user以上字段3选1,未选择请勿填,不必故意填写false + */ + if (StringUtils.isNotBlank(membership_appid)) { + card_info.put("membership_appid", membership_appid); + } else { + if (new_tinyapp_user) { + card_info.put("new_tinyapp_user", true); + } else { + card_info.put("total_user", true); + } + } + card_info_list.add(card_info); + + // 自定义字段,表示支付后领券 + JSONObject custom_info = new JSONObject(1); + custom_info.put("type", "AFTER_PAY_PACKAGE"); + + // 拼装json对象 + info.put("basic_info", basic_info); + info.put("card_info_list", card_info_list); + info.put("custom_info", custom_info); + paramJson.put("info", info); + + // 请求微信接口,得到返回结果[json] + HttpEntity entity = new HttpEntity<>(paramJson, this.getHttpHeadersUTF8JSON()); + JSONObject resultJson = restTemplate.postForObject(WeChatURL.WX_CARD_ACTIVITY_CREATE_URL, entity, JSONObject.class, accessToken); + + // {"errcode":0,"errmsg":"ok","activity_id":"4728935"} + System.out.println(resultJson.toJSONString()); + + return resultJson; + } catch (Exception e) { + WXPayUtil.getLogger().error(e.getMessage(), e); + } + return null; + } + + /** + * 创建活动接口之前的验证 + * + * @param begin_time 活动开始时间,精确到秒 + * @param end_time 活动结束时间,精确到秒 + * @param gift_num 单个礼包社交立减金数量(3-15个) + * @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50,默认为1 + * @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50,默认为1 + * @param min_amt 最少支付金额,单位是元 + * @return msg str + * @author yclimb + * @date 2018/9/18 + */ + public String checkCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act, + int max_partic_times_one_day, String min_amt) { + + // 开始时间不能小于结束时间 + if (DateTimeUtil.latterThan(end_time, begin_time, DateTimeUtil.TIME_FORMAT_NORMAL)) { + return "活动开始时间不能小于活动结束时间"; + } + + // 单个礼包社交立减金数量(3-15个) + if (gift_num < 3 || gift_num > 15) { + return "单个礼包社交立减金数量(3-15个)"; + } + + // 每个用户活动期间最大领取次数,最大为50,默认为1 + if (max_partic_times_act <= 0 || max_partic_times_act > 50) { + return "每个用户活动期间最大领取次数,最大为50,默认为1"; + } + + // 每个用户活动期间单日最大领取次数,最大为50,默认为1 + if (max_partic_times_one_day <= 0 || max_partic_times_one_day > 50) { + return "每个用户活动期间单日最大领取次数,最大为50,默认为1"; + } + + // 最少支付金额,单位是元 + if (BigDecimal.ONE.compareTo(new BigDecimal(min_amt)) > 0) { + return "最少支付金额必须大于1元"; + } + + return null; + } + + /** + * 获取卡券 api_ticket 的 api + * 请求路径:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=wx_card + * + * @param access_token token + * @return api_ticket json obj + * @author yclimb + * @date 2018/9/21 + */ + public String getWxCardApiTicket(String access_token) { + if (StringUtils.isBlank(access_token)) { + return null; + } + try { + + // redis key + String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WXCARD_APITICKET, access_token); + + // 从redis中获取缓存 + Object obj = redisTemplate.opsForValue().get(redisKey); + if (obj != null) { + return obj.toString(); + } + + // 获取卡券 api_ticket + String api_ticket = restTemplate.getForObject(WeChatURL.BASE_API_TICKET, String.class, access_token); + WXPayUtil.getLogger().info("getWxCardApiTicket:api_ticket:{}", api_ticket); + if (StringUtils.isBlank(api_ticket)) { + return null; + } + JSONObject jsonObject = JSON.parseObject(api_ticket); + if (0 != jsonObject.getIntValue("errcode")) { + return null; + } + + // 设置到redis中,下次取直接拿缓存即可,防止多次生成 + String ticket = jsonObject.getString("ticket"); + redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS); + + return ticket; + } catch (Exception e) { + WXPayUtil.getLogger().error(e.getMessage(), e); + } + return null; + } + + /** + * 获取卡券 api_ticket 的 api + * 请求路径:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi + * + * @param access_token token + * @return api_ticket json obj + * @author yclimb + * @date 2018/9/25 + */ + public String getWxApiTicket(String access_token) { + if (StringUtils.isBlank(access_token)) { + return null; + } + try { + + // redis key + String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WX_APITICKET, access_token); + + // 从redis中获取缓存 + Object obj = redisTemplate.opsForValue().get(redisKey); + if (obj != null) { + return obj.toString(); + } + + // 获取 api_ticket + String api_ticket = restTemplate.getForObject(WeChatURL.BASE_JSAPI_TICKET, String.class, access_token); + WXPayUtil.getLogger().info("getWxApiTicket:api_ticket:{}", api_ticket); + if (StringUtils.isBlank(api_ticket)) { + return null; + } + JSONObject jsonObject = JSON.parseObject(api_ticket); + if (0 != jsonObject.getIntValue("errcode")) { + return null; + } + + // 设置到redis中,下次取直接拿缓存即可,防止多次生成 + String ticket = jsonObject.getString("ticket"); + redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS); + + return ticket; + } catch (Exception e) { + WXPayUtil.getLogger().error(e.getMessage(), e); + } + return null; + } + + /** + * 根据代金券批次ID得到组合的cardList + * + * @param cardId 卡包ID + * @return cardList + * @author yclimb + * @date 2018/9/21 + */ + public JSONArray getCardList(String cardId) { + if (StringUtils.isBlank(cardId)) { + return null; + } + try { + + // 获取[爱上悦店]公众号的 access_token + String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE); + String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp()); + String nonce_str = WXPayUtil.generateNonceStr(); + + // 卡券的扩展参数。需进行 JSON 序列化为字符串传入 + JSONObject cardExt = new JSONObject(); + //cardExt.put("code", ""); + //cardExt.put("openid", ""); + //cardExt.put("fixed_begintimestamp", ""); + //cardExt.put("outer_str", ""); + cardExt.put("timestamp", timestamp); + cardExt.put("nonce_str", nonce_str); + + /** + * 1.将 api_ticket、timestamp、card_id、code、openid、nonce_str的value值进行字符串的字典序排序。 + * 2.将所有参数字符串拼接成一个字符串进行sha1加密,得到signature。 + * 3.signature中的timestamp,nonce字段和card_ext中的timestamp,nonce_str字段必须保持一致。 + */ + Map map = new HashMap<>(8); + //map.put("code", ""); + //map.put("openid", ""); + map.put("api_ticket", this.getWxCardApiTicket(accessToken)); + map.put("timestamp", timestamp); + map.put("card_id", cardId); + map.put("nonce_str", nonce_str); + cardExt.put("signature", WXPayUtil.SHA1(WXPayUtil.dictionaryOrder(map, 2))); + + // 卡券对象 + JSONObject cardInfo = new JSONObject(); + cardInfo.put("cardId", cardId); + cardInfo.put("cardExt", cardExt.toJSONString()); + + // 需要添加的卡券列表 + JSONArray cardList = new JSONArray(1); + cardList.add(cardInfo); + + return cardList; + } catch (Exception e) { + WXPayUtil.getLogger().error(e.getMessage(), e); + } + return null; + } + + /** + * 获取微信签名信息 + * + * @param requestUrl 请求页面地址 + * @param appid appid + * @param code code + * @return 返回map:noncestr:随机字符串;timestamp:签名时间戳;appid;微信公众号Id;signature:签名串 + * @author yclimb + * @date 2018/9/25 + */ + public Map getSignature(String requestUrl, String appid, String code) { + Map map = new HashMap<>(); + try { + + // 获取公众号的 access_token、jsapi_ticket + String accessToken = this.getAccessToken(code); + String jsapi_ticket = this.getWxApiTicket(accessToken); + String nonce_str = WXPayUtil.generateNonceStr(); + String timestamp = Long.toString(WXPayUtil.getCurrentTimestamp()); + + // 注意这里参数名必须全部小写,且必须有序 + String dataStr = "jsapi_ticket=" + jsapi_ticket + + "&noncestr=" + nonce_str + + "×tamp=" + timestamp + + "&url=" + requestUrl; + WXPayUtil.getLogger().info(dataStr); + + String signature = WXPayUtil.SHA1(dataStr); + map.put("noncestr", nonce_str); + map.put("timestamp", timestamp); + map.put("appid", appid); + map.put("signature", signature); + } catch (Exception e) { + WXPayUtil.getLogger().error(e.getMessage(), e); + } + + return map; + } + } diff --git a/src/main/test/controller/TestWXPay.java b/src/main/test/controller/TestWXPay.java new file mode 100755 index 0000000..9852b26 --- /dev/null +++ b/src/main/test/controller/TestWXPay.java @@ -0,0 +1,389 @@ +package controller; + +import com.weixin.pay.WXPay; +import com.weixin.pay.WXPayConfigImpl; +import com.weixin.pay.XxxWXPayConfigImpl; +import com.weixin.pay.util.WXPayUtil; +import com.weixin.pay.util.WXUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 统一接口测试 + */ +public class TestWXPay { + + private WXPay wxpay; + private WXPayConfigImpl config; + private String out_trade_no; + private String total_fee; + + public TestWXPay() throws Exception { + config = WXPayConfigImpl.getInstance(); + // wxpay = new WXPay(config); + wxpay = new WXPay(config, true, true); + total_fee = "1.01"; + // out_trade_no = "201701017496748980290321"; + out_trade_no = "20180912004"; + } + + /** + * 获取微信签名 + * @param url + */ + private void getWeixinMap(String url) { + + /*Map map = new HashMap<>(); + + try { + map = WXSignatureUtil.getSignature(request, url); + } catch (IOException | CloneNotSupportedException e) { + e.printStackTrace(); + System.out.println("获取微信签名信息异常!" + e.getMessage()); + } + model.addAttribute("noncestr", map.get("noncestr")); + model.addAttribute("timestamp", map.get("timestamp")); + model.addAttribute("appid", map.get("appid")); + model.addAttribute("signature", map.get("signature"));*/ + } + + /** + * 获取微信签名 + */ + private void getWeixinMap() { + getWeixinMap(getRequestURL()); + } + + private String getRequestURL() { + String url = null; + /*if (null == request.getQueryString()) { + url = request.getRequestURL().toString(); + } else { + url = request.getRequestURL() + "?" + request.getQueryString(); + }*/ + return url; + } + + /** + * 扫码支付 下单 + */ + private void doUnifiedOrder() throws Exception { + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + Map resultMap = wxPay.unifiedOrder("https://api.uat.iyuedian.com/iyd-imall-manage/imall/v1/weixin/pay/wxnotify", + "oPR7T5PFjcfgugIu2abQG6ijQGV4", "悦店-测试商品", WXPayUtil.getPayNo(), "10.01", "127.0.0.1", + "vip", "",null,null); + + String prepay_id = resultMap.get("prepay_id"); + String nonce_str = resultMap.get("nonce_str"); + Map map = wxPay.chooseWXPayMap(prepay_id, nonce_str); + System.out.println("map:" + map); + } + + + private void doOrderClose() { + System.out.println("关闭订单"); + HashMap data = new HashMap(); + data.put("out_trade_no", out_trade_no); + try { + Map r = wxpay.closeOrder(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void doOrderQuery() { + System.out.println("查询订单"); + HashMap data = new HashMap(); + data.put("out_trade_no", out_trade_no); +// data.put("transaction_id", "4008852001201608221962061594"); + try { + Map r = wxpay.orderQuery(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void doOrderReverse() { + System.out.println("撤销"); + HashMap data = new HashMap(); + data.put("out_trade_no", out_trade_no); +// data.put("transaction_id", "4008852001201608221962061594"); + try { + Map r = wxpay.reverse(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 长链接转短链接 + * 测试成功 + */ + private void doShortUrl() { + String long_url = "weixin://wxpay/bizpayurl?pr=etxB4DY"; + HashMap data = new HashMap(); + data.put("long_url", long_url); + try { + Map r = wxpay.shortUrl(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 退款 + * 已测试 + */ + private void doRefund() { + HashMap data = new HashMap(); + data.put("out_trade_no", out_trade_no); + data.put("out_refund_no", out_trade_no); + data.put("total_fee", total_fee); + data.put("refund_fee", total_fee); + data.put("refund_fee_type", "CNY"); + data.put("op_user_id", config.getMchID()); + + try { + Map r = wxpay.refund(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * 查询退款 + * 已经测试 + */ + private void doRefundQuery() { + HashMap data = new HashMap(); + data.put("out_trade_no", out_trade_no); + //data.put("transactionId", out_trade_no); + data.put("out_refund_no", out_trade_no); + //data.put("refund_id", out_trade_no); + try { + Map r = wxpay.refundQuery(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 对账单下载 + * 已测试 + */ + private void doDownloadBill() { + HashMap data = new HashMap(); + data.put("bill_date", "20161102"); + data.put("bill_type", "ALL"); + try { + Map r = wxpay.downloadBill(data); + System.out.println(r); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取沙盒 sandbox_signkey + * + * @author yclimb + * @date 2018/9/18 + */ + private void doGetSandboxSignKey() throws Exception { + WXPayConfigImpl config = WXPayConfigImpl.getInstance(); + HashMap data = new HashMap(); + data.put("mch_id", config.getMchID()); + data.put("nonce_str", WXPayUtil.generateNonceStr()); + String sign = WXPayUtil.generateSignature(data, config.getKey()); + data.put("sign", sign); + WXPay wxPay = new WXPay(config); + // String result = wxPay.requestWithoutCert("https://api.mch.weixin.qq.com/sandbox/pay/getsignkey", data, 10000, 10000); + String result = wxPay.requestWithoutCert("/sandboxnew/pay/getsignkey", data, 10000, 10000); + System.out.println(result); + } + + private void doReport() { + HashMap data = new HashMap(); + data.put("interface_url", "20160822"); + data.put("bill_type", "ALL"); + } + + /** + * 小测试 + */ + private void test001() { + String xmlStr="\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + ""; + try { + System.out.println(xmlStr); + System.out.println("+++++++++++++++++"); + System.out.println(WXPayUtil.isSignatureValid(xmlStr, config.getKey())); + Map hm = WXPayUtil.xmlToMap(xmlStr); + System.out.println("+++++++++++++++++"); + System.out.println(hm); + System.out.println(hm.get("attach").length()); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private void testUnifiedOrderSpeed() throws Exception { + TestWXPay dodo = new TestWXPay(); + + for (int i=0; i<100; ++i) { + long startTs = System.currentTimeMillis(); + out_trade_no = out_trade_no+i; + dodo.doUnifiedOrder(); + long endTs = System.currentTimeMillis(); + System.out.println(endTs-startTs); + Thread.sleep(1000); + } + + } + + /** + * 提现 + * + * @author yclimb + * @date 2018/9/18 + */ + public void doTranster() throws Exception { + // 微信调用接口 + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + Map resultMap = wxPay.transfers("1507928321201809301504246520", + "oPR7T5DWvXuhfKfyqNdi6MTQGaxo", "1.9", "测试退款", "127.0.0.1"); + System.out.println("wxPay.transfers:" + resultMap); + } + + /** + * 发送现金红包 + * + * @author yclimb + * @date 2018/9/18 + */ + private void sendRedPack() throws Exception { + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + wxPay.sendRedPack(WXPayUtil.getPayNo(), "obX_c0YRpT47zKcvq-ZYpjU6GFuA", "1", + "活动名称", "红包祝福语", "备注", "127.0.0.1"); + } + + /** + * 查询现金红包 + * + * @author yclimb + * @date 2018/9/18 + */ + private void getRedPackInfo() throws Exception { + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + wxPay.getRedPackInfo("1507928321201809171554055254"); + } + + /** + * 发送代金券 + * + * @author yclimb + * @date 2018/9/18 + */ + private void sendCoupon() throws Exception { + WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); + wxPay.sendCoupon("9248266", WXPayUtil.getPayNo(), "obX_c0YRpT47zKcvq-ZYpjU6GFuA"); + } + + /** + * 查询代金券信息 + * + * @author yclimb + * @date 2018/9/18 + */ + private void queryCouponsInfo() throws Exception { + WXPay wxPay = new WXPay(XxxWXPayConfigImpl.getInstance()); + wxPay.queryCouponsInfo("3983069127", "9248266", "obX_c0YRpT47zKcvq-ZYpjU6GFuA"); + } + + /** + * 查询代金券批次信息 + * + * @author yclimb + * @date 2018/9/18 + */ + private void queryCouponStock() throws Exception { + WXPay wxPay = new WXPay(XxxWXPayConfigImpl.getInstance()); + wxPay.queryCouponStock("9248266"); + } + + /** + * 创建支付后领取立减金活动接口 + * + * @author yclimb + * @date 2018/9/18 + */ + private void createCardActivity() { + WXUtils wxUtils = new WXUtils(); + wxUtils.createCardActivity("2018-09-18 18:00:00", "2018-09-18 19:59:59", 3, 1, + 1, "pX2-vjpU_MT1gFDsP8lNl15PdaZE", "100", + null, false); + } + + public static void main(String[] args) throws Exception { + System.out.println("--------------->"); + + TestWXPay dodo = new TestWXPay(); + //dodo.doGetSandboxSignKey(); + //dodo.doOrderQuery(); + //dodo.doRefundQuery(); + //dodo.doDownloadBill(); + //dodo.sendRedPack(); + //dodo.getRedPackInfo(); + //dodo.sendCoupon(); + //dodo.queryCouponsInfo(); + //dodo.queryCouponStock(); + //dodo.createCardActivity(); + //dodo.doUnifiedOrder(); + dodo.doTranster(); + + + // 沙箱环境测试 + //WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance(), true, true); + //WXPay wxPay = new WXPay(ChunboWXPayConfigImpl.getInstance()); + + /*Map resultMap = wxPay.refund("http://127.0.0.1:11000/weixin/pay/wxnotify", null, + "20180912004", "20180912004", "5.52", "5.52", "测试退款");*/ + + //System.out.println(resultMap); + + /*Map resultMap = wxPay.refund(null, "10000", "10001", "1.01", "0.01", "测试微信退款"); + System.out.println(WXPayUtil.isSignatureValid(resultMap, WXPayConstants.API_KEY));*/ + + + System.out.println("<---------------"); + } + +}