├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── weixin │ └── pay │ ├── WXPay.java │ ├── WXPayConfig.java │ ├── WXPayConfigImpl.java │ ├── WXPayDomain.java │ ├── WXPayDomainSimpleImpl.java │ ├── WXPayReport.java │ ├── WXPayRequest.java │ ├── XxxWXPayConfigImpl.java │ ├── card │ └── CardBgColorEnum.java │ ├── constants │ ├── WXConstants.java │ ├── WXPayCodeEnum.java │ ├── WXPayConstants.java │ └── WXURL.java │ ├── redis │ ├── RedisKeyEnum.java │ └── RedisKeyUtil.java │ └── util │ ├── AESUtil.java │ ├── DateTimeUtil.java │ ├── WXPayUtil.java │ ├── WXPayXmlUtil.java │ ├── WXSignatureUtil.java │ ├── WXUserUtil.java │ └── WXUtils.java └── test └── controller ├── TestWXPay.java ├── WXAuthController.java └── WXPayController.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, YClimb 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 微信支付 Java SDK 2 | ------ 3 | 4 | 2018最新最全微信支付集成SDK,一行代码调用微信支付,更多丰富接口注释和例子,包含基础支付功能(网页授权、各种签名、统一下单、退款、对账单、用户信息获取)、验收用例指引(沙箱支付、支付验收、免充值产品开通)、商户平台(现金红包、企业付款到用户、代金券或立减优惠)、公众平台(微信卡券、社交立减金活动)、小程序(生成永久二维码、发送模版消息)等等功能。 5 | 6 | 本项目依托于 [微信支付开发者文档](https://pay.weixin.qq.com/wiki/doc/api/index.html),对文档中的接口进行二次封装,从而为小伙伴们提供一个`拿来即用`的支付sdk工具。 7 | 8 | 相关的sdk文档已经更新,请进入以下地址查看: 9 | 10 | 文档地址:https://yclimb.gitbook.io/wxpay 11 | 12 | gitbook:https://github.com/YClimb/wxpay-gitbook 13 | 14 | 15 | ## 如何接入自身项目? 16 | 17 | ``` 18 | 现在一般是有两种方式,pom 引用 jar 包和直接 copy,详情如下: 19 | 20 | 1.第一种是直接把当前项目 clone 下来当作一个支付中间件,修改关键的支付参数后 deploy 到个人或者公司的私服中去,其他项目引用 jar 包; 21 | 2.第二种是直接把我的 sdk 中 java 下的目录和文件拷贝到你的项目中,放到 java 项目路径下; 22 | 23 | 释:两种方式各有利弊,jar 方式比较灵活,独立项目,但是修改参数要注意历史版本兼容和及时推包到私服,否则报错; 24 | 直接拷贝的方式就比较简单,不独立,修改起来方便,但是如果有多个公众号或者小程序,或者多个项目需要使用,则需要拷贝到多个项目中,麻烦不容易管理,比较复杂; 25 | 具体如何选择,看朋友们自身项目决定吧,建议公司级别项目使用第一种,更弹性可扩展。 26 | ``` 27 | 28 | ## 项目结构 29 | 首先需要简单说明整个 `wxpay-sdk` 的项目结构,主体结构如下所示: 30 | 31 | - wxpay-sdk 32 | - src 33 | - main 34 | - java 35 | - com.weixin.pay 36 | - card // 微信卡券 37 | - constants // 常量文件 38 | - redis // redis工具类 39 | - util // 支付工具类(支付、签名、加密解密) 40 | - xxx class // 支付实体类,基础配置信息 41 | - test 42 | - controller 43 | - xxx class // 测试的相关类 44 | - .gitignore 45 | - pom.xml // 引用包 46 | - README.md 47 | 48 | 49 | 50 | 51 | 提供微信支付的基础功能,脱胎于微信官方Java-SDK,进行二次封装后,提供一系列的方法; 52 | 基础方法主要在 `com.weixin.pay.WXPay` 、 `com.weixin.pay.util.WXUtils`类下,此项目包含的微信支付功能主要分为以下几个部分,这里列举一些主要功能,具体的详细功能查询作者gitbook或者公众号查看。 53 | 54 | ### 1. 基础支付功能 55 | 56 | `com.weixin.pay.WXPay` : 57 | 58 | |方法名 | 说明 | 59 | |--------|--------| 60 | |microPay| 刷卡支付 | 61 | |unifiedOrder | 统一下单| 62 | |chooseWXPayMap | 微信支付二次签名| 63 | |orderQuery | 查询订单 | 64 | |reverse | 撤销订单 | 65 | |closeOrder|关闭订单| 66 | |refund|申请退款| 67 | |refundQuery|查询退款| 68 | |downloadBill|下载对账单| 69 | |report|交易保障| 70 | |shortUrl|转换短链接| 71 | |authCodeToOpenid|授权码查询openid| 72 | 73 | 74 | 75 | ### 2. 验收用例 76 | 77 | 支付验收指引:`https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1` 78 | 79 | `controller.TestWXPay` : 80 | 81 | |方法名 | 说明 | 82 | |--------|--------| 83 | |unifiedOrder | 统一下单| 84 | |orderQuery | 查询订单 | 85 | |reverse | 撤销订单 | 86 | |closeOrder|关闭订单| 87 | |refund|申请退款| 88 | |refundQuery|查询退款| 89 | 90 | ### 3. 商户平台-现金红包 91 | 92 | `com.weixin.pay.WXPay` : 93 | 94 | |方法名 | 说明 | 95 | |--------|--------| 96 | |sendRedPack| 企业向指定微信用户的openid发放指定金额红包 | 97 | |getRedPackInfo| 查询红包记录 | 98 | 99 | ### 4. 商户平台-代金券或立减优惠 100 | 101 | `com.weixin.pay.WXPay` : 102 | 103 | |方法名 | 说明 | 104 | |--------|--------| 105 | |sendCoupon| 发放代金券 | 106 | |queryCouponsInfo| 查询代金券信息 | 107 | |queryCouponStock| 查询代金券批次 | 108 | 109 | ### 5. 公众平台-微信卡券 110 | 111 | `com.weixin.pay.util.WXUtils` : 112 | 113 | |方法名 | 说明 | 114 | |--------|--------| 115 | |getAccessToken| 获取微信全局accessToken | 116 | |getJsapiAccessTokenByCode| 网页授权获取用户信息时用于获取access_token以及openid | 117 | |getJsapiUserinfo| 通过access_token和openid请求获取用户信息 | 118 | |getWxCardApiTicket| 获取卡券 api_ticket 的 api | 119 | |getWxApiTicket| 获取卡券 api_ticket 的 api | 120 | 121 | ### 6. 公众平台-社交立减金活动 122 | 123 | `com.weixin.pay.util.WXUtils` : 124 | 125 | |方法名 | 说明 | 126 | |--------|--------| 127 | |getCardList| 根据代金券批次ID得到组合的cardList | 128 | |createCardActivity| 创建支付后领取立减金活动接口 | 129 | 130 | 131 | ### 7. 小程序 132 | 133 | `com.weixin.pay.util.WXUtils` : 134 | 135 | |方法名 | 说明 | 136 | |--------|--------| 137 | |getMiniBaseUserInfo| 获取小程序静默登录返回信息 | 138 | |getWxMiniQRImg| 生成带参数的小程序二维码[] | 139 | 140 | ## 微信支付调用示例 141 | 142 | 具体示例及文章可以查看作者的sdk文档,地址如下: 143 | 144 | 文档地址:https://yclimb.gitbook.io/wxpay 145 | 146 | 或者在本文末扫码关注作者微信公众号,加作者微信&加入讨论群与大家一起讨论。 147 | 148 | `微信公众号网页授权` : 149 | ```$xslt 150 | https://yclimb.gitbook.io/wxpay/pay/authorize 151 | ``` 152 | 153 | `统一下单接口` : 154 | 155 | ```$xslt 156 | public Map saveWxPayUnifiedOrder(Payment payment, User user) throws Exception { 157 | if (payment == null) { 158 | return null; 159 | } 160 | if (user == null) { 161 | return null; 162 | } 163 | 164 | // 1.调用微信统一下单接口 165 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 166 | Map resultMap = wxPay.unifiedOrder(...); 167 | 168 | // 1.1.记录付款流水 169 | ... 170 | 171 | // 下单失败,进行处理 172 | if (WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RETURN_CODE)) || 173 | WXPayConstants.FAIL.equals(resultMap.get(WXPayConstants.RESULT_CODE))) { 174 | 175 | // 处理结果返回,无需继续执行 176 | resultMap.put(WXPayConstants.RESULT_CODE, WXPayConstants.FAIL); 177 | resultMap.put(WXPayConstants.ERR_CODE_DES, resultMap.get(WXPayConstants.RETURN_MSG)); 178 | return resultMap; 179 | } 180 | 181 | // 1.2.获取prepay_id、nonce_str 182 | String prepay_id = resultMap.get("prepay_id"); 183 | String nonce_str = resultMap.get("nonce_str"); 184 | 185 | // 2.根据微信统一下单接口返回数据组装微信支付参数,返回结果 186 | return wxPay.chooseWXPayMap(prepay_id, nonce_str); 187 | } 188 | ``` 189 | 190 | `支付结果通知` : 191 | ```$xslt 192 | https://yclimb.gitbook.io/wxpay/pay/wxnotify 193 | ``` 194 | 195 | `查询订单和关闭订单` : 196 | ```$xslt 197 | https://yclimb.gitbook.io/wxpay/pay/orderquery 198 | ``` 199 | 200 | `申请退款、退款回调接口、查询退款` : 201 | ```$xslt 202 | https://yclimb.gitbook.io/wxpay/refund/refund 203 | ``` 204 | 205 | 206 | 207 | 基础调用方式如上所述,统一返回值为 `Map`,详细信息见实体类,文档会实时更新,尽情期待!!! 208 | 209 | 210 | 关注作者微信公众号,点击下方`讨论群`,扫码即可加入`微信支付讨论群`与小伙伴一起探讨哦~ 211 | --- 212 | 213 | ![关注我的公众号](https://img-blog.csdn.net/20180130111432962?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvWUNsaW1i/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 214 | 215 | --- 216 | 217 | 218 | 219 | ## License 220 | BSD 221 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.weixin.pay 8 | wxpay-sdk 9 | 0.0.1 10 | wxpay-sdk 11 | wxpay sdk 12 | https://github.com/YClimb/wxpay-sdk 13 | 14 | 15 | 16 | 17 | 18 | The BSD 3-Clause License 19 | https://opensource.org/licenses/BSD-3-Clause 20 | repo 21 | 22 | 23 | 24 | 25 | 26 | wxpay 27 | yclimb@qq.com 28 | https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=11_1 29 | 30 | 31 | 32 | 33 | utf-8 34 | utf-8 35 | 36 | 37 | 38 | 39 | 40 | org.apache.httpcomponents 41 | httpclient 42 | 4.5.3 43 | 44 | 45 | 46 | org.slf4j 47 | slf4j-api 48 | 1.7.21 49 | 50 | 51 | 52 | org.slf4j 53 | slf4j-simple 54 | 1.7.21 55 | 56 | 57 | 58 | org.bouncycastle 59 | bcprov-jdk16 60 | 1.46 61 | 62 | 63 | 64 | com.alibaba 65 | fastjson 66 | 1.2.31 67 | 68 | 69 | 70 | javax.servlet 71 | javax.servlet-api 72 | 3.1.0 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 1.16.10 79 | 80 | 81 | 82 | org.apache.commons 83 | commons-lang3 84 | 3.1 85 | 86 | 87 | 88 | 89 | org.springframework 90 | spring-web 91 | 4.3.14.RELEASE 92 | 93 | 94 | 95 | 96 | org.springframework.data 97 | spring-data-redis 98 | 2.0.3.RELEASE 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-compiler-plugin 108 | 109 | 8 110 | 8 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | release 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-source-plugin 125 | 3.0.1 126 | 127 | 128 | package 129 | 130 | jar-no-fork 131 | 132 | 133 | 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-javadoc-plugin 139 | 2.10.4 140 | 141 | 142 | package 143 | 144 | jar 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPay.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | import com.weixin.pay.constants.WXPayConstants.SignType; 5 | import com.weixin.pay.util.DateTimeUtil; 6 | import com.weixin.pay.util.WXPayUtil; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Date; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class WXPay { 15 | 16 | private WXPayConfig config; 17 | private SignType signType; 18 | private boolean autoReport; 19 | private boolean useSandbox; 20 | private String notifyUrl; 21 | private WXPayRequest wxPayRequest; 22 | 23 | public WXPay(final WXPayConfig config) throws Exception { 24 | this(config, null, true, false); 25 | // this(config, null, true, true); 26 | } 27 | 28 | public WXPay(final WXPayConfig config, final boolean autoReport) throws Exception { 29 | this(config, null, autoReport, false); 30 | // this(config, null, autoReport, true); 31 | } 32 | 33 | 34 | public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception { 35 | this(config, null, autoReport, useSandbox); 36 | } 37 | 38 | public WXPay(final WXPayConfig config, final String notifyUrl) throws Exception { 39 | this(config, notifyUrl, true, false); 40 | } 41 | 42 | public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport) throws Exception { 43 | this(config, notifyUrl, autoReport, false); 44 | } 45 | 46 | public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception { 47 | this.config = config; 48 | this.notifyUrl = notifyUrl; 49 | this.autoReport = autoReport; 50 | this.useSandbox = useSandbox; 51 | if (useSandbox) { 52 | this.signType = SignType.MD5; // 沙箱环境 53 | } else { 54 | this.signType = SignType.MD5; // 此处原来不是MD5!!! 55 | } 56 | this.wxPayRequest = new WXPayRequest(config); 57 | } 58 | 59 | private void checkWXPayConfig() throws Exception { 60 | if (this.config == null) { 61 | throw new Exception("config is null"); 62 | } 63 | if (this.config.getAppID() == null || this.config.getAppID().trim().length() == 0) { 64 | throw new Exception("appid in config is empty"); 65 | } 66 | if (this.config.getMchID() == null || this.config.getMchID().trim().length() == 0) { 67 | throw new Exception("appid in config is empty"); 68 | } 69 | if (this.config.getCertStream() == null) { 70 | throw new Exception("cert stream in config is empty"); 71 | } 72 | if (this.config.getWXPayDomain() == null) { 73 | throw new Exception("config.getWXPayDomain() is null"); 74 | } 75 | 76 | if (this.config.getHttpConnectTimeoutMs() < 10) { 77 | throw new Exception("http connect timeout is too small"); 78 | } 79 | if (this.config.getHttpReadTimeoutMs() < 10) { 80 | throw new Exception("http read timeout is too small"); 81 | } 82 | 83 | } 84 | 85 | /** 86 | * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign
87 | * 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口 88 | * 89 | * @param reqData r 90 | * @return map 91 | * @throws Exception e 92 | */ 93 | public Map fillRequestData(Map reqData) throws Exception { 94 | reqData.put("appid", config.getAppID()); 95 | reqData.put("mch_id", config.getMchID()); 96 | reqData.put("nonce_str", WXPayUtil.generateNonceStr()); 97 | if (SignType.MD5.equals(this.signType)) { 98 | reqData.put("sign_type", WXPayConstants.MD5); 99 | } else if (SignType.HMACSHA256.equals(this.signType)) { 100 | reqData.put("sign_type", WXPayConstants.HMACSHA256); 101 | } 102 | reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); 103 | return reqData; 104 | } 105 | 106 | /** 107 | * 向 Map 中添加 appid、mch_id、nonce_str、sign
108 | * 该函数适用于商户适用于适用于红包查询接口,不适用于统一下单接口 109 | * 110 | * @param reqData r 111 | * @return map 112 | * @throws Exception e 113 | */ 114 | public Map fillRequestDataNotType(Map reqData) throws Exception { 115 | reqData.put("appid", config.getAppID()); 116 | reqData.put("mch_id", config.getMchID()); 117 | reqData.put("nonce_str", WXPayUtil.generateNonceStr()); 118 | reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); 119 | return reqData; 120 | } 121 | 122 | /** 123 | * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。 124 | * 125 | * @param reqData 向wxpay post的请求数据 126 | * @return 签名是否有效 127 | * @throws Exception e 128 | */ 129 | public boolean isResponseSignatureValid(Map reqData) throws Exception { 130 | // 返回数据的签名方式和请求中给定的签名方式是一致的 131 | return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), this.signType); 132 | } 133 | 134 | /** 135 | * 判断支付结果通知中的sign是否有效 136 | * 137 | * @param reqData 向wxpay post的请求数据 138 | * @return 签名是否有效 139 | * @throws Exception 140 | */ 141 | public boolean isPayResultNotifySignatureValid(Map reqData) throws Exception { 142 | String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE); 143 | SignType signType; 144 | if (signTypeInData == null) { 145 | signType = SignType.MD5; 146 | } else { 147 | signTypeInData = signTypeInData.trim(); 148 | if (signTypeInData.length() == 0) { 149 | signType = SignType.MD5; 150 | } else if (WXPayConstants.MD5.equals(signTypeInData)) { 151 | signType = SignType.MD5; 152 | } else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) { 153 | signType = SignType.HMACSHA256; 154 | } else { 155 | throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData)); 156 | } 157 | } 158 | return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), signType); 159 | } 160 | 161 | 162 | /** 163 | * 不需要证书的请求 164 | * 165 | * @param urlSuffix String 166 | * @param reqData 向wxpay post的请求数据 167 | * @param connectTimeoutMs 超时时间,单位是毫秒 168 | * @param readTimeoutMs 超时时间,单位是毫秒 169 | * @return API返回数据 170 | * @throws Exception 171 | */ 172 | public String requestWithoutCert(String urlSuffix, Map reqData, 173 | int connectTimeoutMs, int readTimeoutMs) throws Exception { 174 | String msgUUID = reqData.get("nonce_str"); 175 | String reqBody = WXPayUtil.mapToXml(reqData); 176 | 177 | String resp = this.wxPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, autoReport); 178 | return resp; 179 | } 180 | 181 | 182 | /** 183 | * 需要证书的请求 184 | * 185 | * @param urlSuffix String 186 | * @param reqData 向wxpay post的请求数据 Map 187 | * @param connectTimeoutMs 超时时间,单位是毫秒 188 | * @param readTimeoutMs 超时时间,单位是毫秒 189 | * @return API返回数据 190 | * @throws Exception 191 | */ 192 | public String requestWithCert(String urlSuffix, Map reqData, 193 | int connectTimeoutMs, int readTimeoutMs) throws Exception { 194 | String msgUUID = reqData.get("nonce_str"); 195 | String reqBody = WXPayUtil.mapToXml(reqData); 196 | 197 | String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport); 198 | return resp; 199 | } 200 | 201 | /** 202 | * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 203 | * 204 | * @param xmlStr API返回的XML格式数据 205 | * @return Map类型数据 206 | * @throws Exception e 207 | */ 208 | public Map processResponseXml(String xmlStr) throws Exception { 209 | return processResponseXml(xmlStr, true); 210 | } 211 | 212 | /** 213 | * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 214 | * 215 | * @param xmlStr API返回的XML格式数据 216 | * @param isFlag 是否对返回的数据进行sign校验 217 | * @return Map类型数据 218 | * @throws Exception e 219 | */ 220 | public Map processResponseXml(String xmlStr, boolean isFlag) throws Exception { 221 | String RETURN_CODE = "return_code"; 222 | String return_code; 223 | Map respData = WXPayUtil.xmlToMap(xmlStr); 224 | if (respData.containsKey(RETURN_CODE)) { 225 | return_code = respData.get(RETURN_CODE); 226 | } else { 227 | throw new Exception(String.format("No `return_code` in XML: %s", xmlStr)); 228 | } 229 | 230 | if (return_code.equals(WXPayConstants.FAIL)) { 231 | return respData; 232 | } else if (return_code.equals(WXPayConstants.SUCCESS)) { 233 | // 如果isFlag为false,则不需要进行sign校验 234 | if (!isFlag) { 235 | return respData; 236 | } 237 | if (this.isResponseSignatureValid(respData)) { 238 | return respData; 239 | } else { 240 | throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr)); 241 | } 242 | } else { 243 | throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); 244 | } 245 | } 246 | 247 | /** 248 | * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 249 | * 250 | * @param xmlStr API返回的XML格式数据 251 | * @return Map类型数据 252 | * @throws Exception 253 | */ 254 | public Map sendRedPackProcessResponseXml(String xmlStr) throws Exception { 255 | String RETURN_CODE = "return_code"; 256 | String return_code; 257 | Map respData = WXPayUtil.xmlToMap(xmlStr); 258 | if (respData.containsKey(RETURN_CODE)) { 259 | return_code = respData.get(RETURN_CODE); 260 | } else { 261 | throw new Exception(String.format("No `return_code` in XML: %s", xmlStr)); 262 | } 263 | 264 | if (StringUtils.isNotBlank(return_code)) { 265 | return respData; 266 | } else { 267 | throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); 268 | } 269 | } 270 | 271 | /** 272 | * 作用:提交刷卡支付
273 | * 场景:刷卡支付 274 | * 275 | * @param reqData 向wxpay post的请求数据 276 | * @return API返回数据 277 | * @throws Exception 278 | */ 279 | public Map microPay(Map reqData) throws Exception { 280 | return this.microPay(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 281 | } 282 | 283 | 284 | /** 285 | * 作用:提交刷卡支付
286 | * 场景:刷卡支付 287 | * 288 | * @param reqData 向wxpay post的请求数据 289 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 290 | * @param readTimeoutMs 读超时时间,单位是毫秒 291 | * @return API返回数据 292 | * @throws Exception 293 | */ 294 | public Map microPay(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 295 | String url; 296 | if (this.useSandbox) { 297 | url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX; 298 | } else { 299 | url = WXPayConstants.MICROPAY_URL_SUFFIX; 300 | } 301 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 302 | return this.processResponseXml(respXml); 303 | } 304 | 305 | /** 306 | * 提交刷卡支付,针对软POS,尽可能做成功 307 | * 内置重试机制,最多60s 308 | * 309 | * @param reqData 310 | * @return 311 | * @throws Exception 312 | */ 313 | public Map microPayWithPos(Map reqData) throws Exception { 314 | return this.microPayWithPos(reqData, this.config.getHttpConnectTimeoutMs()); 315 | } 316 | 317 | /** 318 | * 提交刷卡支付,针对软POS,尽可能做成功 319 | * 内置重试机制,最多60s 320 | * 321 | * @param reqData 322 | * @param connectTimeoutMs 323 | * @return 324 | * @throws Exception 325 | */ 326 | public Map microPayWithPos(Map reqData, int connectTimeoutMs) throws Exception { 327 | int remainingTimeMs = 60 * 1000; 328 | long startTimestampMs = 0; 329 | Map lastResult = null; 330 | Exception lastException = null; 331 | 332 | while (true) { 333 | startTimestampMs = WXPayUtil.getCurrentTimestampMs(); 334 | int readTimeoutMs = remainingTimeMs - connectTimeoutMs; 335 | if (readTimeoutMs > 1000) { 336 | try { 337 | lastResult = this.microPay(reqData, connectTimeoutMs, readTimeoutMs); 338 | String returnCode = lastResult.get("return_code"); 339 | if (returnCode.equals("SUCCESS")) { 340 | String resultCode = lastResult.get("result_code"); 341 | String errCode = lastResult.get("err_code"); 342 | if (resultCode.equals("SUCCESS")) { 343 | break; 344 | } else { 345 | // 看错误码,若支付结果未知,则重试提交刷卡支付 346 | if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) { 347 | remainingTimeMs = remainingTimeMs - (int) (WXPayUtil.getCurrentTimestampMs() - startTimestampMs); 348 | if (remainingTimeMs <= 100) { 349 | break; 350 | } else { 351 | WXPayUtil.getLogger().info("microPayWithPos: try micropay again"); 352 | if (remainingTimeMs > 5 * 1000) { 353 | Thread.sleep(5 * 1000); 354 | } else { 355 | Thread.sleep(1 * 1000); 356 | } 357 | continue; 358 | } 359 | } else { 360 | break; 361 | } 362 | } 363 | } else { 364 | break; 365 | } 366 | } catch (Exception ex) { 367 | lastResult = null; 368 | lastException = ex; 369 | } 370 | } else { 371 | break; 372 | } 373 | } 374 | 375 | if (lastResult == null) { 376 | throw lastException; 377 | } else { 378 | return lastResult; 379 | } 380 | } 381 | 382 | /** 383 | * 作用:商户平台-现金红包-发放普通红包
384 | * 场景:现金红包发放后会以公众号消息的形式触达用户 385 | * 其他:需要证书 386 | * 387 | * @param reqData 向wxpay post的请求数据 388 | * @return API返回数据 389 | * @throws Exception e 390 | */ 391 | public Map sendRedPack(Map reqData) throws Exception { 392 | String url; 393 | if (this.useSandbox) { 394 | url = WXPayConstants.SANDBOX_SENDREDPACK_URL_SUFFIX; 395 | } else { 396 | url = WXPayConstants.SENDREDPACK_URL_SUFFIX; 397 | } 398 | String respXml = this.requestWithCert(url, this.redPackRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 399 | return this.sendRedPackProcessResponseXml(respXml); 400 | } 401 | 402 | public Map redPackRequestData(Map reqData) throws Exception { 403 | reqData.put("wxappid", config.getAppID()); 404 | reqData.put("mch_id", config.getMchID()); 405 | reqData.put("nonce_str", WXPayUtil.generateUUID()); 406 | reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); 407 | return reqData; 408 | } 409 | 410 | /** 411 | * 作用:商户平台-现金红包-查询红包记录
412 | * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 413 | * 其他:需要证书 414 | * 415 | * @param reqData 向wxpay post的请求数据 416 | * @return API返回数据 417 | * @throws Exception e 418 | */ 419 | public Map getRedPackInfo(Map reqData) throws Exception { 420 | String url; 421 | if (this.useSandbox) { 422 | url = WXPayConstants.SANDBOX_GETHBINFO_URL_SUFFIX; 423 | } else { 424 | url = WXPayConstants.GETHBINFO_URL_SUFFIX; 425 | } 426 | String respXml = this.requestWithCert(url, this.fillRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 427 | return this.processResponseXml(respXml); 428 | } 429 | 430 | /** 431 | * 作用:商户平台-代金券或立减优惠-发放代金券
432 | * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 433 | * 注意:通过接口发放的代金券不会进入微信卡包 434 | * 其他:需要证书 435 | * 436 | * @param reqData 向wxpay post的请求数据 437 | * @return API返回数据 438 | * @throws Exception e 439 | */ 440 | public Map sendCoupon(Map reqData) throws Exception { 441 | String url; 442 | if (this.useSandbox) { 443 | url = WXPayConstants.SANDBOX_SEND_COUPON_URL_SUFFIX; 444 | } else { 445 | url = WXPayConstants.SEND_COUPON_URL_SUFFIX; 446 | } 447 | String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 448 | return this.processResponseXml(respXml, false); 449 | } 450 | 451 | /** 452 | * 作用:商户平台-代金券或立减优惠-查询代金券信息
453 | * 场景:查询代金券信息 454 | * 455 | * @param reqData 向wxpay post的请求数据 456 | * @return API返回数据 457 | * @throws Exception e 458 | */ 459 | public Map queryCouponsInfo(Map reqData) throws Exception { 460 | String url; 461 | if (this.useSandbox) { 462 | url = WXPayConstants.SANDBOX_QUERYCOUPONSINFO_URL_SUFFIX; 463 | } else { 464 | url = WXPayConstants.QUERYCOUPONSINFO_URL_SUFFIX; 465 | } 466 | String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 467 | return this.processResponseXml(respXml, false); 468 | } 469 | 470 | /** 471 | * 作用:商户平台-代金券或立减优惠-查询代金券批次
472 | * 场景:查询代金券批次信息 473 | * 474 | * @param reqData 向wxpay post的请求数据 475 | * @return API返回数据 476 | * @throws Exception e 477 | */ 478 | public Map queryCouponStock(Map reqData) throws Exception { 479 | String url; 480 | if (this.useSandbox) { 481 | url = WXPayConstants.SANDBOX_QUERY_COUPON_STOCK_URL_SUFFIX; 482 | } else { 483 | url = WXPayConstants.QUERY_COUPON_STOCK_URL_SUFFIX; 484 | } 485 | String respXml = this.requestWithCert(url, this.fillRequestDataNotType(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 486 | return this.processResponseXml(respXml, false); 487 | } 488 | 489 | /** 490 | * 作用:商户平台-企业付款-企业向微信用户个人付款
491 | * 场景:企业付款到零钱资金使用商户号余额资金。 492 | * 其他:需要证书 493 | * 494 | * @param reqData 向wxpay post的请求数据 495 | * @return API返回数据 496 | * @throws Exception e 497 | */ 498 | public Map transfers(Map reqData) throws Exception { 499 | String url; 500 | if (this.useSandbox) { 501 | url = WXPayConstants.SANDBOX_TRANSFERS_URL_SUFFIX; 502 | } else { 503 | url = WXPayConstants.TRANSFERS_URL_SUFFIX; 504 | } 505 | String respXml = this.requestWithCert(url, this.transfersRequestData(reqData), config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 506 | return this.sendRedPackProcessResponseXml(respXml); 507 | } 508 | 509 | public Map transfersRequestData(Map reqData) throws Exception { 510 | reqData.put("mch_appid", config.getAppID()); 511 | reqData.put("mchid", config.getMchID()); 512 | reqData.put("nonce_str", WXPayUtil.generateUUID()); 513 | reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); 514 | return reqData; 515 | } 516 | 517 | 518 | /** 519 | * 作用:统一下单
520 | * 场景:公共号支付、扫码支付、APP支付 521 | * 522 | * @param reqData 向wxpay post的请求数据 523 | * @return API返回数据 524 | * @throws Exception 525 | */ 526 | public Map unifiedOrder(Map reqData) throws Exception { 527 | return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 528 | } 529 | 530 | 531 | /** 532 | * 作用:统一下单
533 | * 场景:公共号支付、扫码支付、APP支付 534 | * 535 | * @param reqData 向wxpay post的请求数据 536 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 537 | * @param readTimeoutMs 读超时时间,单位是毫秒 538 | * @return API返回数据 539 | * @throws Exception 540 | */ 541 | public Map unifiedOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 542 | String url; 543 | if (this.useSandbox) { 544 | url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX; 545 | } else { 546 | url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX; 547 | } 548 | if (this.notifyUrl != null) { 549 | reqData.put("notify_url", this.notifyUrl); 550 | } 551 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 552 | return this.processResponseXml(respXml); 553 | } 554 | 555 | 556 | /** 557 | * 作用:查询订单
558 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 559 | * 560 | * @param reqData 向wxpay post的请求数据 561 | * @return API返回数据 562 | * @throws Exception 563 | */ 564 | public Map orderQuery(Map reqData) throws Exception { 565 | return this.orderQuery(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 566 | } 567 | 568 | 569 | /** 570 | * 作用:查询订单
571 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 572 | * 573 | * @param reqData 向wxpay post的请求数据 int 574 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 575 | * @param readTimeoutMs 读超时时间,单位是毫秒 576 | * @return API返回数据 577 | * @throws Exception 578 | */ 579 | public Map orderQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 580 | String url; 581 | if (this.useSandbox) { 582 | url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX; 583 | } else { 584 | url = WXPayConstants.ORDERQUERY_URL_SUFFIX; 585 | } 586 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 587 | return this.processResponseXml(respXml); 588 | } 589 | 590 | 591 | /** 592 | * 作用:撤销订单
593 | * 场景:刷卡支付 594 | * 595 | * @param reqData 向wxpay post的请求数据 596 | * @return API返回数据 597 | * @throws Exception 598 | */ 599 | public Map reverse(Map reqData) throws Exception { 600 | return this.reverse(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 601 | } 602 | 603 | 604 | /** 605 | * 作用:撤销订单
606 | * 场景:刷卡支付
607 | * 其他:需要证书 608 | * 609 | * @param reqData 向wxpay post的请求数据 610 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 611 | * @param readTimeoutMs 读超时时间,单位是毫秒 612 | * @return API返回数据 613 | * @throws Exception 614 | */ 615 | public Map reverse(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 616 | String url; 617 | if (this.useSandbox) { 618 | url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX; 619 | } else { 620 | url = WXPayConstants.REVERSE_URL_SUFFIX; 621 | } 622 | String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 623 | return this.processResponseXml(respXml); 624 | } 625 | 626 | 627 | /** 628 | * 作用:关闭订单
629 | * 场景:公共号支付、扫码支付、APP支付 630 | * 631 | * @param reqData 向wxpay post的请求数据 632 | * @return API返回数据 633 | * @throws Exception 634 | */ 635 | public Map closeOrder(Map reqData) throws Exception { 636 | return this.closeOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 637 | } 638 | 639 | 640 | /** 641 | * 作用:关闭订单
642 | * 场景:公共号支付、扫码支付、APP支付 643 | * 644 | * @param reqData 向wxpay post的请求数据 645 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 646 | * @param readTimeoutMs 读超时时间,单位是毫秒 647 | * @return API返回数据 648 | * @throws Exception 649 | */ 650 | public Map closeOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 651 | String url; 652 | if (this.useSandbox) { 653 | url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX; 654 | } else { 655 | url = WXPayConstants.CLOSEORDER_URL_SUFFIX; 656 | } 657 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 658 | return this.processResponseXml(respXml); 659 | } 660 | 661 | 662 | /** 663 | * 作用:申请退款
664 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 665 | * 666 | * @param reqData 向wxpay post的请求数据 667 | * @return API返回数据 668 | * @throws Exception 669 | */ 670 | public Map refund(Map reqData) throws Exception { 671 | return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 672 | } 673 | 674 | 675 | /** 676 | * 作用:申请退款
677 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付
678 | * 其他:需要证书 679 | * 680 | * @param reqData 向wxpay post的请求数据 681 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 682 | * @param readTimeoutMs 读超时时间,单位是毫秒 683 | * @return API返回数据 684 | * @throws Exception 685 | */ 686 | public Map refund(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 687 | String url; 688 | if (this.useSandbox) { 689 | url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX; 690 | } else { 691 | url = WXPayConstants.REFUND_URL_SUFFIX; 692 | } 693 | String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 694 | return this.processResponseXml(respXml); 695 | } 696 | 697 | 698 | /** 699 | * 作用:退款查询
700 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 701 | * 702 | * @param reqData 向wxpay post的请求数据 703 | * @return API返回数据 704 | * @throws Exception 705 | */ 706 | public Map refundQuery(Map reqData) throws Exception { 707 | return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 708 | } 709 | 710 | 711 | /** 712 | * 作用:退款查询
713 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 714 | * 715 | * @param reqData 向wxpay post的请求数据 716 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 717 | * @param readTimeoutMs 读超时时间,单位是毫秒 718 | * @return API返回数据 719 | * @throws Exception 720 | */ 721 | public Map refundQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 722 | String url; 723 | if (this.useSandbox) { 724 | url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX; 725 | } else { 726 | url = WXPayConstants.REFUNDQUERY_URL_SUFFIX; 727 | } 728 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 729 | return this.processResponseXml(respXml); 730 | } 731 | 732 | 733 | /** 734 | * 作用:对账单下载(成功时返回对账单数据,失败时返回XML格式数据)
735 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 736 | * 737 | * @param reqData 向wxpay post的请求数据 738 | * @return API返回数据 739 | * @throws Exception 740 | */ 741 | public Map downloadBill(Map reqData) throws Exception { 742 | return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 743 | } 744 | 745 | 746 | /** 747 | * 作用:对账单下载
748 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付
749 | * 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data, 750 | * 其中return_code为`SUCCESS`,data为对账单数据。 751 | * 752 | * @param reqData 向wxpay post的请求数据 753 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 754 | * @param readTimeoutMs 读超时时间,单位是毫秒 755 | * @return 经过封装的API返回数据 756 | * @throws Exception 757 | */ 758 | public Map downloadBill(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 759 | String url; 760 | if (this.useSandbox) { 761 | url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX; 762 | } else { 763 | url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX; 764 | } 765 | String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim(); 766 | Map ret; 767 | // 出现错误,返回XML数据 768 | if (respStr.indexOf("<") == 0) { 769 | ret = WXPayUtil.xmlToMap(respStr); 770 | } else { 771 | // 正常返回csv数据 772 | ret = new HashMap(); 773 | ret.put("return_code", WXPayConstants.SUCCESS); 774 | ret.put("return_msg", "ok"); 775 | ret.put("data", respStr); 776 | } 777 | return ret; 778 | } 779 | 780 | 781 | /** 782 | * 作用:交易保障
783 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 784 | * 785 | * @param reqData 向wxpay post的请求数据 786 | * @return API返回数据 787 | * @throws Exception 788 | */ 789 | public Map report(Map reqData) throws Exception { 790 | return this.report(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 791 | } 792 | 793 | 794 | /** 795 | * 作用:交易保障
796 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 797 | * 798 | * @param reqData 向wxpay post的请求数据 799 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 800 | * @param readTimeoutMs 读超时时间,单位是毫秒 801 | * @return API返回数据 802 | * @throws Exception 803 | */ 804 | public Map report(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 805 | String url; 806 | if (this.useSandbox) { 807 | url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX; 808 | } else { 809 | url = WXPayConstants.REPORT_URL_SUFFIX; 810 | } 811 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 812 | return WXPayUtil.xmlToMap(respXml); 813 | } 814 | 815 | 816 | /** 817 | * 作用:转换短链接
818 | * 场景:刷卡支付、扫码支付 819 | * 820 | * @param reqData 向wxpay post的请求数据 821 | * @return API返回数据 822 | * @throws Exception 823 | */ 824 | public Map shortUrl(Map reqData) throws Exception { 825 | return this.shortUrl(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 826 | } 827 | 828 | 829 | /** 830 | * 作用:转换短链接
831 | * 场景:刷卡支付、扫码支付 832 | * 833 | * @param reqData 向wxpay post的请求数据 834 | * @return API返回数据 835 | * @throws Exception 836 | */ 837 | public Map shortUrl(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 838 | String url; 839 | if (this.useSandbox) { 840 | url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX; 841 | } else { 842 | url = WXPayConstants.SHORTURL_URL_SUFFIX; 843 | } 844 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 845 | return this.processResponseXml(respXml); 846 | } 847 | 848 | 849 | /** 850 | * 作用:授权码查询OPENID接口
851 | * 场景:刷卡支付 852 | * 853 | * @param reqData 向wxpay post的请求数据 854 | * @return API返回数据 855 | * @throws Exception 856 | */ 857 | public Map authCodeToOpenid(Map reqData) throws Exception { 858 | return this.authCodeToOpenid(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); 859 | } 860 | 861 | 862 | /** 863 | * 作用:授权码查询OPENID接口
864 | * 场景:刷卡支付 865 | * 866 | * @param reqData 向wxpay post的请求数据 867 | * @param connectTimeoutMs 连接超时时间,单位是毫秒 868 | * @param readTimeoutMs 读超时时间,单位是毫秒 869 | * @return API返回数据 870 | * @throws Exception 871 | */ 872 | public Map authCodeToOpenid(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { 873 | String url; 874 | if (this.useSandbox) { 875 | url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX; 876 | } else { 877 | url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX; 878 | } 879 | String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); 880 | return this.processResponseXml(respXml); 881 | } 882 | 883 | /** 884 | * 作用:统一下单
885 | * 场景:商户在小程序中先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易后调起支付。 886 | * 接口链接:URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder 887 | * 是否需要证书:否 888 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 889 | * 890 | * @param notify_url 公众号用户openid 891 | * @param body 商品简单描述,该字段请按照规范传递,例:腾讯充值中心-QQ会员充值 892 | * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 893 | * @param total_fee 订单总金额,传入参数单位为:元 894 | * @param spbill_create_ip APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP 895 | * @param goods_tag 订单优惠标记,用于区分订单是否可以享受优惠 896 | * @param detail 商品详情 ,单品优惠活动该字段必传 897 | * @param time_start 订单生成时间,格式为yyyyMMddHHmmss 898 | * @param time_expire 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010 899 | * @return API返回数据 900 | * @throws Exception e 901 | */ 902 | public Map unifiedOrder(String notify_url, String openid, String body, String out_trade_no, String total_fee, 903 | String spbill_create_ip, String goods_tag, String detail, 904 | Date time_start, Date time_expire) throws Exception { 905 | 906 | /** 构造请求参数数据 **/ 907 | Map data = new HashMap<>(); 908 | 909 | // 字段名 变量名 必填 类型 示例值 描述 910 | // 标价币种 fee_type 否 String(16) CNY 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型 911 | data.put("fee_type", WXPayConstants.FEE_TYPE_CNY); 912 | // 通知地址 notify_url 是 String(256) http://www.weixin.qq.com/wxpay/pay.php 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 913 | data.put("notify_url", notify_url); 914 | // 交易类型 trade_type 是 String(16) JSAPI 小程序取值如下:JSAPI,详细说明见参数规定 915 | data.put("trade_type", WXPayConstants.TRADE_TYPE); 916 | // 用户标识 openid 否 String(128) oUpF8uMuAJO_M2pxb1Q9zNjWeS6o trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。openid如何获取,可参考【获取openid】。 917 | data.put("openid", openid); 918 | // 商品描述 body 是 String(128) 腾讯充值中心-QQ会员充值 商品简单描述,该字段请按照规范传递,具体请见参数规定 919 | data.put("body", body); 920 | // 商户订单号 out_trade_no 是 String(32) 20150806125346 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 921 | data.put("out_trade_no", out_trade_no); 922 | // 标价金额 total_fee 是 Int 88 订单总金额,单位为分,详见支付金额 923 | // 默认单位为分,系统是元,所以需要*100 924 | data.put("total_fee", String.valueOf(new BigDecimal(total_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 925 | // 终端IP spbill_create_ip 是 String(16) 123.12.12.123 APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 926 | data.put("spbill_create_ip", spbill_create_ip); 927 | 928 | /** 以下参数为非必填参数 **/ 929 | // 订单优惠标记 goods_tag 否 String(32) WXG 订单优惠标记,使用代金券或立减优惠功能时需要的参数,说明详见代金券或立减优惠 930 | if (StringUtils.isNotBlank(goods_tag)) { 931 | data.put("goods_tag", goods_tag); 932 | } 933 | // 商品详情 detail 否 String(6000) 商品详细描述,对于使用单品优惠的商户,改字段必须按照规范上传,详见“单品优惠参数说明” 934 | if (StringUtils.isNotBlank(detail)) { 935 | data.put("detail", detail); 936 | // 接口版本号 新增字段,接口版本号,区分原接口,默认填写1.0。入参新增version后,则支付通知接口也将返回单品优惠信息字段promotion_detail,请确保支付通知的签名验证能通过。 937 | data.put("version", "1.0"); 938 | } 939 | // 设备号 device_info 否 String(32) 013467007045764 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" 940 | data.put("device_info", "WEB"); 941 | 942 | // 交易起始时间 time_start 否 String(14) 20091225091010 订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 943 | data.put("time_start", DateTimeUtil.getTimeShortString(time_start)); 944 | // 交易结束时间 time_expire 否 String(14) 20091227091010 订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则,建议:最短失效时间间隔大于1分钟 945 | data.put("time_expire", DateTimeUtil.getTimeShortString(time_expire)); 946 | /*// 商品ID product_id 否 String(32) 12235413214070356458058 trade_type=NATIVE时(即扫码支付),此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 947 | data.put("product_id", null); 948 | // 指定支付方式 limit_pay 否 String(32) no_credit 上传此参数no_credit--可限制用户不能使用信用卡支付 949 | data.put("limit_pay", null); 950 | // 附加数据 attach 否 String(127) 深圳分店 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 951 | data.put("attach", null);*/ 952 | 953 | /** 以下五个参数,在 this.fillRequestData 方法中会自动赋值 **/ 954 | /*// 小程序ID appid 是 String(32) wxd678efh567hg6787 微信分配的小程序ID 955 | data.put("appid", WXPayConstants.APP_ID); 956 | // 商户号 mch_id 是 String(32) 1230000109 微信支付分配的商户号 957 | data.put("mch_id", WXPayConstants.MCH_ID); 958 | // 随机字符串 nonce_str 是 String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,长度要求在32位以内。推荐随机数生成算法 959 | data.put("nonce_str", nonce_str); 960 | // 签名类型 sign_type 否 String(32) MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 961 | data.put("sign_type", WXPayConstants.MD5); 962 | // 签名 sign 是 String(32) C380BEC2BFD727A4B6845133519F3AD6 通过签名算法计算得出的签名值,详见签名生成算法 963 | data.put("sign", sign);*/ 964 | 965 | // 微信统一下单接口请求地址 966 | Map resultMap = this.unifiedOrder(data); 967 | 968 | WXPayUtil.getLogger().info("wxPay.unifiedOrder:" + resultMap); 969 | 970 | return resultMap; 971 | } 972 | 973 | /** 974 | * 作用:生成微信支付所需参数,微信支付二次签名
975 | * 场景:根据微信统一下单接口返回的 prepay_id 生成微信支付所需的参数 976 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6 977 | * 978 | * @param prepay_id 预支付id 979 | * @param nonce_str 随机字符串 980 | * @return 支付方法调用所需参数map 981 | * @throws Exception e 982 | */ 983 | public Map chooseWXPayMap(String prepay_id, String nonce_str) throws Exception { 984 | 985 | // 支付方法调用所需参数map 986 | Map chooseWXPayMap = new HashMap<>(); 987 | chooseWXPayMap.put("appId", config.getAppID()); 988 | chooseWXPayMap.put("timeStamp", String.valueOf(WXPayUtil.getCurrentTimestamp())); 989 | chooseWXPayMap.put("nonceStr", nonce_str); 990 | chooseWXPayMap.put("package", "prepay_id=" + prepay_id); 991 | chooseWXPayMap.put("signType", WXPayConstants.MD5); 992 | 993 | WXPayUtil.getLogger().info("wxPay.chooseWXPayMap:" + chooseWXPayMap.toString()); 994 | 995 | // 生成支付签名 996 | String paySign = WXPayUtil.generateSignature(chooseWXPayMap, config.getKey()); 997 | chooseWXPayMap.put("paySign", paySign); 998 | 999 | WXPayUtil.getLogger().info("wxPay.paySign:" + paySign); 1000 | 1001 | return chooseWXPayMap; 1002 | } 1003 | 1004 | /** 1005 | * 作用:申请退款
1006 | * 场景:当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家, 1007 | * 微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。 1008 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4 1009 | * 1010 | * @param notify_url 回调地址 1011 | * @param transaction_id 微信生成的订单号,在支付通知中有返回 1012 | * @param out_trade_no 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 1013 | * @param out_refund_no 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 1014 | * @param total_fee 订单总金额,传入参数单位为:元 1015 | * @param refund_fee 退款总金额,订单总金额,传入参数单位为:元 1016 | * @param refund_desc 退款原因,若商户传入,会在下发给用户的退款消息中体现退款原因 1017 | * @return API返回数据 1018 | * @throws Exception e 1019 | */ 1020 | public Map refund(String notify_url, String transaction_id, String out_trade_no, String out_refund_no, 1021 | String total_fee, String refund_fee, String refund_desc) throws Exception { 1022 | 1023 | /** 构造请求参数数据 **/ 1024 | Map data = new HashMap<>(); 1025 | 1026 | // 变量名 字段名 必填 类型 示例值 描述 1027 | // 微信订单号 二选一 String(32) 1.21775E+27 微信生成的订单号,在支付通知中有返回 1028 | if (transaction_id != null) { 1029 | data.put("transaction_id", transaction_id); 1030 | } 1031 | // 商户订单号 String(32) 1.21775E+27 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 1032 | data.put("out_trade_no", out_trade_no); 1033 | // 商户退款单号 是 String(64) 1.21775E+27 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。 1034 | data.put("out_refund_no", out_refund_no); 1035 | // 订单金额 是 Int 100 订单总金额,单位为分,只能为整数,详见支付金额 1036 | data.put("total_fee", String.valueOf(new BigDecimal(total_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 1037 | // 退款金额 是 Int 100 退款总金额,订单总金额,单位为分,只能为整数,详见支付金额 1038 | // 默认单位为分,系统是元,所以需要*100 1039 | data.put("refund_fee", String.valueOf(new BigDecimal(refund_fee).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 1040 | // 退款原因 否 String(80) 商品已售完 若商户传入,会在下发给用户的退款消息中体现退款原因 1041 | data.put("refund_desc", refund_desc); 1042 | // 货币种类 否 String(8) CNY 货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY,其他值列表详见货币类型 1043 | data.put("refund_fee_type", WXPayConstants.FEE_TYPE_CNY); 1044 | // 退款结果通知url 否 String(256) https://weixin.qq.com/notify/ 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数,如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。 1045 | data.put("notify_url", notify_url); 1046 | 1047 | /** 以下参数为非必填参数 **/ 1048 | // 退款资金来源 否 String(30) REFUND_SOURCE_RECHARGE_FUNDS 仅针对老资金流商户使用;REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款);REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款 1049 | // data.put("refund_account", null); 1050 | 1051 | 1052 | /** 以下五个参数,在 this.fillRequestData 方法中会自动赋值 **/ 1053 | /*// 小程序ID appid 是 String(32) wxd678efh567hg6787 微信分配的小程序ID 1054 | data.put("appid", WXPayConstants.APP_ID); 1055 | // 商户号 mch_id 是 String(32) 1230000109 微信支付分配的商户号 1056 | data.put("mch_id", WXPayConstants.MCH_ID); 1057 | // 随机字符串 nonce_str 是 String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,长度要求在32位以内。推荐随机数生成算法 1058 | data.put("nonce_str", nonce_str); 1059 | // 签名类型 sign_type 否 String(32) MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 1060 | data.put("sign_type", WXPayConstants.MD5); 1061 | // 签名 sign 是 String(32) C380BEC2BFD727A4B6845133519F3AD6 通过签名算法计算得出的签名值,详见签名生成算法 1062 | data.put("sign", sign);*/ 1063 | 1064 | // 微信退款接口 1065 | Map resultMap = this.refund(data); 1066 | 1067 | WXPayUtil.getLogger().info("wxPay.refund:" + resultMap); 1068 | 1069 | return resultMap; 1070 | } 1071 | 1072 | /** 1073 | * 作用:企业向微信用户个人付款
1074 | * 场景:企业付款为企业提供付款至用户零钱的能力,支持通过API接口付款,或通过微信支付商户平台(pay.weixin.qq.com)网页操作付款。 1075 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 1076 | * 1077 | * @param partner_trade_no 商户订单号 1078 | * @param openid 用户openid 1079 | * @param amount 企业付款金额 1080 | * @param desc 企业付款描述信息 1081 | * @param spbill_create_ip 该IP可传用户端或者服务端的IP 1082 | * @return API返回数据 1083 | * @throws Exception e 1084 | */ 1085 | public Map transfers(String partner_trade_no, String openid, String amount, String desc, String spbill_create_ip) throws Exception { 1086 | 1087 | /** 构造请求参数数据 **/ 1088 | Map data = new HashMap<>(); 1089 | 1090 | // 商户订单号 partner_trade_no 是 10000098201411111234567890 String 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有符号) 1091 | data.put("partner_trade_no", partner_trade_no); 1092 | // 用户openid openid 是 oxTWIuGaIt6gTKsQRLau2M0yL16E String 商户appid下,某用户的openid 1093 | data.put("openid", openid); 1094 | // 校验用户姓名选项 check_name 是 FORCE_CHECK String NO_CHECK:不校验真实姓名,FORCE_CHECK:强校验真实姓名 1095 | data.put("check_name", "NO_CHECK"); 1096 | // 金额 amount 是 10099 int 企业付款金额,单位为分 1097 | data.put("amount", String.valueOf(new BigDecimal(amount).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 1098 | // 企业付款描述信息 desc 是 理赔 String 企业付款操作说明信息。必填。 1099 | data.put("desc", desc); 1100 | // Ip地址 spbill_create_ip 是 192.168.0.1 String(32) 该IP同在商户平台设置的IP白名单中的IP没有关联,该IP可传用户端或者服务端的IP。 1101 | data.put("spbill_create_ip", spbill_create_ip); 1102 | 1103 | /** 以下参数为非必填参数 **/ 1104 | 1105 | /*// 设备号 device_info 否 013467007045764 String(32) 微信支付分配的终端设备号 1106 | data.put("device_info", "xxx"); 1107 | // 收款用户姓名 re_user_name 可选 王小王 String 收款用户真实姓名。(如果check_name设置为FORCE_CHECK,则必填用户真实姓名) 1108 | data.put("re_user_name", "xxx");*/ 1109 | 1110 | // 微信调用接口 1111 | Map resultMap = this.transfers(data); 1112 | 1113 | WXPayUtil.getLogger().info("wxPay.transfers:" + resultMap); 1114 | 1115 | return resultMap; 1116 | } 1117 | 1118 | /** 1119 | * 作用:企业向指定微信用户的openid发放指定金额红包
1120 | * 场景:商户可以通过本平台向微信支付用户发放现金红包。用户领取红包后,资金到达用户微信支付零钱账户,和零钱包的其他资金有一样的使用出口;若用户未领取,资金将会在24小时后退回商户的微信支付账户中。 1121 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3 1122 | * 1123 | * @param mch_billno 商户订单号 1124 | * @param openid 用户openid 1125 | * @param amount 企业付款金额 1126 | * @param act_name 活动名称 1127 | * @param wishing 红包祝福语 1128 | * @param remark 备注 1129 | * @param spbill_create_ip 该IP可传用户端或者服务端的IP 1130 | * @return API返回数据 1131 | * @throws Exception e 1132 | */ 1133 | public Map sendRedPack(String mch_billno, String openid, String amount, String act_name, String wishing, String remark, String spbill_create_ip) throws Exception { 1134 | 1135 | /** 构造请求参数数据 **/ 1136 | Map data = new HashMap<>(); 1137 | 1138 | // 商户订单号 mch_billno 是 10000098201411111234567890 String(28) 商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z)接口根据商户订单号支持重入,如出现超时可再调用。 1139 | data.put("mch_billno", mch_billno); 1140 | // 商户名称 send_name 是 天虹百货 String(32) 红包发送者名称 1141 | data.put("send_name", "商户名称"); 1142 | // 用户openid re_openid 是 oxTWIuGaIt6gTKsQRLau2M0yL16E String(32) 接受红包的用户openid openid为用户在wxappid下的唯一标识(获取openid参见微信公众平台开发者文档:网页授权获取用户基本信息) 1143 | data.put("re_openid", openid); 1144 | // 付款金额 total_amount 是 1000 int 付款金额,单位分 1145 | data.put("total_amount", String.valueOf(new BigDecimal(amount).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 1146 | // 红包发放总人数 total_num 是 1 int 红包发放总人数 total_num=1 1147 | data.put("total_num", "1"); 1148 | // 红包祝福语 wishing 是 感谢您参加猜灯谜活动,祝您元宵节快乐! String(128) 红包祝福语 1149 | data.put("wishing", wishing); 1150 | // Ip地址 client_ip 是 192.168.0.1 String(15) 调用接口的机器Ip地址 1151 | data.put("client_ip", spbill_create_ip); 1152 | // 活动名称 act_name 是 猜灯谜抢红包活动 String(32) 活动名称 1153 | data.put("act_name", act_name); 1154 | // 备注 remark 是 猜越多得越多,快来抢! String(256) 备注信息 1155 | data.put("remark", remark); 1156 | 1157 | /** 以下参数为非必填参数 **/ 1158 | /* 1159 | * 场景id:scene_id 否 PRODUCT_8 String(32) 发放红包使用场景,红包金额大于200或者小于1元时必传 1160 | * PRODUCT_1:商品促销 1161 | * PRODUCT_2:抽奖 1162 | * PRODUCT_3:虚拟物品兑奖 1163 | * PRODUCT_4:企业内部福利 1164 | * PRODUCT_5:渠道分润 1165 | * PRODUCT_6:保险回馈 1166 | * PRODUCT_7:彩票派奖 1167 | * PRODUCT_8:税务刮奖 1168 | */ 1169 | //data.put("scene_id", "PRODUCT_1"); 1170 | /* 1171 | * 活动信息 risk_info 否 posttime%3d123123412%26clientversion%3d234134%26mobile%3d122344545%26deviceid%3dIOS String(128) 1172 | * posttime:用户操作的时间戳 1173 | * mobile:业务系统账号的手机号,国家代码-手机号。不需要+号 1174 | * deviceid :mac 地址或者设备唯一标识 1175 | * clientversion :用户操作的客户端版本 把值为非空的信息用key=value进行拼接,再进行urlencode urlencode(posttime=xx& mobile =xx&deviceid=xx) 1176 | */ 1177 | // 资金授权商户号 consume_mch_id 否 1222000096 String(32) 资金授权商户号 服务商替特约商户发放时使用 1178 | 1179 | /** 以下四个参数,在 this.redPackRequestData 方法中会自动赋值 **/ 1180 | // 商户号 mch_id 是 10000098 String(32) 微信支付分配的商户号 1181 | // 随机字符串 nonce_str 是 5K8264ILTKCH16CQ2502SI8ZNMTM67VS String(32) 随机字符串,不长于32位 1182 | // 签名 sign 是 C380BEC2BFD727A4B6845133519F3AD6 String(32) 详见签名生成算法 1183 | // 公众账号appid wxappid 是 wx8888888888888888 String(32) 微信分配的公众账号ID(企业号corpid即为此appId)。在微信开放平台(open.weixin.qq.com)申请的移动应用appid无法使用该接口。 1184 | 1185 | // 微信调用接口 1186 | Map resultMap = this.sendRedPack(data); 1187 | 1188 | WXPayUtil.getLogger().info("wxPay.sendRedPack:" + resultMap); 1189 | 1190 | return resultMap; 1191 | } 1192 | 1193 | /** 1194 | * 作用:查询红包记录
1195 | * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 1196 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_6&index=5 1197 | * 1198 | * @param mch_billno 商户订单号 1199 | * @return API返回数据 1200 | * @throws Exception e 1201 | */ 1202 | public Map getRedPackInfo(String mch_billno) throws Exception { 1203 | 1204 | /** 构造请求参数数据 **/ 1205 | Map data = new HashMap<>(); 1206 | 1207 | // 商户订单号 mch_billno 是 10000098201411111234567890 String(28) 商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z)接口根据商户订单号支持重入,如出现超时可再调用。 1208 | data.put("mch_billno", mch_billno); 1209 | // 订单类型 bill_type 是 MCHT String(32) MCHT:通过商户订单号获取红包信息。 1210 | data.put("bill_type", "MCHT"); 1211 | 1212 | /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ 1213 | // 商户号 mch_id 是 10000098 String(32) 微信支付分配的商户号 1214 | // 随机字符串 nonce_str 是 5K8264ILTKCH16CQ2502SI8ZNMTM67VS String(32) 随机字符串,不长于32位 1215 | // 签名 sign 是 C380BEC2BFD727A4B6845133519F3AD6 String(32) 详见签名生成算法 1216 | // 公众账号appid appid 是 wx8888888888888888 String(32) 微信分配的公众账号ID(企业号corpid即为此appId)。在微信开放平台(open.weixin.qq.com)申请的移动应用appid无法使用该接口。 1217 | 1218 | // 微信调用接口 1219 | Map resultMap = this.getRedPackInfo(data); 1220 | 1221 | WXPayUtil.getLogger().info("wxPay.getRedPackInfo:" + resultMap); 1222 | 1223 | return resultMap; 1224 | } 1225 | 1226 | /** 1227 | * 作用:商户平台-代金券或立减优惠-发放代金券
1228 | * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 1229 | * 注意:通过接口发放的代金券不会进入微信卡包 1230 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_3&index=4 1231 | * 1232 | * @param coupon_stock_id 代金券批次id 1233 | * @param partner_trade_no 商户单据号 1234 | * @param openid 用户openid 1235 | * @return API返回数据 1236 | * @throws Exception e 1237 | */ 1238 | public Map sendCoupon(String coupon_stock_id, String partner_trade_no, String openid) throws Exception { 1239 | 1240 | /** 构造请求参数数据 **/ 1241 | Map data = new HashMap<>(); 1242 | 1243 | // 代金券批次id coupon_stock_id 是 1757 String 代金券批次id 1244 | data.put("coupon_stock_id", coupon_stock_id); 1245 | // openid记录数 openid_count 是 1 int openid记录数(目前支持num=1) 1246 | data.put("openid_count", "1"); 1247 | // 商户单据号 partner_trade_no 是 1000009820141203515766 String 商户此次发放凭据号(格式:商户id+日期+流水号),商户侧需保持唯一性 1248 | data.put("partner_trade_no", partner_trade_no); 1249 | // 用户openid openid 是 onqOjjrXT-776SpHnfexGm1_P7iE String Openid信息,用户在appid下的唯一标识 1250 | data.put("openid", openid); 1251 | 1252 | /** 以下参数为非必填参数 **/ 1253 | // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 1254 | // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 1255 | // 协议版本 version 否 1.0 String(32) 默认1.0 1256 | // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 1257 | 1258 | 1259 | /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ 1260 | // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 1261 | // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 1262 | // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 1263 | // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 1264 | 1265 | // 微信调用接口 1266 | Map resultMap = this.sendCoupon(data); 1267 | 1268 | WXPayUtil.getLogger().info("wxPay.sendCoupon:" + resultMap); 1269 | 1270 | return resultMap; 1271 | } 1272 | 1273 | /** 1274 | * 作用:商户平台-代金券或立减优惠-查询代金券信息
1275 | * 场景:查询代金券信息 1276 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_5&index=6 1277 | * 1278 | * @param coupon_id 代金券id 1279 | * @param stock_id 批次号 1280 | * @param openid 用户openid 1281 | * @return API返回数据 1282 | * @throws Exception e 1283 | */ 1284 | public Map queryCouponsInfo(String coupon_id, String stock_id, String openid) throws Exception { 1285 | 1286 | /** 构造请求参数数据 **/ 1287 | Map data = new HashMap<>(); 1288 | 1289 | // 代金券id coupon_id 是 1565 String 代金券id 1290 | data.put("coupon_id", coupon_id); 1291 | // 用户openid openid 是 onqOjjrXT-776SpHnfexGm1_P7iE String Openid信息,用户在appid下的唯一标识 1292 | data.put("openid", openid); 1293 | // 批次号 stock_id 是 58818 String(32) 代金劵对应的批次号 1294 | data.put("stock_id", stock_id); 1295 | 1296 | /** 以下参数为非必填参数 **/ 1297 | // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 1298 | // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 1299 | // 协议版本 version 否 1.0 String(32) 默认1.0 1300 | // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 1301 | 1302 | 1303 | /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ 1304 | // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 1305 | // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 1306 | // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 1307 | // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 1308 | 1309 | // 微信调用接口 1310 | Map resultMap = this.queryCouponsInfo(data); 1311 | 1312 | WXPayUtil.getLogger().info("wxPay.queryCouponsInfo:" + resultMap); 1313 | 1314 | return resultMap; 1315 | } 1316 | 1317 | /** 1318 | * 作用:商户平台-代金券或立减优惠-查询代金券批次
1319 | * 场景:查询代金券批次信息 1320 | * 接口文档地址:https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_4&index=5 1321 | * 1322 | * @param coupon_stock_id 代金券批次id 1323 | * @return API返回数据 1324 | * @throws Exception e 1325 | */ 1326 | public Map queryCouponStock(String coupon_stock_id) throws Exception { 1327 | 1328 | /** 构造请求参数数据 **/ 1329 | Map data = new HashMap<>(); 1330 | 1331 | // 代金券批次id coupon_stock_id 是 1757 String 代金券批次id 1332 | data.put("coupon_stock_id", coupon_stock_id); 1333 | 1334 | /** 以下参数为非必填参数 **/ 1335 | // 操作员 op_user_id 否 10000098 String(32) 操作员帐号, 默认为商户号 可在商户平台配置操作员对应的api权限 1336 | // 设备号 device_info 否 String(32) 微信支付分配的终端设备号 1337 | // 协议版本 version 否 1.0 String(32) 默认1.0 1338 | // 协议类型 type 否 XML String(32) XML【目前仅支持默认XML】 1339 | 1340 | 1341 | /** 以下四个参数,在 this.fillRequestData 方法中会自动赋值 **/ 1342 | // 公众账号ID appid 是 wx5edab3bdfba3dc1c String(32) 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 1343 | // 商户号 mch_id 是 10000098 String(32) 微信为发券方商户分配的商户号 1344 | // 随机字符串 nonce_str 是 1417574675 String(32) 随机字符串,不长于32位 1345 | // 签名 sign 是 841B3002FE2220C87A2D08ABD8A8F791 String(32) 签名参数,详见签名生成算法 1346 | 1347 | // 微信调用接口 1348 | Map resultMap = this.queryCouponStock(data); 1349 | 1350 | WXPayUtil.getLogger().info("wxPay.queryCouponStock:" + resultMap); 1351 | 1352 | return resultMap; 1353 | } 1354 | 1355 | } // end class 1356 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayConfig.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import java.io.InputStream; 4 | 5 | /** 6 | * 微信支付配置接口,用户设置获取微信支付关键信息抽象方法 7 | * 8 | * @author yclimb 9 | * @date 2018/8/17 10 | */ 11 | public abstract class WXPayConfig { 12 | 13 | /** 14 | * 获取 App ID 15 | * 16 | * @return App ID 17 | */ 18 | abstract String getAppID(); 19 | 20 | /** 21 | * 获取 Mch ID 22 | * 23 | * @return Mch ID 24 | */ 25 | abstract String getMchID(); 26 | 27 | /** 28 | * 获取 API 密钥 29 | * 30 | * @return API密钥 31 | */ 32 | abstract String getKey(); 33 | 34 | /** 35 | * 获取商户证书内容 36 | * 37 | * @return 商户证书内容 38 | */ 39 | abstract InputStream getCertStream(); 40 | 41 | /** 42 | * HTTP(S) 连接超时时间,单位毫秒 43 | * 44 | * @return int 45 | */ 46 | public int getHttpConnectTimeoutMs() { 47 | return 6 * 1000; 48 | } 49 | 50 | /** 51 | * HTTP(S) 读数据超时时间,单位毫秒 52 | * 53 | * @return int 54 | */ 55 | public int getHttpReadTimeoutMs() { 56 | return 8 * 1000; 57 | } 58 | 59 | /** 60 | * 获取WXPayDomain, 用于多域名容灾自动切换 61 | * 62 | * @return IWXPayDomain 63 | */ 64 | abstract WXPayDomain getWXPayDomain(); 65 | 66 | /** 67 | * 是否自动上报。 68 | * 若要关闭自动上报,子类中实现该函数返回 false 即可。 69 | * 70 | * @return boolean 71 | */ 72 | public boolean shouldAutoReport() { 73 | return true; 74 | } 75 | 76 | /** 77 | * 进行健康上报的线程的数量 78 | * 79 | * @return int 80 | */ 81 | public int getReportWorkerNum() { 82 | return 6; 83 | } 84 | 85 | /** 86 | * 健康上报缓存消息的最大数量。会有线程去独立上报 87 | * 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受 88 | * 89 | * @return int 90 | */ 91 | public int getReportQueueMaxSize() { 92 | return 10000; 93 | } 94 | 95 | /** 96 | * 批量上报,一次最多上报多个数据 97 | * 98 | * @return int 99 | */ 100 | public int getReportBatchSize() { 101 | return 10; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayConfigImpl.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * 微信支付配置接口实现类 12 | * 13 | * @author yclimb 14 | * @date 2018/8/17 15 | */ 16 | public class WXPayConfigImpl extends WXPayConfig { 17 | 18 | private byte[] certData; 19 | private static WXPayConfigImpl INSTANCE; 20 | 21 | private WXPayConfigImpl() throws Exception { 22 | String certPath = WXPayConstants.APICLIENT_CERT; 23 | File file = new File(certPath); 24 | InputStream certStream = new FileInputStream(file); 25 | this.certData = new byte[(int) file.length()]; 26 | certStream.read(this.certData); 27 | certStream.close(); 28 | } 29 | 30 | public static WXPayConfigImpl getInstance() throws Exception { 31 | if (INSTANCE == null) { 32 | synchronized (WXPayConfigImpl.class) { 33 | if (INSTANCE == null) { 34 | INSTANCE = new WXPayConfigImpl(); 35 | } 36 | } 37 | } 38 | return INSTANCE; 39 | } 40 | 41 | @Override 42 | public String getAppID() { 43 | return WXPayConstants.APP_ID; 44 | } 45 | 46 | @Override 47 | public String getMchID() { 48 | return WXPayConstants.MCH_ID; 49 | } 50 | 51 | @Override 52 | public String getKey() { 53 | return WXPayConstants.API_KEY; 54 | } 55 | 56 | @Override 57 | public InputStream getCertStream() { 58 | ByteArrayInputStream certBis; 59 | certBis = new ByteArrayInputStream(this.certData); 60 | return certBis; 61 | } 62 | 63 | @Override 64 | public int getHttpConnectTimeoutMs() { 65 | return 2000; 66 | } 67 | 68 | @Override 69 | public int getHttpReadTimeoutMs() { 70 | return 10000; 71 | } 72 | 73 | @Override 74 | WXPayDomain getWXPayDomain() { 75 | return WXPayDomainSimpleImpl.instance(); 76 | } 77 | 78 | public String getPrimaryDomain() { 79 | return "api.mch.weixin.qq.com"; 80 | } 81 | 82 | public String getAlternateDomain() { 83 | return "api2.mch.weixin.qq.com"; 84 | } 85 | 86 | @Override 87 | public int getReportWorkerNum() { 88 | return 1; 89 | } 90 | 91 | @Override 92 | public int getReportBatchSize() { 93 | return 2; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayDomain.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | /** 4 | * 域名管理,实现主备域名自动切换 5 | * 6 | * @author yclimb 7 | * @date 2018/8/17 8 | */ 9 | public abstract interface WXPayDomain { 10 | /** 11 | * 上报域名网络状况 12 | * @param domain 域名。 比如:api.mch.weixin.qq.com 13 | * @param elapsedTimeMillis 耗时 14 | * @param ex 网络请求中出现的异常。 15 | * null表示没有异常 16 | * ConnectTimeoutException,表示建立网络连接异常 17 | * UnknownHostException, 表示dns解析异常 18 | */ 19 | abstract void report(final String domain, long elapsedTimeMillis, final Exception ex); 20 | 21 | /** 22 | * 获取域名 23 | * @param config 配置 24 | * @return 域名 25 | */ 26 | abstract DomainInfo getDomain(final WXPayConfig config); 27 | 28 | static class DomainInfo{ 29 | public String domain; //域名 30 | public boolean primaryDomain; //该域名是否为主域名。例如:api.mch.weixin.qq.com为主域名 31 | public DomainInfo(String domain, boolean primaryDomain) { 32 | this.domain = domain; 33 | this.primaryDomain = primaryDomain; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "DomainInfo{" + 39 | "domain='" + domain + '\'' + 40 | ", primaryDomain=" + primaryDomain + 41 | '}'; 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayDomainSimpleImpl.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | import org.apache.http.conn.ConnectTimeoutException; 5 | 6 | import java.net.UnknownHostException; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * 微信支付域名实现类(官方简单版) 12 | * 13 | * @author yclimb 14 | * @date 2018/8/17 15 | */ 16 | public class WXPayDomainSimpleImpl implements WXPayDomain { 17 | private WXPayDomainSimpleImpl() { 18 | } 19 | 20 | private static class WxpayDomainHolder { 21 | private static WXPayDomain holder = new WXPayDomainSimpleImpl(); 22 | } 23 | 24 | public static WXPayDomain instance() { 25 | return WxpayDomainHolder.holder; 26 | } 27 | 28 | @Override 29 | public synchronized void report(final String domain, long elapsedTimeMillis, final Exception ex) { 30 | DomainStatics info = domainData.get(domain); 31 | if (info == null) { 32 | info = new DomainStatics(domain); 33 | domainData.put(domain, info); 34 | } 35 | 36 | // success 37 | if (ex == null) { 38 | if (info.succCount >= 2) { 39 | // continue succ, clear error count 40 | info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0; 41 | } else { 42 | ++info.succCount; 43 | } 44 | } else if (ex instanceof ConnectTimeoutException) { 45 | info.succCount = info.dnsErrorCount = 0; 46 | ++info.connectTimeoutCount; 47 | } else if (ex instanceof UnknownHostException) { 48 | info.succCount = 0; 49 | ++info.dnsErrorCount; 50 | } else { 51 | info.succCount = 0; 52 | ++info.otherErrorCount; 53 | } 54 | } 55 | 56 | @Override 57 | public synchronized DomainInfo getDomain(final WXPayConfig config) { 58 | DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API); 59 | if (primaryDomain == null || 60 | primaryDomain.isGood()) { 61 | return new DomainInfo(WXPayConstants.DOMAIN_API, true); 62 | } 63 | 64 | long now = System.currentTimeMillis(); 65 | if (switchToAlternateDomainTime == 0) { 66 | // first switch 67 | switchToAlternateDomainTime = now; 68 | return new DomainInfo(WXPayConstants.DOMAIN_API2, false); 69 | } else if (now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC) { 70 | DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2); 71 | if (alternateDomain == null || 72 | alternateDomain.isGood() || 73 | alternateDomain.badCount() < primaryDomain.badCount()) { 74 | return new DomainInfo(WXPayConstants.DOMAIN_API2, false); 75 | } else { 76 | return new DomainInfo(WXPayConstants.DOMAIN_API, true); 77 | } 78 | } else { //force switch back 79 | switchToAlternateDomainTime = 0; 80 | primaryDomain.resetCount(); 81 | DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2); 82 | if (alternateDomain != null) { 83 | alternateDomain.resetCount(); 84 | } 85 | return new DomainInfo(WXPayConstants.DOMAIN_API, true); 86 | } 87 | } 88 | 89 | static class DomainStatics { 90 | final String domain; 91 | int succCount = 0; 92 | int connectTimeoutCount = 0; 93 | int dnsErrorCount = 0; 94 | int otherErrorCount = 0; 95 | 96 | DomainStatics(String domain) { 97 | this.domain = domain; 98 | } 99 | 100 | void resetCount() { 101 | succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0; 102 | } 103 | 104 | boolean isGood() { 105 | return connectTimeoutCount <= 2 && dnsErrorCount <= 2; 106 | } 107 | 108 | int badCount() { 109 | return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4; 110 | } 111 | } 112 | 113 | // 3 minutes 114 | private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; 115 | private long switchToAlternateDomainTime = 0; 116 | private Map domainData = new HashMap(); 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayReport.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | import com.weixin.pay.util.WXPayUtil; 5 | import org.apache.http.HttpEntity; 6 | import org.apache.http.HttpResponse; 7 | import org.apache.http.client.HttpClient; 8 | import org.apache.http.client.config.RequestConfig; 9 | import org.apache.http.client.methods.HttpPost; 10 | import org.apache.http.config.RegistryBuilder; 11 | import org.apache.http.conn.socket.ConnectionSocketFactory; 12 | import org.apache.http.conn.socket.PlainConnectionSocketFactory; 13 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 14 | import org.apache.http.entity.StringEntity; 15 | import org.apache.http.impl.client.HttpClientBuilder; 16 | import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 17 | import org.apache.http.util.EntityUtils; 18 | 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.LinkedBlockingQueue; 22 | import java.util.concurrent.ThreadFactory; 23 | 24 | /** 25 | * 交易保障(官方版) 26 | * 27 | * @author yclimb 28 | * @date 2018/8/17 29 | */ 30 | public class WXPayReport { 31 | 32 | public static class ReportInfo { 33 | 34 | /** 35 | * 布尔变量使用int。0为false, 1为true。 36 | */ 37 | 38 | // 基本信息 39 | private String version = "v1"; 40 | private String sdk = WXPayConstants.WXPAYSDK_VERSION; 41 | private String uuid; // 交易的标识 42 | private long timestamp; // 上报时的时间戳,单位秒 43 | private long elapsedTimeMillis; // 耗时,单位 毫秒 44 | 45 | // 针对主域名 46 | private String firstDomain; // 第1次请求的域名 47 | private boolean primaryDomain; //是否主域名 48 | private int firstConnectTimeoutMillis; // 第1次请求设置的连接超时时间,单位 毫秒 49 | private int firstReadTimeoutMillis; // 第1次请求设置的读写超时时间,单位 毫秒 50 | private int firstHasDnsError; // 第1次请求是否出现dns问题 51 | private int firstHasConnectTimeout; // 第1次请求是否出现连接超时 52 | private int firstHasReadTimeout; // 第1次请求是否出现连接超时 53 | 54 | public ReportInfo(String uuid, long timestamp, long elapsedTimeMillis, String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis, boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) { 55 | this.uuid = uuid; 56 | this.timestamp = timestamp; 57 | this.elapsedTimeMillis = elapsedTimeMillis; 58 | this.firstDomain = firstDomain; 59 | this.primaryDomain = primaryDomain; 60 | this.firstConnectTimeoutMillis = firstConnectTimeoutMillis; 61 | this.firstReadTimeoutMillis = firstReadTimeoutMillis; 62 | this.firstHasDnsError = firstHasDnsError ? 1 : 0; 63 | this.firstHasConnectTimeout = firstHasConnectTimeout ? 1 : 0; 64 | this.firstHasReadTimeout = firstHasReadTimeout ? 1 : 0; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "ReportInfo{" + 70 | "version='" + version + '\'' + 71 | ", sdk='" + sdk + '\'' + 72 | ", uuid='" + uuid + '\'' + 73 | ", timestamp=" + timestamp + 74 | ", elapsedTimeMillis=" + elapsedTimeMillis + 75 | ", firstDomain='" + firstDomain + '\'' + 76 | ", primaryDomain=" + primaryDomain + 77 | ", firstConnectTimeoutMillis=" + firstConnectTimeoutMillis + 78 | ", firstReadTimeoutMillis=" + firstReadTimeoutMillis + 79 | ", firstHasDnsError=" + firstHasDnsError + 80 | ", firstHasConnectTimeout=" + firstHasConnectTimeout + 81 | ", firstHasReadTimeout=" + firstHasReadTimeout + 82 | '}'; 83 | } 84 | 85 | /** 86 | * 转换成 csv 格式 87 | * 88 | * @return 89 | */ 90 | public String toLineString(String key) { 91 | String separator = ","; 92 | Object[] objects = new Object[]{ 93 | version, sdk, uuid, timestamp, elapsedTimeMillis, 94 | firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis, 95 | firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout 96 | }; 97 | StringBuffer sb = new StringBuffer(); 98 | for (Object obj : objects) { 99 | sb.append(obj).append(separator); 100 | } 101 | try { 102 | String sign = WXPayUtil.HMACSHA256(sb.toString(), key); 103 | sb.append(sign); 104 | return sb.toString(); 105 | } catch (Exception ex) { 106 | return null; 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | private static final String REPORT_URL = "http://report.mch.weixin.qq.com/wxpay/report/default"; 114 | // private static final String REPORT_URL = "http://127.0.0.1:5000/test"; 115 | 116 | 117 | private static final int DEFAULT_CONNECT_TIMEOUT_MS = 6 * 1000; 118 | private static final int DEFAULT_READ_TIMEOUT_MS = 8 * 1000; 119 | 120 | private LinkedBlockingQueue reportMsgQueue = null; 121 | private WXPayConfig config; 122 | private ExecutorService executorService; 123 | 124 | private volatile static WXPayReport INSTANCE; 125 | 126 | private WXPayReport(final WXPayConfig config) { 127 | this.config = config; 128 | reportMsgQueue = new LinkedBlockingQueue(config.getReportQueueMaxSize()); 129 | 130 | // 添加处理线程 131 | executorService = Executors.newFixedThreadPool(config.getReportWorkerNum(), new ThreadFactory() { 132 | public Thread newThread(Runnable r) { 133 | Thread t = Executors.defaultThreadFactory().newThread(r); 134 | t.setDaemon(true); 135 | return t; 136 | } 137 | }); 138 | 139 | if (config.shouldAutoReport()) { 140 | WXPayUtil.getLogger().info("report worker num: {}", config.getReportWorkerNum()); 141 | for (int i = 0; i < config.getReportWorkerNum(); ++i) { 142 | executorService.execute(new Runnable() { 143 | public void run() { 144 | while (true) { 145 | // 先用 take 获取数据 146 | try { 147 | StringBuffer sb = new StringBuffer(); 148 | String firstMsg = reportMsgQueue.take(); 149 | WXPayUtil.getLogger().info("get first report msg: {}", firstMsg); 150 | String msg = null; 151 | sb.append(firstMsg); //会阻塞至有消息 152 | int remainNum = config.getReportBatchSize() - 1; 153 | for (int j = 0; j < remainNum; ++j) { 154 | WXPayUtil.getLogger().info("try get remain report msg"); 155 | // msg = reportMsgQueue.poll(); // 不阻塞了 156 | msg = reportMsgQueue.take(); 157 | WXPayUtil.getLogger().info("get remain report msg: {}", msg); 158 | if (msg == null) { 159 | break; 160 | } else { 161 | sb.append("\n"); 162 | sb.append(msg); 163 | } 164 | } 165 | // 上报 166 | WXPayReport.httpRequest(sb.toString(), DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS); 167 | } catch (Exception ex) { 168 | WXPayUtil.getLogger().warn("report fail. reason: {}", ex.getMessage()); 169 | } 170 | } 171 | } 172 | }); 173 | } 174 | } 175 | 176 | } 177 | 178 | /** 179 | * 单例,双重校验,请在 JDK 1.5及更高版本中使用 180 | * 181 | * @param config 182 | * @return 183 | */ 184 | public static WXPayReport getInstance(WXPayConfig config) { 185 | if (INSTANCE == null) { 186 | synchronized (WXPayReport.class) { 187 | if (INSTANCE == null) { 188 | INSTANCE = new WXPayReport(config); 189 | } 190 | } 191 | } 192 | return INSTANCE; 193 | } 194 | 195 | public void report(String uuid, long elapsedTimeMillis, 196 | String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis, 197 | boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) { 198 | long currentTimestamp = WXPayUtil.getCurrentTimestamp(); 199 | ReportInfo reportInfo = new ReportInfo(uuid, currentTimestamp, elapsedTimeMillis, 200 | firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis, 201 | firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout); 202 | String data = reportInfo.toLineString(config.getKey()); 203 | WXPayUtil.getLogger().info("report {}", data); 204 | if (data != null) { 205 | reportMsgQueue.offer(data); 206 | } 207 | } 208 | 209 | 210 | @Deprecated 211 | private void reportSync(final String data) throws Exception { 212 | httpRequest(data, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS); 213 | } 214 | 215 | @Deprecated 216 | private void reportAsync(final String data) throws Exception { 217 | new Thread(new Runnable() { 218 | public void run() { 219 | try { 220 | httpRequest(data, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS); 221 | } catch (Exception ex) { 222 | WXPayUtil.getLogger().warn("report fail. reason: {}", ex.getMessage()); 223 | } 224 | } 225 | }).start(); 226 | } 227 | 228 | /** 229 | * http 请求 230 | * 231 | * @param data 232 | * @param connectTimeoutMs 233 | * @param readTimeoutMs 234 | * @return 235 | * @throws Exception 236 | */ 237 | private static String httpRequest(String data, int connectTimeoutMs, int readTimeoutMs) throws Exception { 238 | BasicHttpClientConnectionManager connManager; 239 | connManager = new BasicHttpClientConnectionManager( 240 | RegistryBuilder.create() 241 | .register("http", PlainConnectionSocketFactory.getSocketFactory()) 242 | .register("https", SSLConnectionSocketFactory.getSocketFactory()) 243 | .build(), 244 | null, 245 | null, 246 | null 247 | ); 248 | HttpClient httpClient = HttpClientBuilder.create() 249 | .setConnectionManager(connManager) 250 | .build(); 251 | 252 | HttpPost httpPost = new HttpPost(REPORT_URL); 253 | 254 | RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build(); 255 | httpPost.setConfig(requestConfig); 256 | 257 | StringEntity postEntity = new StringEntity(data, "UTF-8"); 258 | httpPost.addHeader("Content-Type", "text/xml"); 259 | httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT); 260 | httpPost.setEntity(postEntity); 261 | 262 | HttpResponse httpResponse = httpClient.execute(httpPost); 263 | HttpEntity httpEntity = httpResponse.getEntity(); 264 | return EntityUtils.toString(httpEntity, "UTF-8"); 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/WXPayRequest.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.util.WXPayUtil; 4 | import org.apache.http.HttpEntity; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.client.HttpClient; 7 | import org.apache.http.client.config.RequestConfig; 8 | import org.apache.http.client.methods.HttpPost; 9 | import org.apache.http.config.RegistryBuilder; 10 | import org.apache.http.conn.ConnectTimeoutException; 11 | import org.apache.http.conn.socket.ConnectionSocketFactory; 12 | import org.apache.http.conn.socket.PlainConnectionSocketFactory; 13 | import org.apache.http.conn.ssl.DefaultHostnameVerifier; 14 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 15 | import org.apache.http.entity.StringEntity; 16 | import org.apache.http.impl.client.HttpClientBuilder; 17 | import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 18 | import org.apache.http.util.EntityUtils; 19 | 20 | import javax.net.ssl.KeyManagerFactory; 21 | import javax.net.ssl.SSLContext; 22 | import java.io.InputStream; 23 | import java.net.SocketTimeoutException; 24 | import java.net.UnknownHostException; 25 | import java.security.KeyStore; 26 | import java.security.SecureRandom; 27 | 28 | import static com.weixin.pay.constants.WXPayConstants.USER_AGENT; 29 | 30 | /** 31 | * 微信支付请求类 32 | * 33 | * @author yclimb 34 | * @date 2018/8/17 35 | */ 36 | public class WXPayRequest { 37 | private WXPayConfig config; 38 | 39 | public WXPayRequest(WXPayConfig config) throws Exception { 40 | 41 | this.config = config; 42 | } 43 | 44 | /** 45 | * 请求,只请求一次,不做重试 46 | * 47 | * @param domain 48 | * @param urlSuffix 49 | * @param uuid 50 | * @param data 51 | * @param connectTimeoutMs 52 | * @param readTimeoutMs 53 | * @param useCert 是否使用证书,针对退款、撤销等操作 54 | * @return 55 | * @throws Exception 56 | */ 57 | private String requestOnce(final String domain, String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert) throws Exception { 58 | BasicHttpClientConnectionManager connManager; 59 | if (useCert) { 60 | // 证书 61 | char[] password = config.getMchID().toCharArray(); 62 | InputStream certStream = config.getCertStream(); 63 | KeyStore ks = KeyStore.getInstance("PKCS12"); 64 | ks.load(certStream, password); 65 | 66 | // 实例化密钥库 & 初始化密钥工厂 67 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 68 | kmf.init(ks, password); 69 | 70 | // 创建 SSLContext 71 | SSLContext sslContext = SSLContext.getInstance("TLS"); 72 | sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); 73 | 74 | SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( 75 | sslContext, 76 | new String[]{"TLSv1"}, 77 | null, 78 | new DefaultHostnameVerifier()); 79 | 80 | connManager = new BasicHttpClientConnectionManager( 81 | RegistryBuilder.create() 82 | .register("http", PlainConnectionSocketFactory.getSocketFactory()) 83 | .register("https", sslConnectionSocketFactory) 84 | .build(), 85 | null, 86 | null, 87 | null 88 | ); 89 | } else { 90 | connManager = new BasicHttpClientConnectionManager( 91 | RegistryBuilder.create() 92 | .register("http", PlainConnectionSocketFactory.getSocketFactory()) 93 | .register("https", SSLConnectionSocketFactory.getSocketFactory()) 94 | .build(), 95 | null, 96 | null, 97 | null 98 | ); 99 | } 100 | 101 | HttpClient httpClient = HttpClientBuilder.create() 102 | .setConnectionManager(connManager) 103 | .build(); 104 | 105 | String url = "https://" + domain + urlSuffix; 106 | HttpPost httpPost = new HttpPost(url); 107 | 108 | RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build(); 109 | httpPost.setConfig(requestConfig); 110 | 111 | StringEntity postEntity = new StringEntity(data, "UTF-8"); 112 | httpPost.addHeader("Content-Type", "text/xml"); 113 | httpPost.addHeader("User-Agent", USER_AGENT + " " + config.getMchID()); 114 | httpPost.setEntity(postEntity); 115 | 116 | HttpResponse httpResponse = httpClient.execute(httpPost); 117 | HttpEntity httpEntity = httpResponse.getEntity(); 118 | return EntityUtils.toString(httpEntity, "UTF-8"); 119 | 120 | } 121 | 122 | 123 | private String request(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert, boolean autoReport) throws Exception { 124 | Exception exception = null; 125 | long elapsedTimeMillis = 0; 126 | long startTimestampMs = WXPayUtil.getCurrentTimestampMs(); 127 | boolean firstHasDnsErr = false; 128 | boolean firstHasConnectTimeout = false; 129 | boolean firstHasReadTimeout = false; 130 | WXPayDomain.DomainInfo domainInfo = config.getWXPayDomain().getDomain(config); 131 | if (domainInfo == null) { 132 | throw new Exception("WXPayConfig.getWXPayDomain().getDomain() is empty or null"); 133 | } 134 | try { 135 | String result = requestOnce(domainInfo.domain, urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, useCert); 136 | elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; 137 | config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, null); 138 | WXPayReport.getInstance(config).report( 139 | uuid, 140 | elapsedTimeMillis, 141 | domainInfo.domain, 142 | domainInfo.primaryDomain, 143 | connectTimeoutMs, 144 | readTimeoutMs, 145 | firstHasDnsErr, 146 | firstHasConnectTimeout, 147 | firstHasReadTimeout); 148 | return result; 149 | } catch (UnknownHostException ex) { // dns 解析错误,或域名不存在 150 | exception = ex; 151 | firstHasDnsErr = true; 152 | elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; 153 | WXPayUtil.getLogger().warn("UnknownHostException for domainInfo {}", domainInfo); 154 | WXPayReport.getInstance(config).report( 155 | uuid, 156 | elapsedTimeMillis, 157 | domainInfo.domain, 158 | domainInfo.primaryDomain, 159 | connectTimeoutMs, 160 | readTimeoutMs, 161 | firstHasDnsErr, 162 | firstHasConnectTimeout, 163 | firstHasReadTimeout 164 | ); 165 | } catch (ConnectTimeoutException ex) { 166 | exception = ex; 167 | firstHasConnectTimeout = true; 168 | elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; 169 | WXPayUtil.getLogger().warn("connect timeout happened for domainInfo {}", domainInfo); 170 | WXPayReport.getInstance(config).report( 171 | uuid, 172 | elapsedTimeMillis, 173 | domainInfo.domain, 174 | domainInfo.primaryDomain, 175 | connectTimeoutMs, 176 | readTimeoutMs, 177 | firstHasDnsErr, 178 | firstHasConnectTimeout, 179 | firstHasReadTimeout 180 | ); 181 | } catch (SocketTimeoutException ex) { 182 | exception = ex; 183 | firstHasReadTimeout = true; 184 | elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; 185 | WXPayUtil.getLogger().warn("timeout happened for domainInfo {}", domainInfo); 186 | WXPayReport.getInstance(config).report( 187 | uuid, 188 | elapsedTimeMillis, 189 | domainInfo.domain, 190 | domainInfo.primaryDomain, 191 | connectTimeoutMs, 192 | readTimeoutMs, 193 | firstHasDnsErr, 194 | firstHasConnectTimeout, 195 | firstHasReadTimeout); 196 | } catch (Exception ex) { 197 | exception = ex; 198 | elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; 199 | WXPayReport.getInstance(config).report( 200 | uuid, 201 | elapsedTimeMillis, 202 | domainInfo.domain, 203 | domainInfo.primaryDomain, 204 | connectTimeoutMs, 205 | readTimeoutMs, 206 | firstHasDnsErr, 207 | firstHasConnectTimeout, 208 | firstHasReadTimeout); 209 | } 210 | config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, exception); 211 | throw exception; 212 | } 213 | 214 | 215 | /** 216 | * 可重试的,非双向认证的请求 217 | * 218 | * @param urlSuffix 219 | * @param uuid 220 | * @param data 221 | * @return 222 | */ 223 | public String requestWithoutCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception { 224 | return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), false, autoReport); 225 | } 226 | 227 | /** 228 | * 可重试的,非双向认证的请求 229 | * 230 | * @param urlSuffix 231 | * @param uuid 232 | * @param data 233 | * @param connectTimeoutMs 234 | * @param readTimeoutMs 235 | * @return 236 | */ 237 | public String requestWithoutCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception { 238 | return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, false, autoReport); 239 | } 240 | 241 | /** 242 | * 可重试的,双向认证的请求 243 | * 244 | * @param urlSuffix 245 | * @param uuid 246 | * @param data 247 | * @return 248 | */ 249 | public String requestWithCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception { 250 | return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), true, autoReport); 251 | } 252 | 253 | /** 254 | * 可重试的,双向认证的请求 255 | * 256 | * @param urlSuffix 257 | * @param uuid 258 | * @param data 259 | * @param connectTimeoutMs 260 | * @param readTimeoutMs 261 | * @return 262 | */ 263 | public String requestWithCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception { 264 | return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, true, autoReport); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/XxxWXPayConfigImpl.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * 测试/第二个微信支付实体的微信支付实现类 12 | * 13 | * @author yclimb 14 | * @date 2018/7/31 15 | */ 16 | public class XxxWXPayConfigImpl extends WXPayConfig { 17 | 18 | private byte[] certData; 19 | private static XxxWXPayConfigImpl INSTANCE; 20 | 21 | private XxxWXPayConfigImpl() throws Exception{ 22 | String certPath = WXPayConstants.APICLIENT_CERT_XXX; 23 | File file = new File(certPath); 24 | InputStream certStream = new FileInputStream(file); 25 | this.certData = new byte[(int) file.length()]; 26 | certStream.read(this.certData); 27 | certStream.close(); 28 | } 29 | 30 | public static XxxWXPayConfigImpl getInstance() throws Exception{ 31 | if (INSTANCE == null) { 32 | synchronized (XxxWXPayConfigImpl.class) { 33 | if (INSTANCE == null) { 34 | INSTANCE = new XxxWXPayConfigImpl(); 35 | } 36 | } 37 | } 38 | return INSTANCE; 39 | } 40 | 41 | @Override 42 | public String getAppID() { 43 | return WXPayConstants.APP_ID_XXX; 44 | } 45 | 46 | @Override 47 | public String getMchID() { 48 | return WXPayConstants.MCH_ID_XXX; 49 | } 50 | 51 | @Override 52 | public String getKey() { 53 | return WXPayConstants.API_KEY_XXX; 54 | } 55 | 56 | @Override 57 | public InputStream getCertStream() { 58 | ByteArrayInputStream certBis; 59 | certBis = new ByteArrayInputStream(this.certData); 60 | return certBis; 61 | } 62 | 63 | @Override 64 | public int getHttpConnectTimeoutMs() { 65 | return 2000; 66 | } 67 | 68 | @Override 69 | public int getHttpReadTimeoutMs() { 70 | return 10000; 71 | } 72 | 73 | @Override 74 | WXPayDomain getWXPayDomain() { 75 | return WXPayDomainSimpleImpl.instance(); 76 | } 77 | 78 | public String getPrimaryDomain() { 79 | return "api.mch.weixin.qq.com"; 80 | } 81 | 82 | public String getAlternateDomain() { 83 | return "api2.mch.weixin.qq.com"; 84 | } 85 | 86 | @Override 87 | public int getReportWorkerNum() { 88 | return 1; 89 | } 90 | 91 | @Override 92 | public int getReportBatchSize() { 93 | return 2; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/card/CardBgColorEnum.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.card; 2 | 3 | /** 4 | * 微信卡券背景颜色枚举类 5 | * 6 | * @author yclimb 7 | * @date 2018/9/18 8 | */ 9 | public enum CardBgColorEnum { 10 | 11 | /** 12 | * 淡绿色 13 | */ 14 | COLOR_010("Color010", "#63b359"), 15 | /** 16 | * 深绿色 17 | */ 18 | COLOR_020("Color020", "#2c9f67"), 19 | /** 20 | * 淡蓝色 21 | */ 22 | COLOR_030("Color030", "#509fc9"), 23 | /** 24 | * 深蓝色 25 | */ 26 | COLOR_040("Color040", "#5885cf"), 27 | /** 28 | * 淡紫色 29 | */ 30 | COLOR_050("Color050", "#9062c0"), 31 | /** 32 | * 土黄色 33 | */ 34 | COLOR_060("Color060", "#d09a45"), 35 | /** 36 | * 淡黄色 37 | */ 38 | COLOR_070("Color070", "#e4b138"), 39 | /** 40 | * 橘黄色 41 | */ 42 | COLOR_080("Color080", "#ee903c"), 43 | /** 44 | * 橘黄色 plus 45 | */ 46 | COLOR_081("Color081", "#f08500"), 47 | /** 48 | * 青色 49 | */ 50 | COLOR_082("Color082", "#a9d92d"), 51 | /** 52 | * 淡红色 53 | */ 54 | COLOR_090("Color090", "#dd6549"), 55 | /** 56 | * 深红色 57 | */ 58 | COLOR_100("Color100", "#cc463d"), 59 | /** 60 | * 玫红色 61 | */ 62 | COLOR_101("Color101", "#cf3e36"), 63 | /** 64 | * 深灰色 65 | */ 66 | COLOR_102("Color102", "#5E6671") 67 | ; 68 | 69 | /** 70 | * 背景颜色名称 71 | */ 72 | private String bgName; 73 | 74 | /** 75 | * 色值 76 | */ 77 | private String bgVal; 78 | 79 | CardBgColorEnum(String bgName, String bgVal) { 80 | this.bgName = bgName; 81 | this.bgVal = bgVal; 82 | } 83 | 84 | public String getBgName() { 85 | return bgName; 86 | } 87 | 88 | public String getBgVal() { 89 | return bgVal; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/constants/WXConstants.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.constants; 2 | 3 | /** 4 | * 微信基础常量类 5 | * 6 | * @author yclimb 7 | * @date 2018/8/17 8 | */ 9 | public class WXConstants { 10 | 11 | /** 12 | * 对于前端访问返回参数,本例使用string,推荐自主封装json对象 13 | */ 14 | public static final String SUCCESS = "success"; 15 | public static final String ERROR = "error"; 16 | 17 | /** 18 | * 授权作用域 不弹出授权页面,直接跳转,只能获取用户openid 19 | **/ 20 | public static final String OAUTH_BASE_SCOPE = "snsapi_base"; 21 | 22 | /** 23 | * 授权作用域 弹出授权页面 能获取昵称、头像等信息 24 | **/ 25 | public static final String OAUTH_USERINFO_SCOPE = "snsapi_userinfo"; 26 | 27 | /** 28 | * 网页授权 重定向后会带上state参数 29 | */ 30 | public static final String OAUTH_STATE = "xxx"; 31 | 32 | /** 33 | * 小程序获取 access token code 34 | */ 35 | public static final String WX_MINI_PROGRAM_CODE = "xxxx"; 36 | 37 | /** 38 | * 微信全局accessToken 39 | */ 40 | public static final String WECHAT_ACCESSTOKEN = OAUTH_STATE + ":wx:accessToken:"; 41 | 42 | /** 43 | * 微信全局accessTokenLock 44 | */ 45 | public static final String WECHAT_ACCESSTOKEN_LOCK = OAUTH_STATE + ":wx:accessTokenLock:"; 46 | 47 | /** 48 | * 微信网页授权openid,时限:7200秒 49 | */ 50 | public static final String WECHAT_JSAPI_OPENID = OAUTH_STATE + ":wx:jsapi:openid:"; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/constants/WXPayCodeEnum.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.constants; 2 | 3 | /** 4 | * 微信支付code码常量类 5 | * 6 | * @author yclimb 7 | * @date 2018/8/6 8 | */ 9 | public enum WXPayCodeEnum { 10 | 11 | /** 12 | * 余额不足 13 | */ 14 | ERR_CODE_NOTENOUGH("NOTENOUGH", "余额不足"); 15 | 16 | private String code; 17 | private String des; 18 | 19 | WXPayCodeEnum(String code, String des) { 20 | this.code = code; 21 | this.des = des; 22 | } 23 | 24 | public String getCode() { 25 | return code; 26 | } 27 | 28 | public void setCode(String code) { 29 | this.code = code; 30 | } 31 | 32 | public String getDes() { 33 | return des; 34 | } 35 | 36 | public void setDes(String des) { 37 | this.des = des; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/constants/WXPayConstants.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.constants; 2 | 3 | import org.apache.http.client.HttpClient; 4 | 5 | /** 6 | * 微信支付SDK常量 7 | */ 8 | public class WXPayConstants { 9 | 10 | /** 11 | * 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 12 | */ 13 | public static String NOTIFY_URL = "https://xxx.com/v1/weixin/pay/wxnotify"; 14 | 15 | /** 16 | * 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数,如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。 17 | */ 18 | public static String NOTIFY_URL_REFUND = "https://xxx.com/v1/weixin/pay/refund"; 19 | 20 | /** 21 | * 微信签名枚举类型 22 | */ 23 | public enum SignType { 24 | MD5, HMACSHA256, SHA1 25 | } 26 | 27 | /** 28 | * 公众号、小程序appid 29 | */ 30 | public static String APP_ID = "xxx"; // 真实 31 | public static String APP_ID_XXX = "xxx"; // 测试/第二个账号 32 | 33 | /** 34 | * AppSecret 35 | */ 36 | public static String SECRET = "xxx"; // 真实 37 | public static String SECRET_XXX = "xxx"; // 测试/第二个账号 38 | 39 | /** 40 | * 商户号 41 | */ 42 | public static final String MCH_ID = "xxx"; // 真实 43 | public static final String MCH_ID_XXX = "xxx"; // 测试/第二个账号 44 | 45 | 46 | /** 47 | * API密钥,在商户平台设置 48 | */ 49 | public static final String API_KEY = "xxx"; // 真实 50 | public static final String API_KEY_XXX = "xxx"; // 测试/第二个账号 51 | public static final String API_KEY_SANDBOX = "xxx"; // sandbox_signkey 52 | 53 | /** 54 | * 证书路径 55 | */ 56 | public static String APICLIENT_CERT = "/data/ops/cert/apiclient_cert.p12"; // 真实 57 | public static String APICLIENT_CERT_XXX = "/data/ops/cert_xxx/apiclient_cert.p12"; // 真实 58 | 59 | /** 60 | * 交易类型 61 | * JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里 62 | * MICROPAY--刷卡支付,刷卡支付有单独的支付接口,不调用统一下单接口 63 | */ 64 | public static final String TRADE_TYPE = "JSAPI"; 65 | public static final String TRADE_TYPE_APP = "APP"; 66 | public static final String TRADE_TYPE_NATIVE = "NATIVE"; 67 | 68 | /** 69 | * 微信 - API域名地址 70 | * 域名管理实现主备域名自动切换 71 | */ 72 | public static final String DOMAIN_API = "api.mch.weixin.qq.com"; 73 | public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com"; 74 | public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com"; 75 | public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com"; 76 | 77 | /** 78 | * 微信 - 默认接口返回状态码 79 | * SUCCESS/FAIL 80 | * 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 81 | */ 82 | public static final String RESULT_CODE = "result_code"; 83 | public static final String FAIL = "FAIL"; 84 | public static final String SUCCESS = "SUCCESS"; 85 | /** 86 | * 返回状态码 return_code SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 87 | * 返回信息 return_msg 当return_code为FAIL时返回信息为错误原因 88 | * 错误代码 err_code 当result_code为FAIL时返回错误代码,详细参见下文错误列表 89 | * 错误代码描述 err_code_des 当result_code为FAIL时返回错误描述,详细参见下文错误列表 90 | */ 91 | public static final String RETURN_CODE = "return_code"; 92 | public static final String RETURN_MSG = "return_msg"; 93 | public static final String ERR_CODE = "err_code"; 94 | public static final String ERR_CODE_DES = "err_code_des"; 95 | 96 | /** 97 | * 签名类型,默认为MD5,支持HMAC-SHA256和MD5。 98 | */ 99 | public static final String HMACSHA256 = "HMAC-SHA256"; 100 | public static final String MD5 = "MD5"; 101 | 102 | /** 103 | * 标价币种:fee_type 104 | * 符合ISO 4217标准的三位字母代码,默认人民币:CNY,详细列表请参见货币类型 105 | */ 106 | public static final String FEE_TYPE_CNY = "CNY"; 107 | 108 | /** 109 | * 微信签名:通过签名算法计算得出的签名值 110 | */ 111 | public static final String FIELD_SIGN = "sign"; 112 | public static final String FIELD_SIGN_TYPE = "sign_type"; 113 | 114 | /** 115 | * 微信支付版本 116 | */ 117 | public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9"; 118 | public static final String USER_AGENT = WXPAYSDK_VERSION + 119 | " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") + 120 | ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion(); 121 | 122 | /** 123 | * 作用:企业付款到零钱资金使用商户号余额资金
124 | * 场景:用于企业向微信用户个人付款 125 | */ 126 | public static final String TRANSFERS_URL_SUFFIX = "/mmpaymkttransfers/promotion/transfers"; 127 | /** 128 | * 作用:商户平台-现金红包-发放普通红包
129 | * 场景:现金红包发放后会以公众号消息的形式触达用户 130 | * 其他:需要证书 131 | */ 132 | public static final String SENDREDPACK_URL_SUFFIX = "/mmpaymkttransfers/sendredpack"; 133 | /** 134 | * 作用:商户平台-现金红包-查询红包记录
135 | * 场景:用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 136 | * 其他:需要证书 137 | */ 138 | public static final String GETHBINFO_URL_SUFFIX = "/mmpaymkttransfers/gethbinfo"; 139 | /** 140 | * 作用:商户平台-代金券或立减优惠-发放代金券
141 | * 场景:用于商户主动调用接口给用户发放代金券的场景,已做防小号处理,给小号发放代金券将返回错误码。 142 | * 注意:通过接口发放的代金券不会进入微信卡包 143 | * 其他:请求需要双向证书 144 | */ 145 | public static final String SEND_COUPON_URL_SUFFIX = "/mmpaymkttransfers/send_coupon"; 146 | /** 147 | * 作用:商户平台-代金券或立减优惠-查询代金券信息
148 | * 场景:查询代金券信息。 149 | */ 150 | public static final String QUERYCOUPONSINFO_URL_SUFFIX = "/mmpaymkttransfers/querycouponsinfo"; 151 | /** 152 | * 作用:商户平台-代金券或立减优惠-查询代金券批次
153 | * 场景:查询代金券批次信息。 154 | */ 155 | public static final String QUERY_COUPON_STOCK_URL_SUFFIX = "/mmpaymkttransfers/query_coupon_stock"; 156 | /** 157 | * 作用:提交刷卡支付
158 | * 场景:刷卡支付 159 | */ 160 | public static final String MICROPAY_URL_SUFFIX = "/pay/micropay"; 161 | /** 162 | * 作用:统一下单
163 | * 场景:公共号支付、扫码支付、APP支付 164 | */ 165 | public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder"; 166 | /** 167 | * 作用:查询订单
168 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 169 | */ 170 | public static final String ORDERQUERY_URL_SUFFIX = "/pay/orderquery"; 171 | /** 172 | * 作用:撤销订单
173 | * 场景:刷卡支付
174 | * 其他:需要证书 175 | */ 176 | public static final String REVERSE_URL_SUFFIX = "/secapi/pay/reverse"; 177 | /** 178 | * 作用:关闭订单
179 | * 场景:公共号支付、扫码支付、APP支付 180 | */ 181 | public static final String CLOSEORDER_URL_SUFFIX = "/pay/closeorder"; 182 | /** 183 | * 作用:申请退款
184 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付
185 | * 其他:需要证书 186 | */ 187 | public static final String REFUND_URL_SUFFIX = "/secapi/pay/refund"; 188 | /** 189 | * 作用:退款查询
190 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 191 | */ 192 | public static final String REFUNDQUERY_URL_SUFFIX = "/pay/refundquery"; 193 | /** 194 | * 作用:对账单下载
195 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付
196 | * 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data, 197 | * 其中return_code为`SUCCESS`,data为对账单数据。 198 | */ 199 | public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill"; 200 | /** 201 | * 作用:交易保障
202 | * 场景:刷卡支付、公共号支付、扫码支付、APP支付 203 | */ 204 | public static final String REPORT_URL_SUFFIX = "/payitil/report"; 205 | /** 206 | * 作用:转换短链接
207 | * 场景:刷卡支付、扫码支付 208 | */ 209 | public static final String SHORTURL_URL_SUFFIX = "/tools/shorturl"; 210 | /** 211 | * 作用:授权码查询OPENID接口
212 | * 场景:刷卡支付 213 | */ 214 | public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid"; 215 | 216 | /** 217 | * 沙箱说明:sandbox 218 | * 219 | * 微信支付沙箱环境,是提供给微信支付商户的开发者,用于模拟支付及回调通知。以验证商户是否理解回调通知、账单格式,以及是否对异常做了正确的处理。 220 | * ◆ 如何对接沙箱环境? 221 | * 1、修改商户自有程序或配置中,微信支付api的链接,如:被扫支付官网的url为:https://api.mch.weixin.qq.com/pay/micropay增加sandbox路径,变更为https://api.mch.weixin.qq.com/sandbox/pay/micropay, 即可接入沙箱验收环境,其它接口类似; 222 | * 2、在微信支付开发调试站点(站点链接:http://mch.weixin.qq.com/wiki/doc/api/index.php),按接口文档填入正确的支付参数,发起微信支付请求,完成支付; 223 | * 3、验收完成后,修改程序或配置中的api链接(重要!),去掉sandbox路径。对接现网环境。 224 | * 225 | * 说明地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1 226 | * https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=21_2 227 | */ 228 | public static final String SANDBOX_MICROPAY_URL_SUFFIX = "/sandboxnew/pay/micropay"; 229 | public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder"; 230 | public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery"; 231 | public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse"; 232 | public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder"; 233 | public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/pay/refund"; 234 | public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery"; 235 | public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill"; 236 | public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report"; 237 | public static final String SANDBOX_SHORTURL_URL_SUFFIX = "/sandboxnew/tools/shorturl"; 238 | public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid"; 239 | public static final String SANDBOX_SENDREDPACK_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/sendredpack"; 240 | public static final String SANDBOX_TRANSFERS_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/promotion/transfers"; 241 | public static final String SANDBOX_GETHBINFO_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/promotion/gethbinfo"; 242 | public static final String SANDBOX_SEND_COUPON_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/send_coupon"; 243 | public static final String SANDBOX_QUERYCOUPONSINFO_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/querycouponsinfo"; 244 | public static final String SANDBOX_QUERY_COUPON_STOCK_URL_SUFFIX = "/sandboxnew/mmpaymkttransfers/query_coupon_stock"; 245 | 246 | 247 | } 248 | 249 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/constants/WXURL.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.constants; 2 | 3 | /** 4 | * 微信基础URL链接 5 | * 6 | * @author yclimb 7 | * @date 2018/8/17 8 | */ 9 | public class WXURL { 10 | 11 | /** 12 | * 请求URL之获取jsapi_ticket 13 | */ 14 | public static final String PAGE_URL_SIGN = "jsapi_ticket={0}&noncestr={1}×tamp={2}&url={3}"; 15 | 16 | /** 17 | * 请求URL之获取access_token 18 | */ 19 | public static final String BASE_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}"; 20 | 21 | /** 22 | * 请求URL之获取jsapi_ticket 23 | */ 24 | public static final String BASE_JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi"; 25 | 26 | /** 27 | * 请求URL之创建菜单 28 | */ 29 | public static final String MENU_CREATE = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token={0}"; 30 | 31 | /** 32 | * 请求URL之查询菜单 33 | */ 34 | public static final String MENU_QUERY = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}"; 35 | 36 | /** 37 | * 请求URL之删除菜单 38 | */ 39 | public static final String MENU_DELETE = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={0}"; 40 | 41 | /** 42 | * 页面授权获取code地址 43 | */ 44 | public static final 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"; 45 | 46 | /** 47 | * 通过code换取网页授权access_token 48 | */ 49 | public static final String OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code"; 50 | 51 | /** 52 | * 页面授权获取指定微信号的基础信息 53 | */ 54 | public static final String OAUTH_GET_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}&lang=zh_CN"; 55 | 56 | /** 57 | * 获取指定微信号的基础信息 通过全局access_token 58 | */ 59 | public static final String GET_USERINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN"; 60 | 61 | /** 62 | * 微信模板消息发送 63 | */ 64 | public static final String WX_TEMPLATE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}"; 65 | 66 | /** 67 | * 微信客户消息发送 68 | */ 69 | public static final String WX_CUSTMOER_SERVICE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={0}"; 70 | 71 | /*** 72 | * 微信创建二维码ticket 73 | */ 74 | public static final String WX_TICKET_CREATE = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}"; 75 | 76 | /** 77 | * 小程序登录校验 78 | */ 79 | public static final String WX_MINI_LOGIN = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; 80 | 81 | /** 82 | * 小程序模板信息 83 | */ 84 | public static final String WX_MINI_TEMPLATE_MSG = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token={0}"; 85 | 86 | /** 87 | * 获取小程序二维码,通过该接口生成的小程序码,永久有效,数量暂无限制 88 | */ 89 | public static final String WX_MINI_QR_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={0}"; 90 | 91 | /** 92 | * 创建支付后领取立减金活动接口 93 | * 通过此接口创建立减金活动。 94 | * 将已创建的代金券cardid、跳转小程序appid、发起支付的商户号等信息通过此接口创建立减金活动,成功返回活动id即为创建成功。 95 | * 接口地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21515658940X5pIn 96 | * 97 | * 协议:https 98 | * http请求方式: POST 99 | * 请求URL:https://api.weixin.qq.com/card/mkt/activity/create?access_token=ACCESS_TOKEN 100 | * POST数据格式:JSON 101 | */ 102 | public static final String WX_CARD_ACTIVITY_CREATE_URL = "https://api.weixin.qq.com/card/mkt/activity/create?access_token={0}"; 103 | 104 | /** 105 | * 卡券签名和JSSDK的签名完全独立,两者的算法和意义完全不同,请不要混淆。 106 | * JSSDK的签名是使用所有JS接口都需要走的一层鉴权,用以标识调用者的身份,和卡券本身并无关系。 107 | * 其次,卡券的签名考虑到协议的扩展性和简单的防数据擅改,设计了一套独立的签名协议。 108 | * 另外由于历史原因,卡券的JS接口先于JSSDK出现,当时的JSAPI并没有鉴权体系,所以在卡券的签名里也加上了appsecret/api_ticket这些身份信息,希望开发者理解。 109 | * 卡券 api_ticket 是用于调用卡券相关接口的临时票据,有效期为 7200 秒,通过 access_token 来获取。这里要注意与 jsapi_ticket 区分开来。 110 | * 由于获取卡券 api_ticket 的 api 调用次数非常有限,频繁刷新卡券 api_ticket 会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存卡券 api_ticket 。 111 | */ 112 | public static final String BASE_API_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=wx_card"; 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/redis/RedisKeyEnum.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.redis; 2 | 3 | /** 4 | * Redis 枚举类 5 | * 6 | * @author yclimb 7 | * @date 2018/4/19 8 | */ 9 | public enum RedisKeyEnum { 10 | 11 | /** 12 | * 生成带参数的小程序二维码KEY 13 | */ 14 | XXX_MINI_WX_CODE(RedisKeyUtil.KEY_PREFIX, "mini", "getwxacodeunlimit", "生成永久无限制微信二维码"), 15 | /** 16 | * 获取卡券api_ticket 17 | */ 18 | IMALL_WXCARD_APITICKET(RedisKeyUtil.KEY_PREFIX, "jsapi", "getWxCardApiTicket", "获取卡券api_ticket的api"), 19 | /** 20 | * 获取卡券api_ticket 21 | */ 22 | IMALL_WX_APITICKET(RedisKeyUtil.KEY_PREFIX, "jsapi", "getWxApiTicket", "获取api_ticket的api") 23 | 24 | ; 25 | 26 | /** 27 | * 系统标识 28 | */ 29 | private String keyPrefix; 30 | 31 | /** 32 | * 模块名称 33 | */ 34 | private String module; 35 | 36 | /** 37 | * 方法名称 38 | */ 39 | private String func; 40 | 41 | /** 42 | * remark 43 | */ 44 | private String remark; 45 | 46 | RedisKeyEnum(String keyPrefix, String module, String func, String remark) { 47 | this.keyPrefix = keyPrefix; 48 | this.module = module; 49 | this.func = func; 50 | this.remark = remark; 51 | } 52 | 53 | public String getKeyPrefix() { 54 | return keyPrefix; 55 | } 56 | 57 | public void setKeyPrefix(String keyPrefix) { 58 | this.keyPrefix = keyPrefix; 59 | } 60 | 61 | public String getModule() { 62 | return module; 63 | } 64 | 65 | public void setModule(String module) { 66 | this.module = module; 67 | } 68 | 69 | public String getFunc() { 70 | return func; 71 | } 72 | 73 | public void setFunc(String func) { 74 | this.func = func; 75 | } 76 | 77 | public String getRemark() { 78 | return remark; 79 | } 80 | 81 | public void setRemark(String remark) { 82 | this.remark = remark; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/redis/RedisKeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.redis; 2 | 3 | /** 4 | * Redis 工具类 5 | * 6 | * @author yclimb 7 | * @date 2018/4/19 8 | */ 9 | public class RedisKeyUtil { 10 | 11 | /** 12 | * 主数据系统标识 13 | */ 14 | public static final String KEY_PREFIX = "xxx"; 15 | 16 | /** 17 | * 分割字符,默认[:] 18 | */ 19 | public static final String KEY_SPLIT_CHAR = ":"; 20 | 21 | /** 22 | * redis的key键规则定义 23 | * 24 | * @param module 模块名称 25 | * @param func 方法名称 26 | * @return key 27 | */ 28 | public static String keyBuilder(String module, String func) { 29 | return keyBuilder(null, module, func, (String[]) null); 30 | } 31 | 32 | /** 33 | * redis的key键规则定义 34 | * 35 | * @param module 模块名称 36 | * @param func 方法名称 37 | * @param args 参数.. 38 | * @return key 39 | */ 40 | public static String keyBuilder(String module, String func, String... args) { 41 | return keyBuilder(null, module, func, args); 42 | } 43 | 44 | /** 45 | * redis的key键规则定义 46 | * 47 | * @param module 模块名称 48 | * @param func 方法名称 49 | * @param objStr 对象.toString() 50 | * @return key 51 | */ 52 | public static String keyBuilder(String module, String func, String objStr) { 53 | return keyBuilder(null, module, func, new String[]{objStr}); 54 | } 55 | 56 | /** 57 | * redis的key键规则定义 58 | * 59 | * @param prefix 项目前缀 60 | * @param module 模块名称 61 | * @param func 方法名称 62 | * @param objStr 对象.toString() 63 | * @return key 64 | */ 65 | public static String keyBuilder(String prefix, String module, String func, String objStr) { 66 | return keyBuilder(prefix, module, func, new String[]{objStr}); 67 | } 68 | 69 | /** 70 | * redis的key键规则定义 71 | * 72 | * @param prefix 项目前缀 73 | * @param module 模块名称 74 | * @param func 方法名称 75 | * @param args 参数.. 76 | * @return key 77 | */ 78 | public static String keyBuilder(String prefix, String module, String func, String... args) { 79 | // 项目前缀 80 | if (prefix == null) { 81 | prefix = KEY_PREFIX; 82 | } 83 | 84 | StringBuilder key = new StringBuilder(prefix); 85 | // KEY_SPLIT_CHAR 为分割字符 86 | key.append(KEY_SPLIT_CHAR).append(module).append(KEY_SPLIT_CHAR).append(func); 87 | 88 | // args 为空时不需要循环 89 | if (args == null || args.length <= 0) { 90 | return key.toString(); 91 | } 92 | 93 | // args 不为空时循环拼接字符 94 | for (String arg : args) { 95 | key.append(KEY_SPLIT_CHAR).append(arg); 96 | } 97 | 98 | return key.toString(); 99 | } 100 | 101 | /** 102 | * redis的key键规则定义 103 | * 104 | * @param redisEnum 枚举对象 105 | * @return key 106 | */ 107 | public static String keyBuilder(RedisKeyEnum redisEnum) { 108 | return keyBuilder(redisEnum.getKeyPrefix(), redisEnum.getModule(), redisEnum.getFunc(), (String[]) null); 109 | } 110 | 111 | /** 112 | * redis的key键规则定义 113 | * 114 | * @param redisEnum 枚举对象 115 | * @param objStr 对象.toString() 116 | * @return key 117 | */ 118 | public static String keyBuilder(RedisKeyEnum redisEnum, String objStr) { 119 | return keyBuilder(redisEnum.getKeyPrefix(), redisEnum.getModule(), redisEnum.getFunc(), objStr); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/AESUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 5 | 6 | import javax.crypto.Cipher; 7 | import javax.crypto.spec.SecretKeySpec; 8 | import java.io.UnsupportedEncodingException; 9 | import java.security.Security; 10 | import java.util.Base64; 11 | 12 | /** 13 | * 微信支付AES加解密工具类 14 | * 15 | * @author yclimb 16 | * @date 2018/6/21 17 | */ 18 | public class AESUtil { 19 | 20 | /** 21 | * 密钥算法 22 | */ 23 | private static final String ALGORITHM = "AES"; 24 | 25 | /** 26 | * 加解密算法/工作模式/填充方式 27 | */ 28 | private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding"; 29 | 30 | /** 31 | * 生成key 32 | */ 33 | private static SecretKeySpec KEY; 34 | 35 | static { 36 | try { 37 | KEY = new SecretKeySpec(WXPayUtil.MD5(WXPayConstants.API_KEY).toLowerCase().getBytes(), ALGORITHM); 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | 43 | /** 44 | * AES加密 45 | * 46 | * @param data d 47 | * @return str 48 | * @throws Exception e 49 | */ 50 | public static String encryptData(String data) throws Exception { 51 | // 创建密码器 52 | Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); 53 | Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC"); 54 | // 初始化 55 | cipher.init(Cipher.ENCRYPT_MODE, KEY); 56 | return base64Encode8859(new String(cipher.doFinal(data.getBytes()), "ISO-8859-1")); 57 | 58 | } 59 | 60 | /** 61 | * AES解密 62 | * 63 | * @param base64Data 64 64 | * @return str 65 | * @throws Exception e 66 | */ 67 | public static String decryptData(String base64Data) throws Exception { 68 | Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); 69 | Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING, "BC"); 70 | cipher.init(Cipher.DECRYPT_MODE, KEY); 71 | return new String(cipher.doFinal(base64Decode8859(base64Data).getBytes("ISO-8859-1")), "utf-8"); 72 | } 73 | 74 | /** 75 | * Base64解码 76 | * @param source base64 str 77 | * @return str 78 | */ 79 | public static String base64Decode8859(final String source) { 80 | String result = ""; 81 | final Base64.Decoder decoder = Base64.getDecoder(); 82 | try { 83 | // 此处的字符集是ISO-8859-1 84 | result = new String(decoder.decode(source), "ISO-8859-1"); 85 | } catch (final UnsupportedEncodingException e) { 86 | e.printStackTrace(); 87 | } 88 | return result; 89 | } 90 | 91 | /** 92 | * Base64加密 93 | * @param source str 94 | * @return base64 str 95 | */ 96 | public static String base64Encode8859(final String source) { 97 | String result = ""; 98 | final Base64.Encoder encoder = Base64.getEncoder(); 99 | byte[] textByte = null; 100 | try { 101 | //注意此处的编码是ISO-8859-1 102 | textByte = source.getBytes("ISO-8859-1"); 103 | result = encoder.encodeToString(textByte); 104 | } catch (final UnsupportedEncodingException e) { 105 | e.printStackTrace(); 106 | } 107 | return result; 108 | } 109 | 110 | public static void main(String[] args) throws Exception { 111 | 112 | String A = "qS/pmvAXYetUObwHm9bAod9G3SVBKQK5CiIgETwHJT4ExUpJnIg87m37KlokIsBZCnQBIO2Ear7Q/IazZ6jDNsnmsITqYt1hPYloGjdjRGlqdSSBVRjk9NIkRRQIlb+5AOHJttfVKMsbMK8FzoysE+rL8yKaOzXvsNCA2g60z3bEw3x891ZwPPiUSkVJGeIHpafWdR94Y/j3hfsrEw5KOTGiPneH5d9zhC73MW/kDWu9+wDkJCtCf5fNc9GIC5x2zKNZozpQ9wT/WLyjSz/En166xbgUt9tApaaQSayFQ0eSokMjYYLKO5KJQ355QtkvZlW96rX9IO6hVHXDgPD7kJOTh/L99ZQtG5umLBfOd9i3xVH4qH+gvi/i0gEpvQOhTvxcrZeKs8Rsliua46u/aBdUy6GlICRxQPmvKBfL9cE2L5MZGqHkCMTmSr1i4L8Ubxoi3Yv6TCTTOo4MVc64igb9HttMVfOiLFrZKAyH64Y5C6+GATUMSzWhXDn089QyrZk+W6GFkQlA6dBlO7v0aucF8t3L6SFtnxm6XkH6eD4/FFxKz+wsqKDX1s+GnPGQdwjxsS3RLGjJuNoSB7N+v4AUbMgLT2sBzew89ow7/vEUMjJMQt3eISwprOaDZqZQBgdLVUwDyWnrWi50Rr2wEuJXv/m6x8f40wN93L8GvGbMsWGXlp9V9W3LR2LZD9CnrWAlhoYoDGMAwCKuPh+dfjXmVGttGxegM+PlUR8nq6Qr1zwHz4dV3PgzWlf3n5qR72tAJ/Y0045n3dT7Iw4UNzBHC6XkUIA884paHbZ3D0V95+WrdyVQ4icsgZIneaAMZfslVsnigUjnXl3m/qZGlW5A6d93VXNe8bQgA6s6lJeEsaZc3sLVPi5Tlr2nfbgdhB4XqYkR4DebEbUzalSOqM+OOeCsYj920+FboIxvShy2ECk6bjebMM3kYw0s1NUWXynKFTvbgZ35H9TNKaeom1qYVmbb/581N8+sO3yDDFzZaaqLqOtaUtgIe2SOS2A6GRnKSanqbsJVU4j2amWEpicl3WchYV9KPeuoqodu+4UCsaY2juUIcbbof/ygkG5NkDz27RA4fBxxAlqvtzEftw=="; 113 | 114 | Security.addProvider(new BouncyCastleProvider()); 115 | 116 | System.out.println(AESUtil.decryptData(A)); 117 | 118 | /*String B = AESUtil.decryptData(A); 119 | System.out.println(B);*/ 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import java.text.DateFormat; 4 | import java.text.ParseException; 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | 8 | /** 9 | * 时间处理工具类 10 | * 11 | * @author yclimb 12 | * @date 2018/11/1 13 | */ 14 | public class DateTimeUtil { 15 | 16 | public static final String TIME_FORMAT_SHORT = "yyyyMMddHHmmss"; 17 | public static final String TIME_FORMAT_SHORT_HOUR = "yyyyMMddHH"; 18 | public static final String TIME_FORMAT_YMD = "yyyy/MM/dd HH:mm:ss"; 19 | public static final String TIME_FORMAT_NORMAL = "yyyy-MM-dd HH:mm:ss"; 20 | public static final String TIME_FORMAT_ENGLISH = "MM/dd/yyyy HH:mm:ss"; 21 | public static final String TIME_FORMAT_CHINA = "yyyy年MM月dd日 HH时mm分ss秒"; 22 | public static final String TIME_FORMAT_CHINA_M = "yyyy年MM月dd日 HH时mm分"; 23 | public static final String TIME_FORMAT_CHINA_S = "yyyy年M月d日 H时m分s秒"; 24 | public static final String TIME_FORMAT_SHORT_S = "HH:mm:ss"; 25 | 26 | public static final String DATE_FORMAT_SHORT = "yyyyMMdd"; 27 | public static final String DATE_FORMAT_NORMAL = "yyyy-MM-dd"; 28 | public static final String DATE_FORMAT_ENGLISH = "MM/dd/yyyy"; 29 | public static final String DATE_FORMAT_CHINA = "yyyy年MM月dd日"; 30 | public static final String DATE_FORMAT_CHINA_YEAR_MONTH = "yyyy年MM月"; 31 | public static final String MONTH_FORMAT = "yyyyMM"; 32 | public static final String YEAR_MONTH_FORMAT = "yyyy-MM"; 33 | public static final String DATE_FORMAT_MINUTE = "yyyyMMddHHmm"; 34 | public static final String MONTH_DAY_FORMAT = "MM-dd"; 35 | public static final String YEAR_FORMAT = "yyyy"; 36 | public static final String TIME_FORMAT_TIME = "yyyy/MM/dd HH:mm"; 37 | private static final SimpleDateFormat sdf = new SimpleDateFormat( 38 | DATE_FORMAT_NORMAL); 39 | 40 | private static final SimpleDateFormat sdfTime = new SimpleDateFormat( 41 | TIME_FORMAT_NORMAL); 42 | 43 | private static final SimpleDateFormat sdfTimes = new SimpleDateFormat( 44 | "yyyyMMddHHmmssSSS"); 45 | 46 | private static final SimpleDateFormat sdfTChina = new SimpleDateFormat( 47 | TIME_FORMAT_CHINA); 48 | 49 | /** 50 | * 把日期字符串转换为日期类型 51 | * 52 | * @param dateStr 日期字符串 53 | * @return 日期 54 | * @since 0.1 55 | */ 56 | public static Date convertAsDate(String dateStr) { 57 | if (dateStr == null || "".equals(dateStr)) { 58 | return null; 59 | } 60 | DateFormat fmt = null; 61 | if (dateStr.matches("\\d{14}")) { 62 | fmt = new SimpleDateFormat(TIME_FORMAT_SHORT); 63 | } else if (dateStr 64 | .matches("\\d{4}-\\d{1,2}-\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}")) { 65 | fmt = new SimpleDateFormat(TIME_FORMAT_NORMAL); 66 | } else if (dateStr 67 | .matches("\\d{1,2}/\\d{1,2}/\\d{4} \\d{1,2}:\\d{1,2}:\\d{1,2}")) { 68 | fmt = new SimpleDateFormat(TIME_FORMAT_ENGLISH); 69 | } else if (dateStr 70 | .matches("\\d{4}年\\d{1,2}月\\d{1,2}日 \\d{1,2}时\\d{1,2}分\\d{1,2}秒")) { 71 | fmt = new SimpleDateFormat(TIME_FORMAT_CHINA); 72 | } else if (dateStr.matches("\\d{8}")) { 73 | fmt = new SimpleDateFormat(DATE_FORMAT_SHORT); 74 | } else if (dateStr.matches("\\d{4}-\\d{1,2}-\\d{1,2}")) { 75 | fmt = new SimpleDateFormat(DATE_FORMAT_NORMAL); 76 | } else if (dateStr.matches("\\d{1,2}/\\d{1,2}/\\d{4}")) { 77 | fmt = new SimpleDateFormat(DATE_FORMAT_ENGLISH); 78 | } else if (dateStr.matches("\\d{4}年\\d{1,2}月\\d{1,2}日")) { 79 | fmt = new SimpleDateFormat(DATE_FORMAT_CHINA); 80 | } else if (dateStr.matches("\\d{4}\\d{1,2}\\d{1,2}\\d{1,2}\\d{1,2}")) { 81 | fmt = new SimpleDateFormat(DATE_FORMAT_MINUTE); 82 | } else if (dateStr.matches("\\d{1,2}:\\d{1,2}:\\d{1,2}")) { 83 | fmt = new SimpleDateFormat(TIME_FORMAT_SHORT_S); 84 | } 85 | try { 86 | return fmt.parse(dateStr); 87 | } catch (ParseException e) { 88 | throw new IllegalArgumentException( 89 | "Date or Time String is invalid."); 90 | } 91 | } 92 | 93 | /** 94 | * 得到时间字符串,格式为 yyyyMMddHHmmss 95 | * @return 返回当前时间的字符串 96 | * 97 | * @author yclimb 98 | * @date 2018/11/1 99 | */ 100 | public static String getTimeShortString(Date date) { 101 | return new SimpleDateFormat(TIME_FORMAT_SHORT).format(date); 102 | } 103 | 104 | /** 105 | * 得到十位数的时间戳 106 | * @param date 时间对象 107 | * @return long 108 | * 109 | * @author yclimb 110 | * @date 2018/9/18 111 | */ 112 | public static long getTenTimeByDate(Date date) { 113 | return date.getTime() / 1000; 114 | } 115 | 116 | /** 117 | * 得到十位数的时间戳 118 | * @param dateStr 时间字符串 119 | * @return long 120 | * 121 | * @author yclimb 122 | * @date 2018/9/18 123 | */ 124 | public static long getTenTimeByDate(String dateStr) { 125 | return convertAsDate(dateStr).getTime() / 1000; 126 | } 127 | 128 | 129 | /** 130 | * Description: 比较两个字符串格式的时间大小
131 | * 如果第二个时间大于第一个时间返回true,否则返回false 132 | * 133 | * @param strFirst 第一个时间 134 | * @param strSecond 第二个时间 135 | * @param strFormat 时间格式化方式 eg:"yyyy-MM-dd HH:mm:ss"," yyyy-MM-dd" 136 | * @return true-第二个时间晚于第一个时间,false-第二个时间不晚于第一个时间 137 | * 138 | * @author yclimb 139 | * @date 2018/11/1 140 | */ 141 | public static boolean latterThan(String strFirst, String strSecond, 142 | String strFormat) { 143 | SimpleDateFormat ft = new SimpleDateFormat(strFormat); 144 | try { 145 | Date date1 = ft.parse(strFirst); 146 | Date date2 = ft.parse(strSecond); 147 | long quot = date2.getTime() - date1.getTime(); 148 | if (0 < quot) { 149 | return true; 150 | } else { 151 | return false; 152 | } 153 | } catch (ParseException e) { 154 | e.printStackTrace(); 155 | } 156 | return false; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/WXPayUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import com.weixin.pay.constants.WXPayConstants; 4 | import com.weixin.pay.constants.WXPayConstants.SignType; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.w3c.dom.Node; 8 | import org.w3c.dom.NodeList; 9 | 10 | import javax.crypto.Mac; 11 | import javax.crypto.spec.SecretKeySpec; 12 | import javax.xml.parsers.DocumentBuilder; 13 | import javax.xml.transform.OutputKeys; 14 | import javax.xml.transform.Transformer; 15 | import javax.xml.transform.TransformerFactory; 16 | import javax.xml.transform.dom.DOMSource; 17 | import javax.xml.transform.stream.StreamResult; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.InputStream; 20 | import java.io.StringWriter; 21 | import java.security.MessageDigest; 22 | import java.security.SecureRandom; 23 | import java.util.*; 24 | 25 | /** 26 | * 微信支付工具类 27 | * 28 | * @author yclimb 29 | * @date 2018/8/17 30 | */ 31 | public class WXPayUtil { 32 | 33 | private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 34 | 35 | private static final Random RANDOM = new SecureRandom(); 36 | 37 | /** 38 | * XML格式字符串转换为Map 39 | * 40 | * @param strXML XML字符串 41 | * @return XML数据转换后的Map 42 | * @throws Exception 43 | */ 44 | public static Map xmlToMap(String strXML) throws Exception { 45 | try { 46 | Map data = new HashMap(); 47 | DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder(); 48 | InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); 49 | org.w3c.dom.Document doc = documentBuilder.parse(stream); 50 | doc.getDocumentElement().normalize(); 51 | NodeList nodeList = doc.getDocumentElement().getChildNodes(); 52 | for (int idx = 0; idx < nodeList.getLength(); ++idx) { 53 | Node node = nodeList.item(idx); 54 | if (node.getNodeType() == Node.ELEMENT_NODE) { 55 | org.w3c.dom.Element element = (org.w3c.dom.Element) node; 56 | data.put(element.getNodeName(), element.getTextContent()); 57 | } 58 | } 59 | try { 60 | stream.close(); 61 | } catch (Exception ex) { 62 | // do nothing 63 | } 64 | return data; 65 | } catch (Exception ex) { 66 | WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML); 67 | throw ex; 68 | } 69 | 70 | } 71 | 72 | /** 73 | * 将Map转换为XML格式的字符串 74 | * 75 | * @param data Map类型数据 76 | * @return XML格式的字符串 77 | * @throws Exception 78 | */ 79 | public static String mapToXml(Map data) throws Exception { 80 | org.w3c.dom.Document document = WXPayXmlUtil.newDocument(); 81 | org.w3c.dom.Element root = document.createElement("xml"); 82 | document.appendChild(root); 83 | for (String key: data.keySet()) { 84 | String value = data.get(key); 85 | if (value == null) { 86 | value = ""; 87 | } 88 | value = value.trim(); 89 | org.w3c.dom.Element filed = document.createElement(key); 90 | filed.appendChild(document.createTextNode(value)); 91 | root.appendChild(filed); 92 | } 93 | TransformerFactory tf = TransformerFactory.newInstance(); 94 | Transformer transformer = tf.newTransformer(); 95 | DOMSource source = new DOMSource(document); 96 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 97 | transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 98 | StringWriter writer = new StringWriter(); 99 | StreamResult result = new StreamResult(writer); 100 | transformer.transform(source, result); 101 | String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", ""); 102 | try { 103 | writer.close(); 104 | } 105 | catch (Exception ex) { 106 | } 107 | return output; 108 | } 109 | 110 | 111 | /** 112 | * 生成带有 sign 的 XML 格式字符串 113 | * 114 | * @param data Map类型数据 115 | * @param key API密钥 116 | * @return 含有sign字段的XML 117 | */ 118 | public static String generateSignedXml(final Map data, String key) throws Exception { 119 | return generateSignedXml(data, key, WXPayConstants.SignType.MD5); 120 | } 121 | 122 | /** 123 | * 生成带有 sign 的 XML 格式字符串 124 | * 125 | * @param data Map类型数据 126 | * @param key API密钥 127 | * @param signType 签名类型 128 | * @return 含有sign字段的XML 129 | */ 130 | public static String generateSignedXml(final Map data, String key, SignType signType) throws Exception { 131 | String sign = generateSignature(data, key, signType); 132 | data.put(WXPayConstants.FIELD_SIGN, sign); 133 | return mapToXml(data); 134 | } 135 | 136 | 137 | /** 138 | * 判断签名是否正确 139 | * 140 | * @param xmlStr XML格式数据 141 | * @param key API密钥 142 | * @return 签名是否正确 143 | * @throws Exception 144 | */ 145 | public static boolean isSignatureValid(String xmlStr, String key) throws Exception { 146 | Map data = xmlToMap(xmlStr); 147 | if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { 148 | return false; 149 | } 150 | String sign = data.get(WXPayConstants.FIELD_SIGN); 151 | return generateSignature(data, key).equals(sign); 152 | } 153 | 154 | /** 155 | * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。 156 | * 157 | * @param data Map类型数据 158 | * @param key API密钥 159 | * @return 签名是否正确 160 | * @throws Exception 161 | */ 162 | public static boolean isSignatureValid(Map data, String key) throws Exception { 163 | return isSignatureValid(data, key, SignType.MD5); 164 | } 165 | 166 | /** 167 | * 判断签名是否正确,必须包含sign字段,否则返回false。 168 | * 169 | * @param data Map类型数据 170 | * @param key API密钥 171 | * @param signType 签名方式 172 | * @return 签名是否正确 173 | * @throws Exception 174 | */ 175 | public static boolean isSignatureValid(Map data, String key, SignType signType) throws Exception { 176 | if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { 177 | return false; 178 | } 179 | String sign = data.get(WXPayConstants.FIELD_SIGN); 180 | return generateSignature(data, key, signType).equals(sign); 181 | } 182 | 183 | /** 184 | * 生成签名 185 | * 186 | * @param data 待签名数据 187 | * @param key API密钥 188 | * @return 签名 189 | */ 190 | public static String generateSignature(final Map data, String key) throws Exception { 191 | return generateSignature(data, key, SignType.MD5); 192 | } 193 | 194 | /** 195 | * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。 196 | * 197 | * @param data 待签名数据 198 | * @param key API密钥 199 | * @param signType 签名方式 200 | * @return 签名 201 | */ 202 | public static String generateSignature(final Map data, String key, SignType signType) throws Exception { 203 | Set keySet = data.keySet(); 204 | String[] keyArray = keySet.toArray(new String[keySet.size()]); 205 | Arrays.sort(keyArray); 206 | StringBuilder sb = new StringBuilder(); 207 | for (String k : keyArray) { 208 | if (k.equals(WXPayConstants.FIELD_SIGN)) { 209 | continue; 210 | } 211 | if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名 212 | sb.append(k).append("=").append(data.get(k).trim()).append("&"); 213 | } 214 | sb.append("key=").append(key); 215 | if (SignType.MD5.equals(signType)) { 216 | WXPayUtil.getLogger().info("signPay=" + sb.toString()); 217 | return MD5(sb.toString()).toUpperCase(); 218 | } 219 | else if (SignType.HMACSHA256.equals(signType)) { 220 | return HMACSHA256(sb.toString(), key); 221 | } 222 | else { 223 | throw new Exception(String.format("Invalid sign_type: %s", signType)); 224 | } 225 | } 226 | 227 | 228 | /** 229 | * 获取随机字符串 Nonce Str 230 | * 231 | * @return String 随机字符串 232 | */ 233 | public static String generateNonceStr() { 234 | char[] nonceChars = new char[32]; 235 | for (int index = 0; index < nonceChars.length; ++index) { 236 | nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); 237 | } 238 | return new String(nonceChars); 239 | } 240 | 241 | /** 242 | * 根据字典排序 243 | * @param type 1:根据key键排序;2:根据value值排序 244 | * @param data d 245 | * @return sb 246 | */ 247 | public static String dictionaryOrder(final Map data, int type) { 248 | Set keySet = data.keySet(); 249 | String[] keyArray = keySet.toArray(new String[keySet.size()]); 250 | StringBuilder sb = new StringBuilder(); 251 | if (type == 2) { 252 | String[] valArray = new String[keySet.size()]; 253 | for (int i = 0; i < keySet.size(); i++) { 254 | valArray[i] = data.get(keyArray[i]); 255 | } 256 | Arrays.sort(valArray); 257 | for (String v : valArray) { 258 | // 参数值为空,则不参与签名 259 | if (v.trim().length() > 0) { 260 | sb.append(v); 261 | } 262 | } 263 | } else { 264 | Arrays.sort(keyArray); 265 | for (int i = 0; i < keyArray.length; i++) { 266 | // 参数值为空,则不参与签名 267 | if (data.get(keyArray[i]).trim().length() > 0) { 268 | if (i == keyArray.length - 1) { 269 | sb.append(keyArray[i]).append("=").append(data.get(keyArray[i]).trim()); 270 | } else { 271 | sb.append(keyArray[i]).append("=").append(data.get(keyArray[i]).trim()).append("&"); 272 | } 273 | } 274 | } 275 | } 276 | return sb.toString(); 277 | } 278 | 279 | 280 | /** 281 | * 生成 MD5 282 | * 283 | * @param data 待处理数据 284 | * @return MD5结果 285 | */ 286 | public static String MD5(String data) throws Exception { 287 | MessageDigest md = MessageDigest.getInstance("MD5"); 288 | byte[] array = md.digest(data.getBytes("UTF-8")); 289 | StringBuilder sb = new StringBuilder(); 290 | for (byte item : array) { 291 | sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 292 | } 293 | return sb.toString().toUpperCase(); 294 | } 295 | 296 | /** 297 | * 生成 HMACSHA256 298 | * @param data 待处理数据 299 | * @param key 密钥 300 | * @return 加密结果 301 | * @throws Exception e 302 | */ 303 | public static String HMACSHA256(String data, String key) throws Exception { 304 | Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 305 | SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 306 | sha256_HMAC.init(secret_key); 307 | byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); 308 | StringBuilder sb = new StringBuilder(); 309 | for (byte item : array) { 310 | sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 311 | } 312 | return sb.toString().toUpperCase(); 313 | } 314 | 315 | /** 316 | * SHA1 安全加密算法 317 | * @param data 待处理数据 318 | * @return str 319 | * @throws Exception e 320 | */ 321 | public static String SHA1(String data) throws Exception { 322 | //指定sha1算法 323 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 324 | digest.update(data.getBytes("UTF-8")); 325 | //获取字节数组 326 | byte messageDigest[] = digest.digest(); 327 | // Create Hex String 328 | StringBuilder hexString = new StringBuilder(); 329 | // 字节数组转换为 十六进制 数 330 | for (int i = 0; i < messageDigest.length; i++) { 331 | String shaHex = Integer.toHexString(messageDigest[i] & 0xFF); 332 | if (shaHex.length() < 2) { 333 | hexString.append(0); 334 | } 335 | hexString.append(shaHex); 336 | } 337 | return hexString.toString(); 338 | 339 | } 340 | 341 | /** 342 | * 日志 343 | * @return log 344 | */ 345 | public static Logger getLogger() { 346 | return LoggerFactory.getLogger("wxpay java sdk"); 347 | } 348 | 349 | /** 350 | * 获取当前时间戳,单位秒 351 | * @return long 352 | */ 353 | public static long getCurrentTimestamp() { 354 | return System.currentTimeMillis() / 1000; 355 | } 356 | 357 | /** 358 | * 获取当前时间戳,单位毫秒 359 | * @return long 360 | */ 361 | public static long getCurrentTimestampMs() { 362 | return System.currentTimeMillis(); 363 | } 364 | 365 | /** 366 | * 生成 uuid, 即用来标识一笔单,也用做 nonce_str 367 | * @return str 368 | */ 369 | public static String generateUUID() { 370 | return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); 371 | } 372 | 373 | /** 374 | * 通用商户单号(每个单号必须唯一)28位 375 | * 组成:mch_id+yyyyMMddHHmmss+4位随机数 376 | * 377 | * @author yclimb 378 | * @date 2018/9/18 379 | */ 380 | public static String getPayNo() { 381 | String yyyyMMddHHmmss = DateTimeUtil.getTimeShortString(new Date()); 382 | int str4 = (int) (Math.random() * 9000) + 1000; 383 | return WXPayConstants.MCH_ID + yyyyMMddHHmmss + str4; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/WXPayXmlUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import org.w3c.dom.Document; 4 | 5 | import javax.xml.XMLConstants; 6 | import javax.xml.parsers.DocumentBuilder; 7 | import javax.xml.parsers.DocumentBuilderFactory; 8 | import javax.xml.parsers.ParserConfigurationException; 9 | 10 | /** 11 | * 微信支付xml转换工具类 12 | * 13 | * @author yclimb 14 | * @date 2018/8/17 15 | */ 16 | public final class WXPayXmlUtil { 17 | 18 | /** 19 | * 生成一个微信的xml文档 20 | * @return DocumentBuilder 21 | * @throws ParserConfigurationException e 22 | */ 23 | public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { 24 | DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 25 | documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 26 | documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); 27 | documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 28 | documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 29 | documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 30 | documentBuilderFactory.setXIncludeAware(false); 31 | documentBuilderFactory.setExpandEntityReferences(false); 32 | return documentBuilderFactory.newDocumentBuilder(); 33 | } 34 | 35 | /** 36 | * 新建doc文档 37 | * @return Document 38 | * @throws ParserConfigurationException e 39 | */ 40 | public static Document newDocument() throws ParserConfigurationException { 41 | return newDocumentBuilder().newDocument(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/WXSignatureUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.weixin.pay.constants.WXPayConstants; 5 | import org.apache.http.HttpEntity; 6 | import org.apache.http.HttpResponse; 7 | import org.apache.http.client.ClientProtocolException; 8 | import org.apache.http.client.HttpClient; 9 | import org.apache.http.client.methods.HttpGet; 10 | import org.apache.http.impl.client.DefaultHttpClient; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpSession; 14 | import java.io.*; 15 | import java.security.MessageDigest; 16 | import java.security.NoSuchAlgorithmException; 17 | import java.util.Formatter; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.UUID; 21 | 22 | /** 23 | * 微信签名工具 24 | * 25 | * @author yclimb 26 | * @date 2018/8/17 27 | */ 28 | public class WXSignatureUtil { 29 | 30 | /** 31 | * 获取微信签名信息 32 | * @param request 33 | * @param requestUrl 请求页面地址 34 | * @return 返回map:noncestr:随机字符串;timestamp:签名时间戳;appid;微信公众号Id;signature:签名串 35 | * @throws ClientProtocolException 36 | * @throws IOException 37 | * @throws CloneNotSupportedException 38 | */ 39 | public static Map getSignature(HttpServletRequest request, String requestUrl) throws ClientProtocolException, IOException, CloneNotSupportedException{ 40 | Map map = new HashMap(); 41 | 42 | HttpSession session = request.getSession(); 43 | 44 | // 直接查询签名信息 45 | Object objMap = session.getAttribute(requestUrl); 46 | if (objMap != null) { 47 | return (Map) objMap; 48 | } 49 | 50 | String appid = WXPayConstants.APP_ID; 51 | String secret = WXPayConstants.SECRET; 52 | String token; 53 | String jsapi_ticket; 54 | 55 | Object tokenObj = session.getAttribute("token"); 56 | if (tokenObj != null) { 57 | token = String.valueOf(tokenObj); 58 | } else { 59 | token = getToken(appid, secret); 60 | session.setAttribute("token", token); 61 | } 62 | 63 | Object jsapiTicketObj = session.getAttribute("jsapi_ticket"); 64 | if (jsapiTicketObj != null) { 65 | jsapi_ticket = String.valueOf(jsapiTicketObj); 66 | } else { 67 | jsapi_ticket = getTicket(token); 68 | session.setAttribute("jsapi_ticket", jsapi_ticket); 69 | } 70 | 71 | String nonce_str = createNonceStr(); 72 | String timestamp = createTimestamp(); 73 | 74 | String signature = ""; 75 | 76 | //注意这里参数名必须全部小写,且必须有序 77 | String string1 = "jsapi_ticket=" + jsapi_ticket + 78 | "&noncestr=" + nonce_str + 79 | "×tamp=" + timestamp + 80 | "&url=" + requestUrl; 81 | WXPayUtil.getLogger().info(string1); 82 | 83 | try { 84 | MessageDigest crypt = MessageDigest.getInstance("SHA-1"); 85 | crypt.reset(); 86 | crypt.update(string1.getBytes("UTF-8")); 87 | signature = byteToHex(crypt.digest()); 88 | } catch (NoSuchAlgorithmException e) { 89 | e.printStackTrace(); 90 | } catch (UnsupportedEncodingException e) { 91 | e.printStackTrace(); 92 | } 93 | 94 | map.put("noncestr", nonce_str); 95 | map.put("timestamp", timestamp); 96 | map.put("appid", appid); 97 | map.put("signature", signature); 98 | 99 | session.setAttribute(requestUrl, map); 100 | 101 | return map; 102 | } 103 | 104 | private static String byteToHex(final byte[] hash) { 105 | Formatter formatter = new Formatter(); 106 | for (byte b : hash) 107 | { 108 | formatter.format("%02x", b); 109 | } 110 | String result = formatter.toString(); 111 | formatter.close(); 112 | return result; 113 | } 114 | 115 | public static String createNonceStr() { 116 | return UUID.randomUUID().toString(); 117 | } 118 | 119 | public static String createTimestamp() { 120 | return Long.toString(System.currentTimeMillis() / 1000); 121 | } 122 | 123 | /** 124 | * 125 | * @param appid 公众号应用id 126 | * @param secret 公众号应用密钥 127 | * @return 返回token 128 | * @throws IOException 129 | * @throws CloneNotSupportedException 130 | */ 131 | public static String getToken(String appid,String secret) throws IOException, CloneNotSupportedException{ 132 | String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+appid+"&secret="+secret; 133 | WXPayUtil.getLogger().info("url:" + url); 134 | // 生成一个请求对象 135 | HttpGet httpGet = new HttpGet(url); 136 | // 生成一个Http客户端对象 137 | HttpClient httpClient = new DefaultHttpClient(); 138 | // 下面使用Http客户端发送请求,并获取响应内容 139 | InputStream inputStream = null; 140 | // 发送请求并获得响应对象 141 | HttpResponse mHttpResponse = null; 142 | 143 | BufferedReader bufferedReader = null; 144 | String result = ""; 145 | String line; 146 | try { 147 | mHttpResponse = httpClient.execute(httpGet); 148 | // 获得响应的消息实体 149 | HttpEntity mHttpEntity = mHttpResponse.getEntity(); 150 | 151 | // 获取一个输入流 152 | inputStream = mHttpEntity.getContent(); 153 | bufferedReader = new BufferedReader( 154 | new InputStreamReader(inputStream)); 155 | while ((line = bufferedReader.readLine()) != null) { 156 | result += line; 157 | } 158 | JSONObject json = JSONObject.parseObject(result); 159 | return json.get("access_token").toString(); 160 | } catch (IOException e) { 161 | throw new IOException("获取access_token异常!"); 162 | }finally { 163 | bufferedReader.close(); 164 | inputStream.close(); 165 | httpGet.clone(); 166 | } 167 | } 168 | 169 | /** 170 | * 171 | * @param access_token 生成的token 172 | * @return 返回jsapi_ticket 173 | * @throws ClientProtocolException 174 | * @throws IOException 175 | */ 176 | public static String getTicket(String access_token) throws ClientProtocolException, IOException{ 177 | HttpGet httpGet = new HttpGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+access_token+"&type=jsapi"); 178 | // 生成一个Http客户端对象 179 | HttpClient httpClient = new DefaultHttpClient(); 180 | // 下面使用Http客户端发送请求,并获取响应内容 181 | InputStream inputStream = null; 182 | // 发送请求并获得响应对象 183 | HttpResponse mHttpResponse = null; 184 | 185 | BufferedReader bufferedReader = null; 186 | String result = ""; 187 | String line; 188 | try { 189 | mHttpResponse = httpClient.execute(httpGet); 190 | HttpEntity mHttpEntity = mHttpResponse.getEntity(); 191 | inputStream = mHttpEntity.getContent(); 192 | bufferedReader = new BufferedReader( 193 | new InputStreamReader(inputStream)); 194 | while ((line = bufferedReader.readLine()) != null) { 195 | result += line; 196 | } 197 | JSONObject json2 = JSONObject.parseObject(result); 198 | 199 | return json2.get("ticket").toString(); 200 | } catch (IOException e) { 201 | throw new IOException("获取jsapi_ticket异常!"); 202 | }finally { 203 | bufferedReader.close(); 204 | inputStream.close(); 205 | } 206 | } 207 | 208 | /** 209 | * 210 | * @Title getPhotoWeixinUrl 211 | * @Description 获取微信图片上传路径 212 | * @param media_id 213 | * @return 214 | * @throws NoSuchAlgorithmException 215 | * @throws CloneNotSupportedException 216 | * @throws IOException 217 | * @throws 218 | */ 219 | public static String getPhotoWeixinUrl(String media_id) throws NoSuchAlgorithmException, IOException, CloneNotSupportedException{ 220 | String appid = WXPayConstants.APP_ID; 221 | String secret = WXPayConstants.SECRET; 222 | String token = getToken(appid,secret); 223 | String url = "http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=" + token + "&media_id=" + media_id; 224 | return url; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/WXUserUtil.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.commons.codec.binary.Base64; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | 8 | /** 9 | * 对于微信用户名称emoji等特殊字符处理 10 | * 11 | * @author yclimb 12 | * @date 2018/8/17 13 | */ 14 | @Slf4j 15 | public class WXUserUtil { 16 | 17 | /** 18 | * 编码用户昵称 19 | * 20 | * @param nickName 未编码等名称 21 | * @return base64 str 22 | */ 23 | public static String encodeNickName(String nickName) { 24 | try { 25 | return Base64.encodeBase64String(nickName.toString().getBytes("utf-8")); 26 | } catch (UnsupportedEncodingException e) { 27 | log.error("编码用户昵称报错", e); 28 | } 29 | return null; 30 | } 31 | 32 | /** 33 | * 解码用户昵称 34 | * 35 | * @param nickName base64 str 36 | * @return 原始名称 37 | */ 38 | public static String decodeNickName(String nickName) { 39 | return new String(Base64.decodeBase64(nickName)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/weixin/pay/util/WXUtils.java: -------------------------------------------------------------------------------- 1 | package com.weixin.pay.util; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONArray; 5 | import com.alibaba.fastjson.JSONObject; 6 | import com.weixin.pay.card.CardBgColorEnum; 7 | import com.weixin.pay.constants.WXConstants; 8 | import com.weixin.pay.constants.WXPayConstants; 9 | import com.weixin.pay.constants.WXURL; 10 | import com.weixin.pay.redis.RedisKeyEnum; 11 | import com.weixin.pay.redis.RedisKeyUtil; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.http.HttpEntity; 16 | import org.springframework.http.HttpHeaders; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.web.client.RestTemplate; 21 | 22 | import javax.annotation.Resource; 23 | import java.io.ByteArrayInputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.math.BigDecimal; 27 | import java.text.MessageFormat; 28 | import java.util.HashMap; 29 | import java.util.Map; 30 | import java.util.UUID; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | /** 34 | * 微信小工具类 35 | * 36 | * @author yclimb 37 | * @date 2018/8/17 38 | */ 39 | @Slf4j 40 | @Component 41 | public class WXUtils { 42 | 43 | @Resource 44 | private RestTemplate restTemplate; 45 | 46 | @Resource 47 | private RedisTemplate redisTemplate; 48 | 49 | /** 50 | * 获取微信全局accessToken 51 | * 52 | * @param code 标识 53 | * @return accessToken 54 | */ 55 | public String getAccessToken(String code) { 56 | 57 | // 取redis数据 58 | String key = WXConstants.WECHAT_ACCESSTOKEN + code; 59 | String accessToken = (String) redisTemplate.opsForValue().get(key); 60 | if (accessToken != null) { 61 | return accessToken; 62 | } 63 | 64 | // 通过接口取得access_token 65 | JSONObject jsonObject = restTemplate.getForObject(MessageFormat.format(WXURL.BASE_ACCESS_TOKEN, WXPayConstants.APP_ID, WXPayConstants.SECRET), JSONObject.class); 66 | String token = (String) jsonObject.get("access_token"); 67 | if (StringUtils.isNotBlank(token)) { 68 | // 存储redis 69 | redisTemplate.opsForValue().set(key, token, 7000, TimeUnit.SECONDS); 70 | return token; 71 | } else { 72 | log.error("获取微信accessToken出错,微信返回信息为:[{}]", jsonObject.toString()); 73 | } 74 | return null; 75 | } 76 | 77 | /** 78 | * 获取小程序静默登录返回信息 79 | * 80 | * @param code code 81 | * @param appId appId 82 | * @param appSecret appSecret 83 | * @return json 84 | */ 85 | public JSONObject getMiniBaseUserInfo(String code, String appId, String appSecret) { 86 | log.info("getMiniBaseUserInfo:params:[{}]", code); 87 | String data = restTemplate.getForObject(WXURL.WX_MINI_LOGIN, String.class, appId, appSecret, code); 88 | log.info("getMiniBaseUserInfo:result:[{}]", data); 89 | return JSONObject.parseObject(data); 90 | 91 | } 92 | 93 | /** 94 | * 网页授权获取用户信息时用于获取access_token以及openid 95 | * 请求路径:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code(最后一个参数不变) 96 | * @param code c 97 | * @return access_token json obj 98 | * 99 | * @author yclimb 100 | * @date 2018/7/30 101 | */ 102 | public JSONObject getJsapiAccessTokenByCode(String code) { 103 | if (StringUtils.isBlank(code)) { 104 | return null; 105 | } 106 | try { 107 | // 获取access_token 108 | String access_token_json = restTemplate.getForObject(WXURL.OAUTH_ACCESS_TOKEN_URL, String.class, 109 | WXPayConstants.APP_ID_XXX, WXPayConstants.SECRET_XXX, code); 110 | log.info("getAccessToken:access_token_json:{}", access_token_json); 111 | if (StringUtils.isBlank(access_token_json)) { 112 | return null; 113 | } 114 | JSONObject jsonObject = JSON.parseObject(access_token_json); 115 | if (StringUtils.isBlank(jsonObject.getString("access_token"))) { 116 | return null; 117 | } 118 | return jsonObject; 119 | } catch (Exception e) { 120 | log.error(e.getMessage(), e); 121 | } 122 | return null; 123 | } 124 | 125 | /** 126 | * 通过access_token和openid请求获取用户信息 127 | * 请求路径:https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 128 | * @param access_token t 129 | * @param openid o 130 | * @return userinfo json obj 131 | * 132 | * @author yclimb 133 | * @date 2018/7/30 134 | */ 135 | public JSONObject getJsapiUserinfo(String access_token, String openid) { 136 | if (StringUtils.isBlank(access_token) || StringUtils.isBlank(openid)) { 137 | return null; 138 | } 139 | try { 140 | // 获取access_token和openid 141 | String userinfo_json = restTemplate.getForObject(WXURL.OAUTH_GET_USERINFO_URL, String.class, access_token, openid); 142 | log.info("getUserinfo:userinfo_json:{}", userinfo_json); 143 | if (StringUtils.isBlank(userinfo_json)) { 144 | return null; 145 | } 146 | JSONObject jsonObject = JSON.parseObject(userinfo_json); 147 | if (0 != jsonObject.getIntValue("errcode")) { 148 | return null; 149 | } 150 | return jsonObject; 151 | } catch (Exception e) { 152 | log.error(e.getMessage(), e); 153 | } 154 | return null; 155 | } 156 | 157 | /** 158 | * 生成带参数的小程序二维码[] 159 | * 160 | * @param scene 参数 161 | * @param page 小程序页面 162 | * @return img path 163 | */ 164 | public String getWxMiniQRImg(String scene, String page) { 165 | InputStream inputStream = null; 166 | String imgUrl = ""; 167 | try { 168 | 169 | // redis key 170 | String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.XXX_MINI_WX_CODE, scene + RedisKeyUtil.KEY_SPLIT_CHAR + page); 171 | 172 | // 从redis中获取缓存图片 173 | Object obj = redisTemplate.opsForValue().get(redisKey); 174 | if (obj != null) { 175 | return obj.toString(); 176 | } 177 | 178 | // 获取微信永久无限制二维码 179 | byte[] code = this.getwxacodeunlimit(scene, page); 180 | if (code == null || code.length <= 0) { 181 | return imgUrl; 182 | } 183 | 184 | // 将返回字节数组转为输入流 185 | inputStream = new ByteArrayInputStream(code); 186 | 187 | // 取得uuid的文件名称 188 | String newFileName = UUID.randomUUID().toString().replaceAll("-", "").replace(".", "") + ".png"; 189 | log.info("getWxMiniQRImg:fileName:" + newFileName); 190 | 191 | // 上传图片到OSS服务器 192 | // imgUrl = ossUtils.uploadOss(inputStream, ossUtils.getImgPathYYYYMMDD(), newFileName); 193 | 194 | // 图片为空直接返回 195 | if (StringUtils.isBlank(imgUrl)) { 196 | return imgUrl; 197 | } 198 | // 设置到redis中,下次取直接拿缓存即可,防止多次生成 199 | redisTemplate.opsForValue().set(redisKey, imgUrl); 200 | 201 | } catch (Exception e) { 202 | log.error("getWxMiniQRImg:调用小程序生成微信永久小程序码URL接口异常", e); 203 | } finally { 204 | if (inputStream != null) { 205 | try { 206 | inputStream.close(); 207 | } catch (IOException e) { 208 | log.error(e.getMessage(), e); 209 | } 210 | } 211 | } 212 | return imgUrl; 213 | } 214 | 215 | /** 216 | * 获取 application/json;charset=UTF-8 的 HttpHeaders 对象 217 | * 218 | * @return HttpHeaders 219 | * @author yclimb 220 | * @date 2018/7/18 221 | */ 222 | public HttpHeaders getHttpHeadersUTF8JSON() { 223 | HttpHeaders headers = new HttpHeaders(); 224 | headers.setContentType(MediaType.APPLICATION_JSON_UTF8); 225 | headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); 226 | return headers; 227 | } 228 | 229 | /** 230 | * 作用:生成永久无限制微信二维码
231 | * 场景:微信二维码生成,根据参数和页面配置微信二维码,返回二维码字节流 232 | * 接口链接:https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN 233 | * 接口文档地址:https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html 234 | * 235 | * @param scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) 236 | * @param page 必须是已经发布的小程序存在的页面(否则报错),例如 "pages/index/index" ,根路径前不要填加'/',不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 237 | * @return 二维码字节流 238 | * @author yclimb 239 | * @date 2018/7/18 240 | */ 241 | public byte[] getwxacodeunlimit(String scene, String page) { 242 | try { 243 | 244 | // 获取access token 245 | String accessToken = this.getAccessToken("xxx"); 246 | 247 | // 拼接传入参数 248 | Map param = new HashMap<>(5); 249 | param.put("scene", scene); 250 | param.put("page", page); 251 | // 默认:430;二维码的宽度,最小为280 252 | param.put("width", 280); 253 | // 默认:false;自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 254 | param.put("auto_color", false); 255 | 256 | // 默认:{"r":"0","g":"0","b":"0"};二维码图片颜色参数,auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 257 | Map line_color = new HashMap<>(3); 258 | line_color.put("r", 0); 259 | line_color.put("g", 0); 260 | line_color.put("b", 0); 261 | param.put("line_color", line_color); 262 | 263 | // map转换为json传输 264 | String jsonParam = JSON.toJSONString(param); 265 | log.info("getwxacodeunlimit:param:" + jsonParam); 266 | 267 | // 请求微信接口,得到返回结果[二进制流] 268 | HttpEntity entity = new HttpEntity<>(jsonParam, this.getHttpHeadersUTF8JSON()); 269 | ResponseEntity responseEntity = restTemplate.postForEntity(WXURL.WX_MINI_QR_CODE_URL, entity, byte[].class, accessToken); 270 | 271 | // return byte[] 272 | return responseEntity.getBody(); 273 | } catch (Exception e) { 274 | log.error("getwxacodeunlimit:postForEntity:" + e.getMessage(), e); 275 | } 276 | 277 | return null; 278 | } 279 | 280 | /** 281 | * 创建支付后领取立减金活动接口 282 | * 通过此接口创建立减金活动。 283 | * 将已创建的代金券cardid、跳转小程序appid、发起支付的商户号等信息通过此接口创建立减金活动,成功返回活动id即为创建成功。 284 | * 接口地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21515658940X5pIn 285 | * 286 | * @param begin_time 活动开始时间,精确到秒 287 | * @param end_time 活动结束时间,精确到秒 288 | * @param gift_num 单个礼包社交立减金数量(3-15个) 289 | * @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50,默认为1 290 | * @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50,默认为1 291 | * @param card_id 卡券ID 292 | * @param min_amt 最少支付金额,单位是元 293 | * @param membership_appid 奖品指定的会员卡appid。如用户标签有选择商户会员,则需要填写会员卡appid,该appid需要跟所有发放商户号有绑定关系。 294 | * @param new_tinyapp_user 可以指定为是否小程序新用户(membership_appid为空、new_tinyapp_user为false时,指定为所有用户) 295 | * @return json 296 | * @author yclimb 297 | * @date 2018/9/18 298 | */ 299 | public JSONObject createCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act, 300 | int max_partic_times_one_day, String card_id, String min_amt, 301 | String membership_appid, boolean new_tinyapp_user) { 302 | try { 303 | 304 | // 创建活动接口之前的验证 305 | String msg = checkCardActivity(begin_time, end_time, gift_num, max_partic_times_act, max_partic_times_one_day, min_amt); 306 | if (null != msg) { 307 | JSONObject resultJson = new JSONObject(2); 308 | resultJson.put("errcode", "1"); 309 | resultJson.put("errmsg", msg); 310 | return resultJson; 311 | } 312 | 313 | // 获取[商户名称]公众号的 access_token 314 | String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE); 315 | 316 | // 调用接口传入参数 317 | JSONObject paramJson = new JSONObject(1); 318 | 319 | // info 包含 basic_info、card_info_list、custom_info 320 | JSONObject info = new JSONObject(3); 321 | 322 | // 基础信息对象 323 | JSONObject basic_info = new JSONObject(8); 324 | // activity_bg_color 是 活动封面的背景颜色,可参考:选取卡券背景颜色 325 | basic_info.put("activity_bg_color", CardBgColorEnum.COLOR_090.getBgName()); 326 | // activity_tinyappid 是 用户点击链接后可静默添加到列表的小程序appid; 327 | basic_info.put("activity_tinyappid", WXPayConstants.APP_ID); 328 | // mch_code 是 支付商户号 329 | basic_info.put("mch_code", WXPayConstants.MCH_ID); 330 | // begin_time 是 活动开始时间,精确到秒(unix时间戳) 331 | basic_info.put("begin_time", DateTimeUtil.getTenTimeByDate(begin_time)); 332 | // end_time 是 活动结束时间,精确到秒(unix时间戳) 333 | basic_info.put("end_time", DateTimeUtil.getTenTimeByDate(end_time)); 334 | // gift_num 是 单个礼包社交立减金数量(3-15个) 335 | basic_info.put("gift_num", gift_num); 336 | // max_partic_times_act 否 每个用户活动期间最大领取次数,最大为50,不填默认为1 337 | basic_info.put("max_partic_times_act", max_partic_times_act); 338 | // max_partic_times_one_day 否 每个用户活动期间单日最大领取次数,最大为50,不填默认为1 339 | basic_info.put("max_partic_times_one_day", max_partic_times_one_day); 340 | 341 | // card_info_list 是 可以配置两种发放规则:小程序新老用户、新老会员 342 | JSONArray card_info_list = new JSONArray(1); 343 | JSONObject card_info = new JSONObject(3); 344 | // card_id 是 卡券ID 345 | card_info.put("card_id", card_id); 346 | // min_amt 是 最少支付金额,单位是分 347 | card_info.put("min_amt", String.valueOf(new BigDecimal(min_amt).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue())); 348 | /* 349 | * membership_appid 是 奖品指定的会员卡appid。如用户标签有选择商户会员,则需要填写会员卡appid,该appid需要跟所有发放商户号有绑定关系。 350 | * new_tinyapp_user 是 可以指定为是否小程序新用户 351 | * total_user 是 可以指定为所有用户 352 | * membership_appid、new_tinyapp_user、total_user以上字段3选1,未选择请勿填,不必故意填写false 353 | */ 354 | if (StringUtils.isNotBlank(membership_appid)) { 355 | card_info.put("membership_appid", membership_appid); 356 | } else { 357 | if (new_tinyapp_user) { 358 | card_info.put("new_tinyapp_user", true); 359 | } else { 360 | card_info.put("total_user", true); 361 | } 362 | } 363 | card_info_list.add(card_info); 364 | 365 | // 自定义字段,表示支付后领券 366 | JSONObject custom_info = new JSONObject(1); 367 | custom_info.put("type", "AFTER_PAY_PACKAGE"); 368 | 369 | // 拼装json对象 370 | info.put("basic_info", basic_info); 371 | info.put("card_info_list", card_info_list); 372 | info.put("custom_info", custom_info); 373 | paramJson.put("info", info); 374 | 375 | // 请求微信接口,得到返回结果[json] 376 | HttpEntity entity = new HttpEntity<>(paramJson, this.getHttpHeadersUTF8JSON()); 377 | JSONObject resultJson = restTemplate.postForObject(WXURL.WX_CARD_ACTIVITY_CREATE_URL, entity, JSONObject.class, accessToken); 378 | 379 | // {"errcode":0,"errmsg":"ok","activity_id":"4728935"} 380 | System.out.println(resultJson.toJSONString()); 381 | 382 | return resultJson; 383 | } catch (Exception e) { 384 | WXPayUtil.getLogger().error(e.getMessage(), e); 385 | } 386 | return null; 387 | } 388 | 389 | /** 390 | * 创建活动接口之前的验证 391 | * 392 | * @param begin_time 活动开始时间,精确到秒 393 | * @param end_time 活动结束时间,精确到秒 394 | * @param gift_num 单个礼包社交立减金数量(3-15个) 395 | * @param max_partic_times_act 每个用户活动期间最大领取次数,最大为50,默认为1 396 | * @param max_partic_times_one_day 每个用户活动期间单日最大领取次数,最大为50,默认为1 397 | * @param min_amt 最少支付金额,单位是元 398 | * @return msg str 399 | * @author yclimb 400 | * @date 2018/9/18 401 | */ 402 | public String checkCardActivity(String begin_time, String end_time, int gift_num, int max_partic_times_act, 403 | int max_partic_times_one_day, String min_amt) { 404 | 405 | // 开始时间不能小于结束时间 406 | if (DateTimeUtil.latterThan(end_time, begin_time, DateTimeUtil.TIME_FORMAT_NORMAL)) { 407 | return "活动开始时间不能小于活动结束时间"; 408 | } 409 | 410 | // 单个礼包社交立减金数量(3-15个) 411 | if (gift_num < 3 || gift_num > 15) { 412 | return "单个礼包社交立减金数量(3-15个)"; 413 | } 414 | 415 | // 每个用户活动期间最大领取次数,最大为50,默认为1 416 | if (max_partic_times_act <= 0 || max_partic_times_act > 50) { 417 | return "每个用户活动期间最大领取次数,最大为50,默认为1"; 418 | } 419 | 420 | // 每个用户活动期间单日最大领取次数,最大为50,默认为1 421 | if (max_partic_times_one_day <= 0 || max_partic_times_one_day > 50) { 422 | return "每个用户活动期间单日最大领取次数,最大为50,默认为1"; 423 | } 424 | 425 | // 最少支付金额,单位是元 426 | if (BigDecimal.ONE.compareTo(new BigDecimal(min_amt)) > 0) { 427 | return "最少支付金额必须大于1元"; 428 | } 429 | 430 | return null; 431 | } 432 | 433 | /** 434 | * 获取卡券 api_ticket 的 api 435 | * 请求路径:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=wx_card 436 | * 437 | * @param access_token token 438 | * @return api_ticket json obj 439 | * @author yclimb 440 | * @date 2018/9/21 441 | */ 442 | public String getWxCardApiTicket(String access_token) { 443 | if (StringUtils.isBlank(access_token)) { 444 | return null; 445 | } 446 | try { 447 | 448 | // redis key 449 | String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WXCARD_APITICKET, access_token); 450 | 451 | // 从redis中获取缓存 452 | Object obj = redisTemplate.opsForValue().get(redisKey); 453 | if (obj != null) { 454 | return obj.toString(); 455 | } 456 | 457 | // 获取卡券 api_ticket 458 | String api_ticket = restTemplate.getForObject(WXURL.BASE_API_TICKET, String.class, access_token); 459 | WXPayUtil.getLogger().info("getWxCardApiTicket:api_ticket:{}", api_ticket); 460 | if (StringUtils.isBlank(api_ticket)) { 461 | return null; 462 | } 463 | JSONObject jsonObject = JSON.parseObject(api_ticket); 464 | if (0 != jsonObject.getIntValue("errcode")) { 465 | return null; 466 | } 467 | 468 | // 设置到redis中,下次取直接拿缓存即可,防止多次生成 469 | String ticket = jsonObject.getString("ticket"); 470 | redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS); 471 | 472 | return ticket; 473 | } catch (Exception e) { 474 | WXPayUtil.getLogger().error(e.getMessage(), e); 475 | } 476 | return null; 477 | } 478 | 479 | /** 480 | * 获取 jsapi_ticket 481 | * 请求路径:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi 482 | * 483 | * @param access_token token 484 | * @return api_ticket json obj 485 | * @author yclimb 486 | * @date 2018/9/25 487 | */ 488 | public String getWxApiTicket(String access_token) { 489 | if (StringUtils.isBlank(access_token)) { 490 | return null; 491 | } 492 | try { 493 | 494 | // redis key 495 | String redisKey = RedisKeyUtil.keyBuilder(RedisKeyEnum.IMALL_WX_APITICKET, access_token); 496 | 497 | // 从redis中获取缓存 498 | Object obj = redisTemplate.opsForValue().get(redisKey); 499 | if (obj != null) { 500 | return obj.toString(); 501 | } 502 | 503 | // 获取 api_ticket 504 | String api_ticket = restTemplate.getForObject(WXURL.BASE_JSAPI_TICKET, String.class, access_token); 505 | WXPayUtil.getLogger().info("getWxApiTicket:api_ticket:{}", api_ticket); 506 | if (StringUtils.isBlank(api_ticket)) { 507 | return null; 508 | } 509 | JSONObject jsonObject = JSON.parseObject(api_ticket); 510 | if (0 != jsonObject.getIntValue("errcode")) { 511 | return null; 512 | } 513 | 514 | // 设置到redis中,下次取直接拿缓存即可,防止多次生成 515 | String ticket = jsonObject.getString("ticket"); 516 | redisTemplate.opsForValue().set(redisKey, ticket, jsonObject.getIntValue("expires_in"), TimeUnit.SECONDS); 517 | 518 | return ticket; 519 | } catch (Exception e) { 520 | WXPayUtil.getLogger().error(e.getMessage(), e); 521 | } 522 | return null; 523 | } 524 | 525 | /** 526 | * 根据代金券批次ID得到组合的cardList 527 | * 528 | * @param cardId 卡包ID 529 | * @return cardList 530 | * @author yclimb 531 | * @date 2018/9/21 532 | */ 533 | public JSONArray getCardList(String cardId) { 534 | if (StringUtils.isBlank(cardId)) { 535 | return null; 536 | } 537 | try { 538 | 539 | // 获取[商户名称]公众号的 access_token 540 | String accessToken = this.getAccessToken(WXConstants.WX_MINI_PROGRAM_CODE); 541 | String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp()); 542 | String nonce_str = WXPayUtil.generateNonceStr(); 543 | 544 | // 卡券的扩展参数。需进行 JSON 序列化为字符串传入 545 | JSONObject cardExt = new JSONObject(); 546 | //cardExt.put("code", ""); 547 | //cardExt.put("openid", ""); 548 | //cardExt.put("fixed_begintimestamp", ""); 549 | //cardExt.put("outer_str", ""); 550 | cardExt.put("timestamp", timestamp); 551 | cardExt.put("nonce_str", nonce_str); 552 | 553 | /** 554 | * 1.将 api_ticket、timestamp、card_id、code、openid、nonce_str的value值进行字符串的字典序排序。 555 | * 2.将所有参数字符串拼接成一个字符串进行sha1加密,得到signature。 556 | * 3.signature中的timestamp,nonce字段和card_ext中的timestamp,nonce_str字段必须保持一致。 557 | */ 558 | Map map = new HashMap<>(8); 559 | //map.put("code", ""); 560 | //map.put("openid", ""); 561 | map.put("api_ticket", this.getWxCardApiTicket(accessToken)); 562 | map.put("timestamp", timestamp); 563 | map.put("card_id", cardId); 564 | map.put("nonce_str", nonce_str); 565 | cardExt.put("signature", WXPayUtil.SHA1(WXPayUtil.dictionaryOrder(map, 2))); 566 | 567 | // 卡券对象 568 | JSONObject cardInfo = new JSONObject(); 569 | cardInfo.put("cardId", cardId); 570 | cardInfo.put("cardExt", cardExt.toJSONString()); 571 | 572 | // 需要添加的卡券列表 573 | JSONArray cardList = new JSONArray(1); 574 | cardList.add(cardInfo); 575 | 576 | return cardList; 577 | } catch (Exception e) { 578 | WXPayUtil.getLogger().error(e.getMessage(), e); 579 | } 580 | return null; 581 | } 582 | 583 | /** 584 | * 获取微信签名信息 585 | * 586 | * @param requestUrl 请求页面地址 587 | * @param appid appid 588 | * @param code code 589 | * @return 返回map:noncestr:随机字符串;timestamp:签名时间戳;appid;微信公众号Id;signature:签名串 590 | * @author yclimb 591 | * @date 2018/9/25 592 | */ 593 | public Map getSignature(String requestUrl, String appid, String code) { 594 | Map map = new HashMap<>(); 595 | try { 596 | 597 | // 获取公众号的 access_token、jsapi_ticket 598 | String accessToken = this.getAccessToken(code); 599 | String jsapi_ticket = this.getWxApiTicket(accessToken); 600 | String nonce_str = WXPayUtil.generateNonceStr(); 601 | String timestamp = Long.toString(WXPayUtil.getCurrentTimestamp()); 602 | 603 | // 注意这里参数名必须全部小写,且必须有序 604 | String dataStr = "jsapi_ticket=" + jsapi_ticket + 605 | "&noncestr=" + nonce_str + 606 | "×tamp=" + timestamp + 607 | "&url=" + requestUrl; 608 | WXPayUtil.getLogger().info(dataStr); 609 | 610 | String signature = WXPayUtil.SHA1(dataStr); 611 | map.put("noncestr", nonce_str); 612 | map.put("timestamp", timestamp); 613 | map.put("appid", appid); 614 | map.put("signature", signature); 615 | } catch (Exception e) { 616 | WXPayUtil.getLogger().error(e.getMessage(), e); 617 | } 618 | 619 | return map; 620 | } 621 | 622 | } 623 | -------------------------------------------------------------------------------- /src/main/test/controller/TestWXPay.java: -------------------------------------------------------------------------------- 1 | package controller; 2 | 3 | import com.weixin.pay.WXPay; 4 | import com.weixin.pay.WXPayConfigImpl; 5 | import com.weixin.pay.XxxWXPayConfigImpl; 6 | import com.weixin.pay.util.WXPayUtil; 7 | import com.weixin.pay.util.WXUtils; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * 统一接口测试 14 | */ 15 | public class TestWXPay { 16 | 17 | private WXPay wxpay; 18 | private WXPayConfigImpl config; 19 | private String out_trade_no; 20 | private String total_fee; 21 | 22 | public TestWXPay() throws Exception { 23 | config = WXPayConfigImpl.getInstance(); 24 | // wxpay = new WXPay(config); 25 | wxpay = new WXPay(config, true, true); 26 | total_fee = "1.01"; 27 | // out_trade_no = "201701017496748980290321"; 28 | out_trade_no = "20180912004"; 29 | } 30 | 31 | /** 32 | * 获取微信签名 33 | * @param url 34 | */ 35 | private void getWeixinMap(String url) { 36 | 37 | /*Map map = new HashMap<>(); 38 | 39 | try { 40 | map = WXSignatureUtil.getSignature(request, url); 41 | } catch (IOException | CloneNotSupportedException e) { 42 | e.printStackTrace(); 43 | System.out.println("获取微信签名信息异常!" + e.getMessage()); 44 | } 45 | model.addAttribute("noncestr", map.get("noncestr")); 46 | model.addAttribute("timestamp", map.get("timestamp")); 47 | model.addAttribute("appid", map.get("appid")); 48 | model.addAttribute("signature", map.get("signature"));*/ 49 | } 50 | 51 | /** 52 | * 获取微信签名 53 | */ 54 | private void getWeixinMap() { 55 | getWeixinMap(getRequestURL()); 56 | } 57 | 58 | private String getRequestURL() { 59 | String url = null; 60 | /*if (null == request.getQueryString()) { 61 | url = request.getRequestURL().toString(); 62 | } else { 63 | url = request.getRequestURL() + "?" + request.getQueryString(); 64 | }*/ 65 | return url; 66 | } 67 | 68 | /** 69 | * 扫码支付 下单 70 | */ 71 | private void doUnifiedOrder() throws Exception { 72 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 73 | Map resultMap = wxPay.unifiedOrder("https://xxx.com/v1/weixin/pay/wxnotify", 74 | "oPR7T5PFjcfgugIu2abQG6ijQGV4", "xxx-测试商品", WXPayUtil.getPayNo(), "10.01", "127.0.0.1", 75 | "vip", "",null,null); 76 | 77 | String prepay_id = resultMap.get("prepay_id"); 78 | String nonce_str = resultMap.get("nonce_str"); 79 | Map map = wxPay.chooseWXPayMap(prepay_id, nonce_str); 80 | System.out.println("map:" + map); 81 | } 82 | 83 | 84 | private void doOrderClose() { 85 | System.out.println("关闭订单"); 86 | HashMap data = new HashMap(); 87 | data.put("out_trade_no", out_trade_no); 88 | try { 89 | Map r = wxpay.closeOrder(data); 90 | System.out.println(r); 91 | } catch (Exception e) { 92 | e.printStackTrace(); 93 | } 94 | } 95 | 96 | private void doOrderQuery() { 97 | System.out.println("查询订单"); 98 | HashMap data = new HashMap(); 99 | data.put("out_trade_no", out_trade_no); 100 | // data.put("transaction_id", "4008852001201608221962061594"); 101 | try { 102 | Map r = wxpay.orderQuery(data); 103 | System.out.println(r); 104 | } catch (Exception e) { 105 | e.printStackTrace(); 106 | } 107 | } 108 | 109 | private void doOrderReverse() { 110 | System.out.println("撤销"); 111 | HashMap data = new HashMap(); 112 | data.put("out_trade_no", out_trade_no); 113 | // data.put("transaction_id", "4008852001201608221962061594"); 114 | try { 115 | Map r = wxpay.reverse(data); 116 | System.out.println(r); 117 | } catch (Exception e) { 118 | e.printStackTrace(); 119 | } 120 | } 121 | 122 | /** 123 | * 长链接转短链接 124 | * 测试成功 125 | */ 126 | private void doShortUrl() { 127 | String long_url = "weixin://wxpay/bizpayurl?pr=etxB4DY"; 128 | HashMap data = new HashMap(); 129 | data.put("long_url", long_url); 130 | try { 131 | Map r = wxpay.shortUrl(data); 132 | System.out.println(r); 133 | } catch (Exception e) { 134 | e.printStackTrace(); 135 | } 136 | } 137 | 138 | /** 139 | * 退款 140 | * 已测试 141 | */ 142 | private void doRefund() { 143 | HashMap data = new HashMap(); 144 | data.put("out_trade_no", out_trade_no); 145 | data.put("out_refund_no", out_trade_no); 146 | data.put("total_fee", total_fee); 147 | data.put("refund_fee", total_fee); 148 | data.put("refund_fee_type", "CNY"); 149 | data.put("op_user_id", config.getMchID()); 150 | 151 | try { 152 | Map r = wxpay.refund(data); 153 | System.out.println(r); 154 | } catch (Exception e) { 155 | e.printStackTrace(); 156 | } 157 | 158 | } 159 | 160 | /** 161 | * 查询退款 162 | * 已经测试 163 | */ 164 | private void doRefundQuery() { 165 | HashMap data = new HashMap(); 166 | data.put("out_trade_no", out_trade_no); 167 | //data.put("transactionId", out_trade_no); 168 | data.put("out_refund_no", out_trade_no); 169 | //data.put("refund_id", out_trade_no); 170 | try { 171 | Map r = wxpay.refundQuery(data); 172 | System.out.println(r); 173 | } catch (Exception e) { 174 | e.printStackTrace(); 175 | } 176 | } 177 | 178 | /** 179 | * 对账单下载 180 | * 已测试 181 | */ 182 | private void doDownloadBill() { 183 | HashMap data = new HashMap(); 184 | data.put("bill_date", "20161102"); 185 | data.put("bill_type", "ALL"); 186 | try { 187 | Map r = wxpay.downloadBill(data); 188 | System.out.println(r); 189 | } catch (Exception e) { 190 | e.printStackTrace(); 191 | } 192 | } 193 | 194 | /** 195 | * 获取沙盒 sandbox_signkey 196 | * 197 | * @author yclimb 198 | * @date 2018/9/18 199 | */ 200 | private void doGetSandboxSignKey() throws Exception { 201 | WXPayConfigImpl config = WXPayConfigImpl.getInstance(); 202 | HashMap data = new HashMap(); 203 | data.put("mch_id", config.getMchID()); 204 | data.put("nonce_str", WXPayUtil.generateNonceStr()); 205 | String sign = WXPayUtil.generateSignature(data, config.getKey()); 206 | data.put("sign", sign); 207 | WXPay wxPay = new WXPay(config); 208 | // String result = wxPay.requestWithoutCert("https://api.mch.weixin.qq.com/sandbox/pay/getsignkey", data, 10000, 10000); 209 | String result = wxPay.requestWithoutCert("/sandboxnew/pay/getsignkey", data, 10000, 10000); 210 | System.out.println(result); 211 | } 212 | 213 | private void doReport() { 214 | HashMap data = new HashMap(); 215 | data.put("interface_url", "20160822"); 216 | data.put("bill_type", "ALL"); 217 | } 218 | 219 | /** 220 | * 小测试 221 | */ 222 | private void test001() { 223 | String xmlStr="\n" + 224 | "\n" + 225 | "\n" + 226 | "\n" + 227 | "\n" + 228 | "\n" + 229 | "\n" + 230 | "\n" + 231 | "\n" + 232 | "\n" + 233 | "\n" + 234 | "1\n" + 235 | "\n" + 236 | "\n" + 237 | "\n" + 238 | "\n" + 239 | "\n" + 240 | "\n" + 241 | "1\n" + 242 | ""; 243 | try { 244 | System.out.println(xmlStr); 245 | System.out.println("+++++++++++++++++"); 246 | System.out.println(WXPayUtil.isSignatureValid(xmlStr, config.getKey())); 247 | Map hm = WXPayUtil.xmlToMap(xmlStr); 248 | System.out.println("+++++++++++++++++"); 249 | System.out.println(hm); 250 | System.out.println(hm.get("attach").length()); 251 | 252 | } catch (Exception e) { 253 | e.printStackTrace(); 254 | } 255 | 256 | } 257 | 258 | private void testUnifiedOrderSpeed() throws Exception { 259 | TestWXPay dodo = new TestWXPay(); 260 | 261 | for (int i=0; i<100; ++i) { 262 | long startTs = System.currentTimeMillis(); 263 | out_trade_no = out_trade_no+i; 264 | dodo.doUnifiedOrder(); 265 | long endTs = System.currentTimeMillis(); 266 | System.out.println(endTs-startTs); 267 | Thread.sleep(1000); 268 | } 269 | 270 | } 271 | 272 | /** 273 | * 提现 274 | * 275 | * @author yclimb 276 | * @date 2018/9/18 277 | */ 278 | public void doTranster() throws Exception { 279 | // 微信调用接口 280 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 281 | Map resultMap = wxPay.transfers("1507928321201809301504246520", 282 | "oPR7T5DWvXuhfKfyqNdi6MTQGaxo", "1.9", "测试退款", "127.0.0.1"); 283 | System.out.println("wxPay.transfers:" + resultMap); 284 | } 285 | 286 | /** 287 | * 发送现金红包 288 | * 289 | * @author yclimb 290 | * @date 2018/9/18 291 | */ 292 | private void sendRedPack() throws Exception { 293 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 294 | wxPay.sendRedPack(WXPayUtil.getPayNo(), "obX_c0YRpT47zKcvq-ZYpjU6GFuA", "1", 295 | "活动名称", "红包祝福语", "备注", "127.0.0.1"); 296 | } 297 | 298 | /** 299 | * 查询现金红包 300 | * 301 | * @author yclimb 302 | * @date 2018/9/18 303 | */ 304 | private void getRedPackInfo() throws Exception { 305 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 306 | wxPay.getRedPackInfo("1507928321201809171554055254"); 307 | } 308 | 309 | /** 310 | * 发送代金券 311 | * 312 | * @author yclimb 313 | * @date 2018/9/18 314 | */ 315 | private void sendCoupon() throws Exception { 316 | WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance()); 317 | wxPay.sendCoupon("9248266", WXPayUtil.getPayNo(), "obX_c0YRpT47zKcvq-ZYpjU6GFuA"); 318 | } 319 | 320 | /** 321 | * 查询代金券信息 322 | * 323 | * @author yclimb 324 | * @date 2018/9/18 325 | */ 326 | private void queryCouponsInfo() throws Exception { 327 | WXPay wxPay = new WXPay(XxxWXPayConfigImpl.getInstance()); 328 | wxPay.queryCouponsInfo("3983069127", "9248266", "obX_c0YRpT47zKcvq-ZYpjU6GFuA"); 329 | } 330 | 331 | /** 332 | * 查询代金券批次信息 333 | * 334 | * @author yclimb 335 | * @date 2018/9/18 336 | */ 337 | private void queryCouponStock() throws Exception { 338 | WXPay wxPay = new WXPay(XxxWXPayConfigImpl.getInstance()); 339 | wxPay.queryCouponStock("9248266"); 340 | } 341 | 342 | /** 343 | * 创建支付后领取立减金活动接口 344 | * 345 | * @author yclimb 346 | * @date 2018/9/18 347 | */ 348 | private void createCardActivity() { 349 | WXUtils wxUtils = new WXUtils(); 350 | wxUtils.createCardActivity("2018-09-18 18:00:00", "2018-09-18 19:59:59", 3, 1, 351 | 1, "pX2-vjpU_MT1gFDsP8lNl15PdaZE", "100", 352 | null, false); 353 | } 354 | 355 | public static void main(String[] args) throws Exception { 356 | System.out.println("--------------->"); 357 | 358 | TestWXPay dodo = new TestWXPay(); 359 | //dodo.doGetSandboxSignKey(); 360 | //dodo.doOrderQuery(); 361 | //dodo.doRefundQuery(); 362 | //dodo.doDownloadBill(); 363 | //dodo.sendRedPack(); 364 | //dodo.getRedPackInfo(); 365 | //dodo.sendCoupon(); 366 | //dodo.queryCouponsInfo(); 367 | //dodo.queryCouponStock(); 368 | //dodo.createCardActivity(); 369 | //dodo.doUnifiedOrder(); 370 | dodo.doTranster(); 371 | 372 | 373 | // 沙箱环境测试 374 | //WXPay wxPay = new WXPay(WXPayConfigImpl.getInstance(), true, true); 375 | //WXPay wxPay = new WXPay(ChunboWXPayConfigImpl.getInstance()); 376 | 377 | /*Map resultMap = wxPay.refund("http://127.0.0.1:11000/weixin/pay/wxnotify", null, 378 | "20180912004", "20180912004", "5.52", "5.52", "测试退款");*/ 379 | 380 | //System.out.println(resultMap); 381 | 382 | /*Map resultMap = wxPay.refund(null, "10000", "10001", "1.01", "0.01", "测试微信退款"); 383 | System.out.println(WXPayUtil.isSignatureValid(resultMap, WXPayConstants.API_KEY));*/ 384 | 385 | 386 | System.out.println("<---------------"); 387 | } 388 | 389 | } 390 | -------------------------------------------------------------------------------- /src/main/test/controller/WXAuthController.java: -------------------------------------------------------------------------------- 1 | package controller; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.weixin.pay.constants.WXConstants; 5 | import com.weixin.pay.util.WXUtils; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.annotation.Resource; 11 | import javax.servlet.http.HttpServletRequest; 12 | 13 | /** 14 | * 微信用户授权控制类 15 | * 16 | * @author yclimb 17 | * @date 2018/7/30 18 | */ 19 | @Slf4j 20 | @RestController 21 | @RequestMapping("/weixin/auth") 22 | public class WXAuthController { 23 | 24 | @Resource 25 | private WXUtils wxUtils; 26 | 27 | /** 28 | * 微信网页授权 29 | * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 30 | * 第一步:用户同意授权,获取code 31 | * 第二步:通过code换取网页授权access_token 32 | * @return str 33 | * 34 | * @author yclimb 35 | * @date 2018/7/30 36 | */ 37 | /*@ApiOperation(value = "微信支付|网页授权", httpMethod = "GET", notes = "获取前端微信用户的网页授权,得到用户基础信息")*/ 38 | @GetMapping("/authorize") 39 | public String authorize(HttpServletRequest request) throws Exception { 40 | 41 | // 跳转页面标识 42 | String state = request.getParameter("state"); 43 | // 通过code获取access_token 44 | String code = request.getParameter("code"); 45 | log.info("authorize:code:{}", code); 46 | 47 | // 获取access_token和openid 48 | JSONObject jsonToken = wxUtils.getJsapiAccessTokenByCode(code); 49 | if (null == jsonToken) { 50 | return WXConstants.ERROR; 51 | } 52 | 53 | return WXConstants.SUCCESS; 54 | } 55 | 56 | /** 57 | * 通过access_token和openid请求获取用户信息(需scope为 snsapi_userinfo) 58 | * @return str 59 | * 60 | * @author yclimb 61 | * @date 2018/7/31 62 | */ 63 | /*@ApiOperation(value = "微信支付|通过access_token和openid请求获取用户信息", httpMethod = "POST", notes = "通过access_token和openid请求获取用户信息")*/ 64 | @PostMapping("/userinfo/{access_token}/{openid}") 65 | public String userinfo(@PathVariable String access_token, @PathVariable String openid) { 66 | 67 | // 通过access_token和openid请求获取用户信息 68 | JSONObject jsonUserinfo = wxUtils.getJsapiUserinfo(access_token, openid); 69 | if (null == jsonUserinfo) { 70 | return WXConstants.ERROR; 71 | } 72 | 73 | // 判断用户是否在系统中是一个用户 74 | String unionid = jsonUserinfo.getString("unionid"); 75 | if (StringUtils.isBlank(unionid)) { 76 | return WXConstants.ERROR; 77 | } 78 | 79 | /* 80 | // 存储用户信息到数据库 81 | User user = userService.queryByUnionId(unionid); 82 | if (user == null) { 83 | user = JSON.parseObject(jsonUserinfo.toJSONString(), User.class); 84 | user.setUserId(user.getId()); 85 | user.setCreateDate(new Date()); 86 | user.setIsDel(CommonConstantEnum.UNDELETED.getCode()); 87 | // 处理微信昵称emoji表情 88 | if (StringUtils.isNotBlank(user.getNickName())) { 89 | // 编码Base64.decodeBase64() 90 | user.setNickName(UserNickUtil.encodeNickName(user.getNickName())); 91 | } 92 | userService.createEntity(user); 93 | } 94 | 95 | // 用户账户信息 96 | Map map = new HashMap<>(2); 97 | // 用户名称解码 98 | user.setNickName(UserNickUtil.decodeNickName(user.getNickName())); 99 | UserAccount userAccount = userAccountService.queryByUserId(user.getId()); 100 | map.put("user", user); 101 | 102 | return AppMessage.success(map);*/ 103 | 104 | return WXConstants.SUCCESS; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/test/controller/WXPayController.java: -------------------------------------------------------------------------------- 1 | package controller; 2 | 3 | import com.weixin.pay.constants.WXConstants; 4 | import com.weixin.pay.constants.WXPayConstants; 5 | import com.weixin.pay.util.AESUtil; 6 | import com.weixin.pay.util.WXPayUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.BufferedOutputStream; 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.Map; 19 | 20 | /** 21 | * 微信支付Controller 22 | * 23 | * @author yclimb 24 | * @date 2018/6/15 25 | */ 26 | @Slf4j 27 | @RestController 28 | @RequestMapping("/weixin/pay") 29 | public class WXPayController { 30 | 31 | /** 32 | * 返回成功xml 33 | */ 34 | private String resSuccessXml = ""; 35 | 36 | /** 37 | * 返回失败xml 38 | */ 39 | private String resFailXml = ""; 40 | 41 | /** 42 | * 该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。 43 | * 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action” 44 | *

45 | * 支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。 46 | * 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 47 | * (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) 48 | * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 49 | * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 50 | * 特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。 51 | * 52 | * @author yclimb 53 | * @date 2018/6/15 54 | */ 55 | /*@ApiOperation(value = "微信支付|支付回调接口", httpMethod = "POST", notes = "该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。")*/ 56 | @RequestMapping("/wxnotify") 57 | public void wxnotify(HttpServletRequest request, HttpServletResponse response) { 58 | 59 | String resXml = ""; 60 | InputStream inStream; 61 | try { 62 | 63 | inStream = request.getInputStream(); 64 | ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); 65 | byte[] buffer = new byte[1024]; 66 | int len = 0; 67 | while ((len = inStream.read(buffer)) != -1) { 68 | outSteam.write(buffer, 0, len); 69 | } 70 | 71 | WXPayUtil.getLogger().info("wxnotify:微信支付----start----"); 72 | 73 | // 获取微信调用我们notify_url的返回信息 74 | String result = new String(outSteam.toByteArray(), "utf-8"); 75 | WXPayUtil.getLogger().info("wxnotify:微信支付----result----=" + result); 76 | 77 | // 关闭流 78 | outSteam.close(); 79 | inStream.close(); 80 | 81 | // xml转换为map 82 | Map map = WXPayUtil.xmlToMap(result); 83 | boolean isSuccess = false; 84 | if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RESULT_CODE))) { 85 | 86 | WXPayUtil.getLogger().info("wxnotify:微信支付----返回成功"); 87 | 88 | if (WXPayUtil.isSignatureValid(map, WXPayConstants.API_KEY)) { 89 | 90 | // 订单处理 操作 orderconroller 的回写操作? 91 | WXPayUtil.getLogger().info("wxnotify:微信支付----验证签名成功"); 92 | 93 | // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. 94 | resXml = resSuccessXml; 95 | isSuccess = true; 96 | 97 | } else { 98 | WXPayUtil.getLogger().error("wxnotify:微信支付----判断签名错误"); 99 | } 100 | 101 | } else { 102 | WXPayUtil.getLogger().error("wxnotify:支付失败,错误信息:" + map.get(WXPayConstants.ERR_CODE_DES)); 103 | resXml = resFailXml; 104 | } 105 | 106 | /*// 根据付款单号查询付款记录 107 | Payment payment = paymentService.queryPaymentByFlowNumer(map.get("out_trade_no"), PaymentConstantEnum.PAYMENT_TYPE_ORDER.getCode()); 108 | 109 | // 付款记录修改 & 记录付款日志 110 | int resultPay = paymentService.modifyPaymentByWxnotify(payment, isSuccess); 111 | if (resultPay > 0) { 112 | // 处理业务 - 修改订单状态 113 | WXPayUtil.getLogger().info("wxnotify:微信支付回调:修改的订单===>" + map.get("out_trade_no")); 114 | int updateResult = tradeService.modifyWxnotifyByRelationId(payment.getRelationId(), payment.getPrepayId(), isSuccess); 115 | if (updateResult > 0) { 116 | WXPayUtil.getLogger().info("wxnotify:微信支付回调:修改订单支付状态成功"); 117 | } else { 118 | WXPayUtil.getLogger().error("wxnotify:微信支付回调:修改订单支付状态失败"); 119 | } 120 | }*/ 121 | 122 | } catch (Exception e) { 123 | WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:", e); 124 | } finally { 125 | try { 126 | // 处理业务完毕 127 | BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); 128 | out.write(resXml.getBytes()); 129 | out.flush(); 130 | out.close(); 131 | } catch (IOException e) { 132 | WXPayUtil.getLogger().error("wxnotify:支付回调发布异常:out:", e); 133 | } 134 | } 135 | 136 | } 137 | 138 | /** 139 | * 退款结果通知 140 | *

141 | * 在申请退款接口中上传参数“notify_url”以开通该功能 142 | * 如果链接无法访问,商户将无法接收到微信通知。 143 | * 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action” 144 | *

145 | * 当商户申请的退款有结果后,微信会把相关结果发送给商户,商户需要接收处理,并返回应答。 146 | * 对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 147 | * (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒) 148 | * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 149 | * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 150 | * 特别说明:退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容 151 | * @param request req 152 | * @param response resp 153 | * @return res xml 154 | * 155 | * @author yclimb 156 | * @date 2018/6/21 157 | */ 158 | /*@ApiOperation(value = "微信支付|微信退款回调接口", httpMethod = "POST", notes = "该链接是通过【微信退款API】中提交的参数notify_url设置,如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。")*/ 159 | @RequestMapping("/refund") 160 | public void refund(HttpServletRequest request, HttpServletResponse response) { 161 | 162 | String resXml = ""; 163 | InputStream inStream; 164 | try { 165 | 166 | inStream = request.getInputStream(); 167 | ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); 168 | byte[] buffer = new byte[1024]; 169 | int len = 0; 170 | while ((len = inStream.read(buffer)) != -1) { 171 | outSteam.write(buffer, 0, len); 172 | } 173 | WXPayUtil.getLogger().info("refund:微信退款----start----"); 174 | 175 | // 获取微信调用我们notify_url的返回信息 176 | String result = new String(outSteam.toByteArray(), "utf-8"); 177 | WXPayUtil.getLogger().info("refund:微信退款----result----=" + result); 178 | 179 | // 关闭流 180 | outSteam.close(); 181 | inStream.close(); 182 | 183 | // xml转换为map 184 | Map map = WXPayUtil.xmlToMap(result); 185 | boolean isSuccess = false; 186 | if (WXPayConstants.SUCCESS.equalsIgnoreCase(map.get(WXPayConstants.RETURN_CODE))) { 187 | 188 | WXPayUtil.getLogger().info("refund:微信退款----返回成功"); 189 | 190 | /*if (WXPayUtil.isSignatureValid(map, WXPayConstants.API_KEY)) {*/ 191 | 192 | /** 以下字段在return_code为SUCCESS的时候有返回: **/ 193 | // 加密信息:加密信息请用商户秘钥进行解密,详见解密方式 194 | String req_info = map.get("req_info"); 195 | 196 | /** 197 | * 解密方式 198 | * 解密步骤如下: 199 | * (1)对加密串A做base64解码,得到加密串B 200 | * (2)对商户key做md5,得到32位小写key* ( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置 ) 201 | * (3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding) 202 | */ 203 | String resultStr = AESUtil.decryptData(req_info); 204 | 205 | // WXPayUtil.getLogger().info("refund:解密后的字符串:" + resultStr); 206 | Map aesMap = WXPayUtil.xmlToMap(resultStr); 207 | 208 | 209 | /** 以下为返回的加密字段: **/ 210 | // 商户退款单号 是 String(64) 1.21775E+27 商户退款单号 211 | String out_refund_no = aesMap.get("out_refund_no"); 212 | // 退款状态 是 String(16) SUCCESS SUCCESS-退款成功、CHANGE-退款异常、REFUNDCLOSE—退款关闭 213 | String refund_status = aesMap.get("refund_status"); 214 | /*// 微信订单号 是 String(32) 1.21775E+27 微信订单号 215 | String transaction_id = null; 216 | // 商户订单号 是 String(32) 1.21775E+27 商户系统内部的订单号 217 | String out_trade_no = null; 218 | // 微信退款单号 是 String(32) 1.21775E+27 微信退款单号 219 | String refund_id = null; 220 | // 订单金额 是 Int 100 订单总金额,单位为分,只能为整数,详见支付金额 221 | String total_fee = null; 222 | // 应结订单金额 否 Int 100 当该订单有使用非充值券时,返回此字段。应结订单金额=订单金额-非充值代金券金额,应结订单金额<=订单金额。 223 | String settlement_total_fee = null; 224 | // 申请退款金额 是 Int 100 退款总金额,单位为分 225 | String refund_fee = null; 226 | // 退款金额 是 Int 100 退款金额=申请退款金额-非充值代金券退款金额,退款金额<=申请退款金额 227 | String settlement_refund_fee = null;*/ 228 | 229 | // 退款是否成功 230 | if (!WXPayConstants.SUCCESS.equals(refund_status)) { 231 | resXml = resFailXml; 232 | } else { 233 | // 通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了. 234 | resXml = resSuccessXml; 235 | isSuccess = true; 236 | } 237 | 238 | /*// 根据付款单号查询付款记录 239 | Payment payment = paymentService.queryPaymentByFlowNumer(out_refund_no, PaymentConstantEnum.PAYMENT_TYPE_REFUND.getCode()); 240 | 241 | // 付款记录修改 & 记录付款日志 242 | int resultPay = paymentService.modifyPaymentByWxnotify(payment, isSuccess); 243 | if (resultPay > 0) { 244 | 245 | // 退款订单记录 246 | List paymentOrderRefundList = paymentOrderRefundService.queryListByPaymentId(payment.getId()); 247 | 248 | // 处理业务 - 修改订单状态 249 | WXPayUtil.getLogger().info("refund:微信支付回调:修改的订单===>" + out_refund_no); 250 | int updateResult = tradeService.modifyWxrefundByRelationId(payment.getRelationId(), paymentOrderRefundList, isSuccess); 251 | if (updateResult > 0) { 252 | WXPayUtil.getLogger().info("refund:微信支付回调:修改订单支付状态成功"); 253 | } else { 254 | WXPayUtil.getLogger().error("refund:微信支付回调:修改订单支付状态失败"); 255 | } 256 | }*/ 257 | 258 | /*} else { 259 | WXPayUtil.getLogger().error("refund:微信支付----判断签名错误"); 260 | }*/ 261 | 262 | } else { 263 | WXPayUtil.getLogger().error("refund:支付失败,错误信息:" + map.get(WXPayConstants.RETURN_MSG)); 264 | resXml = resFailXml; 265 | } 266 | 267 | } catch (Exception e) { 268 | WXPayUtil.getLogger().error("refund:微信退款回调发布异常:", e); 269 | } finally { 270 | try { 271 | // 处理业务完毕 272 | BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); 273 | out.write(resXml.getBytes()); 274 | out.flush(); 275 | out.close(); 276 | } catch (IOException e) { 277 | WXPayUtil.getLogger().error("refund:微信退款回调发布异常:out:", e); 278 | } 279 | } 280 | } 281 | 282 | /** 283 | * 企业付款到零钱 284 | * @return msg 285 | * 286 | * @author yclimb 287 | * @date 2018/7/30 288 | */ 289 | /*@Token(remove = true) 290 | @ApiOperation(value = "微信支付|企业付款到零钱", httpMethod = "POST", notes = "用于企业向微信用户个人付款")*/ 291 | @PostMapping("/transfers") 292 | public String transfers(HttpServletRequest request) { 293 | try { 294 | String remoteAddr = request.getRemoteAddr(); 295 | return WXConstants.SUCCESS; 296 | } catch (Exception e) { 297 | WXPayUtil.getLogger().error("transfers:微信提现支付异常:", e); 298 | } 299 | return WXConstants.ERROR; 300 | } 301 | 302 | } 303 | --------------------------------------------------------------------------------