From 7156d0ac1397964556ad8c38b01cde1a9ec85cdc Mon Sep 17 00:00:00 2001 From: LJQ <2265940560@qq.com> Date: Fri, 18 Jan 2019 11:36:17 +0800 Subject: [PATCH] Add files via upload --- springboot-pay-example/README.md | 41 +++ springboot-pay-example/pom.xml | 91 ++++++ .../pay/SpringbootPayExampleApplication.java | 12 + .../configuration/AlipayConfiguration.java | 55 ++++ .../pay/configuration/AlipayProperties.java | 84 +++++ .../pay/configuration/MyWXPayConfig.java | 75 +++++ .../pay/configuration/WXPayClient.java | 186 +++++++++++ .../pay/configuration/WXPayConfiguration.java | 34 ++ .../configuration/WebMvcConfiguration.java | 21 ++ .../pay/controller/AlipayController.java | 252 +++++++++++++++ .../controller/AlipayF2FPayController.java | 298 ++++++++++++++++++ .../controller/AlipayPagePayController.java | 81 +++++ .../controller/AlipayWAPPayController.java | 116 +++++++ .../pay/controller/WXPayController.java | 166 ++++++++++ .../pay/controller/WXPayH5PayController.java | 106 +++++++ .../controller/WXPayMicroPayController.java | 57 ++++ .../controller/WXPayPrecreateController.java | 139 ++++++++ .../java/com/example/pay/util/PayUtil.java | 38 +++ .../src/main/resources/application.yml | 25 ++ .../main/resources/templates/gotoH5Page.html | 34 ++ .../main/resources/templates/gotoPagePay.html | 17 + .../main/resources/templates/gotoWapPay.html | 18 ++ .../resources/templates/h5PaySuccess.html | 12 + .../main/resources/templates/pagePayFail.html | 12 + .../resources/templates/pagePaySuccess.html | 12 + .../main/resources/templates/wapPayFail.html | 12 + .../resources/templates/wapPaySuccess.html | 12 + .../SpringbootPayExampleApplicationTests.java | 16 + 28 files changed, 2022 insertions(+) create mode 100644 springboot-pay-example/README.md create mode 100644 springboot-pay-example/pom.xml create mode 100644 springboot-pay-example/src/main/java/com/example/pay/SpringbootPayExampleApplication.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayConfiguration.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayProperties.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/MyWXPayConfig.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayClient.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayConfiguration.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/configuration/WebMvcConfiguration.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/AlipayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/AlipayF2FPayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/AlipayPagePayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/AlipayWAPPayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/WXPayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/WXPayH5PayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/WXPayMicroPayController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/controller/WXPayPrecreateController.java create mode 100644 springboot-pay-example/src/main/java/com/example/pay/util/PayUtil.java create mode 100644 springboot-pay-example/src/main/resources/application.yml create mode 100644 springboot-pay-example/src/main/resources/templates/gotoH5Page.html create mode 100644 springboot-pay-example/src/main/resources/templates/gotoPagePay.html create mode 100644 springboot-pay-example/src/main/resources/templates/gotoWapPay.html create mode 100644 springboot-pay-example/src/main/resources/templates/h5PaySuccess.html create mode 100644 springboot-pay-example/src/main/resources/templates/pagePayFail.html create mode 100644 springboot-pay-example/src/main/resources/templates/pagePaySuccess.html create mode 100644 springboot-pay-example/src/main/resources/templates/wapPayFail.html create mode 100644 springboot-pay-example/src/main/resources/templates/wapPaySuccess.html create mode 100644 springboot-pay-example/src/test/java/com/example/pay/SpringbootPayExampleApplicationTests.java diff --git a/springboot-pay-example/README.md b/springboot-pay-example/README.md new file mode 100644 index 0000000..aeead76 --- /dev/null +++ b/springboot-pay-example/README.md @@ -0,0 +1,41 @@ +# 功能 +该example主要集成了支付宝支付和微信支付,其中支付宝支付使用的是沙箱环境,微信支付使用的是demo中的测试账号,目前已经接入的支付类型如下: + + +支付宝支付接口: + +- AlipayController 支付宝-通用接口 +- AlipayF2FPayController 支付宝-当面付 +- AlipayPagePayController 支付宝-电脑网站支付 +- AlipayWAPPayController 支付宝-手机网站支付 + +微信支付接口: + +- WXPayController 微信支付-通用接口 +- WXPayMicroPayController 微信支付-刷卡支付 +- WXPayPrecreateController 微信支付-扫码支付 +- WXPayH5PayController 微信支付-H5支付 + + +# 关于测试账号 +支付宝支付的账号可以自己在开放平台上直接申请,个人很容易通过,通过后直接使用沙箱环境即可。 + +微信支付如果要申请账号必须是服务号,还要每年缴300元,申请过程比较麻烦,这里直接使用demo中的账号用来测试,如果公司已经申请了账号最好使用公司的账号来测试 + + +# 支付集成步骤 +支付宝支付和微信支付具体集成步骤已经详细的记录在博客里了,如果不熟悉支付宝集成或者微信支付集成的,请移步下面博客 + +注意:微信支付坑很多很多,而且sdk写得过于简陋,很多必要的功能更都没有给出实现,关于坑和一些必要的逻辑实现在example和博客中都指出来。 + +- [Spring Boot入门教程(三十五):支付宝集成-准备工作](https://blog.csdn.net/vbirdbest/article/details/80635194) + +- [Spring Boot入门教程(三十六):支付宝集成-当面付](https://blog.csdn.net/vbirdbest/article/details/80655716) +- [Spring Boot入门教程(三十七):支付宝集成-手机网站支付](https://blog.csdn.net/vbirdbest/article/details/80684460) +- [Spring Boot入门教程(三十八):支付宝集成-电脑网站支付](https://blog.csdn.net/vbirdbest/article/details/80696690) +- [Spring Boot入门教程(三十九):微信支付集成-申请服务号和微信支付](https://blog.csdn.net/vbirdbest/article/details/80717905) +- [Spring Boot入门教程(四十):微信支付集成-刷卡支付](https://blog.csdn.net/vbirdbest/article/details/80720138) +- [Spring Boot入门教程(四十一):微信支付集成-扫码支付](https://blog.csdn.net/vbirdbest/article/details/80723991) +- [Spring Boot入门教程(四十二):微信支付集成-H5支付](https://blog.csdn.net/vbirdbest/article/details/80726616) + + diff --git a/springboot-pay-example/pom.xml b/springboot-pay-example/pom.xml new file mode 100644 index 0000000..13d11d8 --- /dev/null +++ b/springboot-pay-example/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.example + springboot-pay-example + 0.0.1-SNAPSHOT + jar + + springboot-pay-example + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.2.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + com.alipay + alipay-sdk-java + 20170725114550 + + + com.alipay + alipay-trade-sdk + 20161215 + + + com.google.code.gson + gson + + + commons-configuration + commons-configuration + 1.10 + + + com.google.zxing + core + 3.2.1 + + + + + com.github.wxpay + wxpay-sdk + 0.0.3 + + + org.projectlombok + lombok + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/springboot-pay-example/src/main/java/com/example/pay/SpringbootPayExampleApplication.java b/springboot-pay-example/src/main/java/com/example/pay/SpringbootPayExampleApplication.java new file mode 100644 index 0000000..6c639d4 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/SpringbootPayExampleApplication.java @@ -0,0 +1,12 @@ +package com.example.pay; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootPayExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootPayExampleApplication.class, args); + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayConfiguration.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayConfiguration.java new file mode 100644 index 0000000..acd0b65 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayConfiguration.java @@ -0,0 +1,55 @@ +package com.example.pay.configuration; + + +import com.alipay.api.AlipayClient; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.demo.trade.service.AlipayTradeService; +import com.alipay.demo.trade.service.impl.AlipayTradeServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 两个支付宝客户端,用户可以使用任意一个. + * + * alipay-trade-sdk 是对alipay-sdk-java的封装,建议使用alipay-trade-sdk. + * + */ +@Configuration +@EnableConfigurationProperties(AlipayProperties.class) +public class AlipayConfiguration { + + @Autowired + private AlipayProperties properties; + + /** + * alipay-trade-sdk + * @return + */ + @Bean + public AlipayTradeService alipayTradeService() { + return new AlipayTradeServiceImpl.ClientBuilder() + .setGatewayUrl(properties.getGatewayUrl()) + .setAppid(properties.getAppid()) + .setPrivateKey(properties.getAppPrivateKey()) + .setAlipayPublicKey(properties.getAlipayPublicKey()) + .setSignType(properties.getSignType()) + .build(); + } + + /** + * alipay-sdk-java + * @return + */ + @Bean + public AlipayClient alipayClient(){ + return new DefaultAlipayClient(properties.getGatewayUrl(), + properties.getAppid(), + properties.getAppPrivateKey(), + properties.getFormate(), + properties.getCharset(), + properties.getAlipayPublicKey(), + properties.getSignType()); + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayProperties.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayProperties.java new file mode 100644 index 0000000..f66036b --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/AlipayProperties.java @@ -0,0 +1,84 @@ +package com.example.pay.configuration; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.annotation.PostConstruct; + +/** + * 支付宝支付的参数配置 + * + */ +@Data +@Slf4j +@ConfigurationProperties(prefix = "pay.alipay") +public class AlipayProperties { + + /** 支付宝gatewayUrl */ + private String gatewayUrl; + /** 商户应用id */ + private String appid; + /** RSA私钥,用于对商户请求报文加签 */ + private String appPrivateKey; + /** 支付宝RSA公钥,用于验签支付宝应答 */ + private String alipayPublicKey; + /** 签名类型 */ + private String signType = "RSA2"; + + /** 格式 */ + private String formate = "json"; + /** 编码 */ + private String charset = "UTF-8"; + + /** 同步地址 */ + private String returnUrl; + + /** 异步地址 */ + private String notifyUrl; + + /** 最大查询次数 */ + private static int maxQueryRetry = 5; + /** 查询间隔(毫秒) */ + private static long queryDuration = 5000; + /** 最大撤销次数 */ + private static int maxCancelRetry = 3; + /** 撤销间隔(毫秒) */ + private static long cancelDuration = 3000; + + private AlipayProperties() {} + + /** + * PostContruct是spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。 + */ + @PostConstruct + public void init() { + log.info(description()); + } + + public String description() { + StringBuilder sb = new StringBuilder("\nConfigs{"); + sb.append("支付宝网关: ").append(gatewayUrl).append("\n"); + sb.append(", appid: ").append(appid).append("\n"); + sb.append(", 商户RSA私钥: ").append(getKeyDescription(appPrivateKey)).append("\n"); + sb.append(", 支付宝RSA公钥: ").append(getKeyDescription(alipayPublicKey)).append("\n"); + sb.append(", 签名类型: ").append(signType).append("\n"); + + sb.append(", 查询重试次数: ").append(maxQueryRetry).append("\n"); + sb.append(", 查询间隔(毫秒): ").append(queryDuration).append("\n"); + sb.append(", 撤销尝试次数: ").append(maxCancelRetry).append("\n"); + sb.append(", 撤销重试间隔(毫秒): ").append(cancelDuration).append("\n"); + sb.append("}"); + return sb.toString(); + } + + private String getKeyDescription(String key) { + int showLength = 6; + if (StringUtils.isNotEmpty(key) && key.length() > showLength) { + return new StringBuilder(key.substring(0, showLength)).append("******") + .append(key.substring(key.length() - showLength)).toString(); + } + return null; + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/MyWXPayConfig.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/MyWXPayConfig.java new file mode 100644 index 0000000..ed1a9d6 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/MyWXPayConfig.java @@ -0,0 +1,75 @@ +package com.example.pay.configuration; + +import com.github.wxpay.sdk.WXPayConfig; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +/** + * 微信支付的参数配置 + * + */ +@Data +@Slf4j +@ConfigurationProperties(prefix = "pay.wxpay") +public class MyWXPayConfig implements WXPayConfig{ + + /** 公众账号ID */ + private String appID; + + /** 商户号 */ + private String mchID; + + /** API 密钥 */ + private String key; + + /** API 沙箱环境密钥 */ + private String sandboxKey; + + /** API证书绝对路径 */ + private String certPath; + + /** 退款异步通知地址 */ + private String notifyUrl; + + private Boolean useSandbox; + + /** HTTP(S) 连接超时时间,单位毫秒 */ + private int httpConnectTimeoutMs = 8000; + + /** HTTP(S) 读数据超时时间,单位毫秒 */ + private int httpReadTimeoutMs = 10000; + + + /** + * 获取商户证书内容 + * + * @return 商户证书内容 + */ + @Override + public InputStream getCertStream() { + File certFile = new File(certPath); + InputStream inputStream = null; + try { + inputStream = new FileInputStream(certFile); + } catch (FileNotFoundException e) { + log.error("cert file not found, path={}, exception is:{}", certPath, e); + } + return inputStream; + } + + @Override + public String getKey(){ + if (useSandbox) { + return sandboxKey; + } + + return key; + } + +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayClient.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayClient.java new file mode 100644 index 0000000..5c4c4f5 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayClient.java @@ -0,0 +1,186 @@ +package com.example.pay.configuration; + +import com.github.wxpay.sdk.WXPay; +import com.github.wxpay.sdk.WXPayConfig; +import com.github.wxpay.sdk.WXPayConstants; +import com.github.wxpay.sdk.WXPayUtil; +import lombok.extern.slf4j.Slf4j; +import sun.misc.BASE64Decoder; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * WXPayClient + *

+ * 对WXPay的简单封装,处理支付密切相关的逻辑. + * + */ +@Slf4j +public class WXPayClient extends WXPay { + + /** 密钥算法 */ + private static final String ALGORITHM = "AES"; + /** 加解密算法/工作模式/填充方式 */ + private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding"; + /** 用户支付中,需要输入密码 */ + private static final String ERR_CODE_USERPAYING = "USERPAYING"; + private static final String ERR_CODE_AUTHCODEEXPIRE = "AUTHCODEEXPIRE"; + /** 交易状态: 未支付 */ + private static final String TRADE_STATE_NOTPAY = "NOTPAY"; + + /** 用户输入密码,尝试30秒内去查询支付结果 */ + private static Integer remainingTimeMs = 10000; + + private WXPayConfig config; + + public WXPayClient(WXPayConfig config, WXPayConstants.SignType signType, boolean useSandbox) { + super(config, signType, useSandbox); + this.config = config; + } + + /** + * + * 刷卡支付 + * + * 对WXPay#microPay(Map)增加了当支付结果为USERPAYING时去轮询查询支付结果的逻辑处理 + * + * 注意:该方法没有处理return_code=FAIL的情况,暂时不考虑网络问题,这种情况直接返回错误 + * + * @param reqData + * @return + * @throws Exception + */ + public Map microPayWithPOS(Map reqData) throws Exception { + // 开始时间(毫秒) + long startTimestampMs = System.currentTimeMillis(); + + Map responseMapForPay = super.microPay(reqData); + log.info(responseMapForPay.toString()); + + // // 先判断 协议字段返回(return_code),再判断 业务返回,最后判断 交易状态(trade_state) + // 通信标识,非交易标识 + String returnCode = responseMapForPay.get("return_code"); + if (WXPayConstants.SUCCESS.equals(returnCode)) { + String errCode = responseMapForPay.get("err_code"); + // 余额不足,信用卡失效 + if (ERR_CODE_USERPAYING.equals(errCode) || "SYSTEMERROR".equals(errCode) || "BANKERROR".equals(errCode)) { + Map orderQueryMap = null; + Map requestData = new HashMap<>(); + requestData.put("out_trade_no", reqData.get("out_trade_no")); + + // 用户支付中,需要输入密码或系统错误则去重新查询订单API err_code, result_code, err_code_des + // 每次循环时的当前系统时间 - 开始时记录的时间 > 设定的30秒时间就退出 + while (System.currentTimeMillis() - startTimestampMs < remainingTimeMs) { + // 商户收银台得到USERPAYING状态后,经过商户后台系统调用【查询订单API】查询实际支付结果。 + orderQueryMap = super.orderQuery(requestData); + String returnCodeForQuery = orderQueryMap.get("return_code"); + if (WXPayConstants.SUCCESS.equals(returnCodeForQuery)) { + // 通讯成功 + String tradeState = orderQueryMap.get("trade_state"); + if (WXPayConstants.SUCCESS.equals(tradeState)) { + // 如果成功了直接将查询结果返回 + return orderQueryMap; + } + // 如果支付结果仍为USERPAYING,则每隔5秒循环调用【查询订单API】判断实际支付结果 + Thread.sleep(1000); + } + } + + // 如果用户取消支付或累计30秒用户都未支付,商户收银台退出查询流程后继续调用【撤销订单API】撤销支付交易。 + String tradeState = orderQueryMap.get("trade_state"); + if (TRADE_STATE_NOTPAY.equals(tradeState) || ERR_CODE_USERPAYING.equals(tradeState) || ERR_CODE_AUTHCODEEXPIRE.equals(tradeState)) { + Map reverseMap = this.reverse(requestData); + String returnCodeForReverse = reverseMap.get("return_code"); + String resultCode = reverseMap.get("result_code"); + if (WXPayConstants.SUCCESS.equals(returnCodeForReverse) && WXPayConstants.SUCCESS.equals(resultCode)) { + // 如果撤销成功,需要告诉客户端已经撤销订单了 + responseMapForPay.put("err_code_des", "用户取消支付或尚未支付,后台已经撤销该订单,请重新支付!"); + } + } + } + } + + return responseMapForPay; + } + + /** + * 从request的inputStream中获取参数 + * @param request + * @return + * @throws Exception + */ + public Map getNotifyParameter(HttpServletRequest request) throws Exception { + InputStream inputStream = request.getInputStream(); + ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = inputStream.read(buffer)) != -1) { + outSteam.write(buffer, 0, length); + } + outSteam.close(); + inputStream.close(); + + // 获取微信调用我们notify_url的返回信息 + String resultXml = new String(outSteam.toByteArray(), "utf-8"); + Map notifyMap = WXPayUtil.xmlToMap(resultXml); + + return notifyMap; + } + + /** + * 解密退款通知 + * + * 账户设置-->API安全-->密钥设置 ) + Cipher cipher = Cipher.getInstance(ALGORITHM_MODE_PADDING); + SecretKeySpec key = new SecretKeySpec(WXPayUtil.MD5(config.getKey()).toLowerCase().getBytes(), ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key); + + //(3)用key*对加密串B做AES-256-ECB解密(PKCS7Padding) + // java.security.InvalidKeyException: Illegal key size or default parameters + // https://www.cnblogs.com/yaks/p/5608358.html + String responseXml = new String(cipher.doFinal(bytes),"UTF-8"); + Map responseMap = WXPayUtil.xmlToMap(responseXml); + return responseMap; + } + + /** + * 获取沙箱环境验签秘钥API + * 获取验签秘钥API文档 + * @return + * @throws Exception + */ + public Map getSignKey() throws Exception { + Map reqData = new HashMap<>(); + reqData.put("mch_id", config.getMchID()); + reqData.put("nonce_str", WXPayUtil.generateNonceStr()); + String sign = WXPayUtil.generateSignature(reqData, config.getKey(), WXPayConstants.SignType.MD5); + reqData.put("sign", sign); + String responseXml = this.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", reqData, + config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs()); + + Map responseMap = WXPayUtil.xmlToMap(responseXml); + + return responseMap; + } + + +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayConfiguration.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayConfiguration.java new file mode 100644 index 0000000..5d9068d --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/WXPayConfiguration.java @@ -0,0 +1,34 @@ +package com.example.pay.configuration; + + +import com.github.wxpay.sdk.WXPay; +import com.github.wxpay.sdk.WXPayConstants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 微信支付配置 + */ +@Configuration +@EnableConfigurationProperties(MyWXPayConfig.class) +public class WXPayConfiguration { + + @Autowired + private MyWXPayConfig wxPayConfig; + + /** + * useSandbox 沙盒环境 + * @return + */ + @Bean + public WXPay wxPay() { + return new WXPay(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox() ); + } + + @Bean + public WXPayClient wxPayClient() { + return new WXPayClient(wxPayConfig, WXPayConstants.SignType.MD5, wxPayConfig.getUseSandbox()); + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/configuration/WebMvcConfiguration.java b/springboot-pay-example/src/main/java/com/example/pay/configuration/WebMvcConfiguration.java new file mode 100644 index 0000000..e1c407d --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/configuration/WebMvcConfiguration.java @@ -0,0 +1,21 @@ +package com.example.pay.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +/** + * WebMVC 配置. + *

+ * 添加路径和页面的映射关系 + * + */ +@Configuration +public class WebMvcConfiguration extends WebMvcConfigurationSupport { + @Override + protected void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/gotoWapPage").setViewName("gotoWapPay"); + registry.addViewController("/gotoPagePage").setViewName("gotoPagePay"); + super.addViewControllers(registry); + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayController.java new file mode 100644 index 0000000..472bc5d --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayController.java @@ -0,0 +1,252 @@ +package com.example.pay.controller; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.domain.AlipayTradeCloseModel; +import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeCloseRequest; +import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeCloseResponse; +import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse; +import com.alipay.api.response.AlipayTradeQueryResponse; +import com.alipay.api.response.AlipayTradeRefundResponse; +import com.alipay.demo.trade.model.builder.AlipayTradeQueryRequestBuilder; +import com.alipay.demo.trade.model.result.AlipayF2FQueryResult; +import com.alipay.demo.trade.service.AlipayTradeService; +import com.example.pay.configuration.AlipayProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 支付宝通用接口 + */ +@Slf4j +@RestController +@RequestMapping("/alipay") +public class AlipayController { + + @Autowired + private AlipayProperties aliPayProperties; + + @Autowired + private AlipayClient alipayClient; + + @Autowired + private AlipayTradeService alipayTradeService; + + /** + * 支付异步通知 + * + * 接收到异步通知并验签通过后,一定要检查通知内容,包括通知中的app_id、out_trade_no、total_amount是否与请求中的一致,并根据trade_status进行后续业务处理。 + * + * https://docs.open.alipay.com/194/103296 + */ + @RequestMapping("/notify") + public String notify(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException { + // 一定要验签,防止黑客篡改参数 + Map parameterMap = request.getParameterMap(); + StringBuilder notifyBuild = new StringBuilder("/****************************** alipay notify ******************************/\n"); + parameterMap.forEach((key, value) -> notifyBuild.append(key + "=" + value[0] + "\n") ); + log.info(notifyBuild.toString()); + + + boolean flag = this.rsaCheckV1(request); + if (flag) { + /** + * TODO 需要严格按照如下描述校验通知数据的正确性 + * + * 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号, + * 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额), + * 同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email), + * + * 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。 + * 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。 + * 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。 + */ + + //交易状态 + String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8"); + // 商户订单号 + String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8"); + //支付宝交易号 + String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8"); + //付款金额 + String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8"); + // TRADE_FINISHED(表示交易已经成功结束,并不能再对该交易做后续操作); + // TRADE_SUCCESS(表示交易已经成功结束,可以对该交易做后续操作,如:分润、退款等); + if(tradeStatus.equals("TRADE_FINISHED")){ + //判断该笔订单是否在商户网站中已经做过处理 + //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细, + // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序 + //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的 + //如果有做过处理,不执行商户的业务程序 + + //注意: + //如果签约的是可退款协议,退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知 + //如果没有签约可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。 + } else if (tradeStatus.equals("TRADE_SUCCESS")){ + //判断该笔订单是否在商户网站中已经做过处理 + //如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细, + // 并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),并执行商户的业务程序 + //请务必判断请求时的total_fee、seller_id与通知时获取的total_fee、seller_id为一致的 + //如果有做过处理,不执行商户的业务程序 + + //注意: + //如果签约的是可退款协议,那么付款完成后,支付宝系统发送该交易状态通知。 + + } + + return "success"; + } + + return "fail"; + } + + /** + * 订单查询(最主要用于查询订单的支付状态) + * @param orderNo 商户订单号 + * @return + */ + @GetMapping("/query") + public String query(String orderNo){ + + AlipayTradeQueryRequestBuilder builder = new AlipayTradeQueryRequestBuilder() + .setOutTradeNo(orderNo); + AlipayF2FQueryResult result = alipayTradeService.queryTradeResult(builder); + switch (result.getTradeStatus()) { + case SUCCESS: + log.info("查询返回该订单支付成功: )"); + + AlipayTradeQueryResponse resp = result.getResponse(); + log.info(resp.getTradeStatus()); +// log.info(resp.getFundBillList()); + break; + + case FAILED: + log.error("查询返回该订单支付失败!!!"); + break; + + case UNKNOWN: + log.error("系统异常,订单支付状态未知!!!"); + break; + + default: + log.error("不支持的交易状态,交易返回异常!!!"); + break; + } + return result.getResponse().getBody(); + } + + /** + * 退款 + * @param orderNo 商户订单号 + * @return + */ + @PostMapping("/refund") + @ResponseBody + public String refund(String orderNo) throws AlipayApiException { + AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest(); + + AlipayTradeRefundModel model=new AlipayTradeRefundModel(); + // 商户订单号 + model.setOutTradeNo(orderNo); + // 退款金额 + model.setRefundAmount("0.01"); + // 退款原因 + model.setRefundReason("无理由退货"); + // 退款订单号(同一个订单可以分多次部分退款,当分多次时必传) +// model.setOutRequestNo(UUID.randomUUID().toString()); + alipayRequest.setBizModel(model); + + AlipayTradeRefundResponse alipayResponse = alipayClient.execute(alipayRequest); + System.out.println(alipayResponse.getBody()); + + return alipayResponse.getBody(); + } + + /** + * 退款查询 + * @param orderNo 商户订单号 + * @param refundOrderNo 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的外部订单号 + * @return + * @throws AlipayApiException + */ + @GetMapping("/refundQuery") + @ResponseBody + public String refundQuery(String orderNo, String refundOrderNo) throws AlipayApiException { + AlipayTradeFastpayRefundQueryRequest alipayRequest = new AlipayTradeFastpayRefundQueryRequest(); + + AlipayTradeFastpayRefundQueryModel model=new AlipayTradeFastpayRefundQueryModel(); + model.setOutTradeNo(orderNo); + model.setOutRequestNo(refundOrderNo); + alipayRequest.setBizModel(model); + + AlipayTradeFastpayRefundQueryResponse alipayResponse = alipayClient.execute(alipayRequest); + System.out.println(alipayResponse.getBody()); + + return alipayResponse.getBody(); + } + + /** + * 关闭交易 + * @param orderNo + * @return + * @throws AlipayApiException + */ + @PostMapping("/close") + @ResponseBody + public String close(String orderNo) throws AlipayApiException { + AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest(); + AlipayTradeCloseModel model =new AlipayTradeCloseModel(); + model.setOutTradeNo(orderNo); + alipayRequest.setBizModel(model); + + AlipayTradeCloseResponse alipayResponse = alipayClient.execute(alipayRequest); + System.out.println(alipayResponse.getBody()); + + return alipayResponse.getBody(); + } + + /** + * 校验签名 + * @param request + * @return + */ + public boolean rsaCheckV1(HttpServletRequest request){ + // https://docs.open.alipay.com/54/106370 + // 获取支付宝POST过来反馈信息 + Map params = new HashMap<>(); + Map requestParams = request.getParameterMap(); + for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) { + String name = (String) iter.next(); + String[] values = (String[]) requestParams.get(name); + String valueStr = ""; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] + : valueStr + values[i] + ","; + } + params.put(name, valueStr); + } + + try { + boolean verifyResult = AlipaySignature.rsaCheckV1(params, + aliPayProperties.getAlipayPublicKey(), + aliPayProperties.getCharset(), + aliPayProperties.getSignType()); + + return verifyResult; + } catch (AlipayApiException e) { + log.debug("verify sigin error, exception is:{}", e); + return false; + } + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayF2FPayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayF2FPayController.java new file mode 100644 index 0000000..d03fa48 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayF2FPayController.java @@ -0,0 +1,298 @@ +package com.example.pay.controller; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import com.alipay.demo.trade.model.GoodsDetail; +import com.alipay.demo.trade.model.builder.AlipayTradePayRequestBuilder; +import com.alipay.demo.trade.model.builder.AlipayTradePrecreateRequestBuilder; +import com.alipay.demo.trade.model.builder.AlipayTradeRefundRequestBuilder; +import com.alipay.demo.trade.model.result.AlipayF2FPayResult; +import com.alipay.demo.trade.model.result.AlipayF2FPrecreateResult; +import com.alipay.demo.trade.model.result.AlipayF2FRefundResult; +import com.alipay.demo.trade.service.AlipayTradeService; +import com.alipay.demo.trade.utils.ZxingUtils; +import com.example.pay.configuration.AlipayProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.ResourceUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 支付宝-当面付 控制器. + * https://openclub.alipay.com/read.php?tid=1720&fid=40 + * + * https://docs.open.alipay.com/203/105910 + * + */ +@Slf4j +@RestController +@RequestMapping("/alipay/f2fpay") +public class AlipayF2FPayController { + + @Autowired + private AlipayTradeService alipayTradeService; + + @Autowired + private AlipayProperties aliPayProperties; + + + /** + * 当面付-条码付 + * + * 商家使用扫码工具(扫码枪等)扫描用户支付宝的付款码 + * + * @param authCode + */ + @PostMapping("/barCodePay") + public String barCodePay(String authCode){ + // 实际使用时需要根据商品id查询商品的基本信息并计算价格(可能还有什么优惠),这里只是写死值来测试 + + // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线, + String outTradeNo = UUID.randomUUID().toString(); + + // (必填) 订单标题,粗略描述用户的支付目的。如“喜士多(浦东店)消费” + String subject = "测试订单"; + + // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元" + String body = "购买商品2件共20.05元"; + + // (必填) 订单总金额,单位为元,不能超过1亿元 + // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】 + String totalAmount = "0.01"; + + // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段 + // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】 + String undiscountableAmount = ""; + + // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持 + String storeId = "test_store_id"; + + // 商户操作员编号,添加此参数可以为商户操作员做销售统计 + String operatorId = "test_operator_id"; + + + // 商品明细列表,需填写购买商品详细信息, + List goodsDetailList = new ArrayList<>(); + GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "全麦小面包", 1, 1); + goodsDetailList.add(goods1); + GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "黑人牙刷", 1, 2); + goodsDetailList.add(goods2); + + // 支付超时,线下扫码交易定义为5分钟 + String timeoutExpress = "5m"; + + + AlipayTradePayRequestBuilder builder = new AlipayTradePayRequestBuilder() + .setOutTradeNo(outTradeNo) + .setSubject(subject) + .setBody(body) + .setTotalAmount(totalAmount) + .setAuthCode(authCode) + .setTotalAmount(totalAmount) + .setStoreId(storeId) + .setOperatorId(operatorId) + .setGoodsDetailList(goodsDetailList) + .setTimeoutExpress(timeoutExpress); + + // 当面付,面对面付,face to face pay -> face 2 face pay -> f2f pay + // 同步返回支付结果 + AlipayF2FPayResult f2FPayResult = alipayTradeService.tradePay(builder); + // 注意:一定要处理支付的结果,因为不是每次支付都一定会成功,可能会失败 + switch (f2FPayResult.getTradeStatus()) { + case SUCCESS: + log.info("支付宝支付成功: )"); + break; + + case FAILED: + log.error("支付宝支付失败!!!"); + break; + + case UNKNOWN: + log.error("系统异常,订单状态未知!!!"); + break; + + default: + log.error("不支持的交易状态,交易返回异常!!!"); + break; + } + + /** + * { + * "alipay_trade_pay_response": { + * "code": "10000", + * "msg": "Success", + * "buyer_logon_id": "ekf***@sandbox.com", + * "buyer_pay_amount": "0.01", + * "buyer_user_id": "2088102176027680", + * "buyer_user_type": "PRIVATE", + * "fund_bill_list": [ + * { + * "amount": "0.01", + * "fund_channel": "ALIPAYACCOUNT" + * } + * ], + * "gmt_payment": "2018-06-10 14:54:16", + * "invoice_amount": "0.01", + * "out_trade_no": "91fbd3fa-8558-443a-82c2-bd8e941d7e71", + * "point_amount": "0.00", + * "receipt_amount": "0.01", + * "total_amount": "0.01", + * "trade_no": "2018061021001004680200326495" + * }, + * "sign": "BNgMmA2t8fwFZNSa39kyEKgL6hV45DVOKOsdyyzTzsQnX8HEkKOzVevQEDyK083dNYewip1KK/K92BTDY2KMAsrOEqcCNxsk9NLAvK9ZQVxQzFbAFKqs5EBAEzncSWnChJcb7VMhDakUxHZFmclHg38dLJiHE2bEcF8ar9R1zj0p4V0Jr+BXO10kLtaSTc9NeaCwJZ89sPHKitNnUWRroU7t0xPHc1hWpstObwixKmAWnsFyb9eyGwPQnqNBsUVNSNWCJ7Pg3rb03Tx6J3zNsqH5f0YhWilMi09npPe33URFc6zG1HJSfhEm4Gq1zwQrPoA/anW8BbdmEUUmNo1dEw==" + * } + */ + String result = f2FPayResult.getResponse().getBody(); + + return result; + } + + /** + * 当面付-扫码付 + * + * 扫码支付,指用户打开支付宝钱包中的“扫一扫”功能,扫描商户针对每个订单实时生成的订单二维码,并在手机端确认支付。 + * + * 发起预下单请求,同步返回订单二维码 + * + * 适用场景:商家获取二维码展示在屏幕上,然后用户去扫描屏幕上的二维码 + * @return + * @throws AlipayApiException + */ + @PostMapping("/precreate") + public void precreate(HttpServletRequest request, HttpServletResponse response) throws Exception { + // 实际使用时需要根据商品id查询商品的基本信息并计算价格(可能还有什么优惠),这里只是写死值来测试 + + // (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线, + String outTradeNo = UUID.randomUUID().toString(); + + // (必填) 订单标题,粗略描述用户的支付目的。如“喜士多(浦东店)消费” + String subject = "测试订单"; + + // 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元" + String body = "购买商品2件共20.05元"; + + // (必填) 订单总金额,单位为元,不能超过1亿元 + // 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】 + String totalAmount = "0.01"; + + // (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段 + // 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】 + String undiscountableAmount = ""; + + // 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号) + // 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID + String sellerId = ""; + + // (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持 + String storeId = "test_store_id"; + + // 商户操作员编号,添加此参数可以为商户操作员做销售统计 + String operatorId = "test_operator_id"; + + + // 商品明细列表,需填写购买商品详细信息, + List goodsDetailList = new ArrayList<>(); + GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "全麦小面包", 1, 1); + goodsDetailList.add(goods1); + GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "黑人牙刷", 1, 2); + goodsDetailList.add(goods2); + + // 支付超时,线下扫码交易定义为5分钟 + String timeoutExpress = "5m"; + + AlipayTradePrecreateRequestBuilder builder =new AlipayTradePrecreateRequestBuilder() + .setSubject(subject) + .setTotalAmount(totalAmount) + .setOutTradeNo(outTradeNo) + .setUndiscountableAmount(undiscountableAmount) + .setSellerId(sellerId) + .setBody(body) + .setOperatorId(operatorId) + .setStoreId(storeId) + .setTimeoutExpress(timeoutExpress) + //支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置 + .setNotifyUrl(aliPayProperties.getNotifyUrl()) + .setGoodsDetailList(goodsDetailList); + + AlipayF2FPrecreateResult result = alipayTradeService.tradePrecreate(builder); + String qrCodeUrl = null; + switch (result.getTradeStatus()) { + case SUCCESS: + log.info("支付宝预下单成功: )"); + + AlipayTradePrecreateResponse res = result.getResponse(); + File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "images/"); + if (!file.exists()) { + file.mkdirs(); + } + String absolutePath = file.getAbsolutePath(); + String fileName = String.format("%sqr-%s.png", File.separator, res.getOutTradeNo()); + String filePath = new StringBuilder(absolutePath).append(fileName).toString(); + + // 这里只是演示将图片写到服务器中,实际可以返回二维码让前端去生成 + String basePath = request.getScheme()+ "://"+request.getServerName()+":"+ request.getServerPort()+request.getContextPath()+"/"; + qrCodeUrl = basePath + fileName; + response.getWriter().write(""); + ZxingUtils.getQRCodeImge(res.getQrCode(), 256, filePath); + break; + + case FAILED: + log.error("支付宝预下单失败!!!"); + break; + + case UNKNOWN: + log.error("系统异常,预下单状态未知!!!"); + break; + + default: + log.error("不支持的交易状态,交易返回异常!!!"); + break; + } + } + + + + /** + * 退款 + * @param orderNo 商户订单号 + * @return + */ + @PostMapping("/refund") + public String refund(String orderNo){ + AlipayTradeRefundRequestBuilder builder = new AlipayTradeRefundRequestBuilder() + .setOutTradeNo(orderNo) + .setRefundAmount("0.01") + .setRefundReason("当面付退款测试") + .setOutRequestNo(String.valueOf(System.nanoTime())) + .setStoreId("A1"); + AlipayF2FRefundResult result = alipayTradeService.tradeRefund(builder); + switch (result.getTradeStatus()) { + case SUCCESS: + log.info("支付宝退款成功: )"); + break; + + case FAILED: + log.error("支付宝退款失败!!!"); + break; + + case UNKNOWN: + log.error("系统异常,订单退款状态未知!!!"); + break; + + default: + log.error("不支持的交易状态,交易返回异常!!!"); + break; + } + + return result.getResponse().getBody(); + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayPagePayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayPagePayController.java new file mode 100644 index 0000000..5d7bf6a --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayPagePayController.java @@ -0,0 +1,81 @@ +package com.example.pay.controller; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.domain.AlipayTradePagePayModel; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.example.pay.configuration.AlipayProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.UUID; + +/** + * 支付宝-电脑网站支付. + * 电脑网站支付 https://docs.open.alipay.com/270/105898/ + */ +@Controller +@RequestMapping("/alipay/page") +public class AlipayPagePayController { + + @Autowired + private AlipayProperties alipayProperties; + + @Autowired + private AlipayClient alipayClient; + + @Autowired + private AlipayController alipayController; + + + @PostMapping("/gotoPayPage") + public void gotoPayPage(HttpServletResponse response) throws AlipayApiException, IOException { + // 订单模型 + String productCode = "FAST_INSTANT_TRADE_PAY"; + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + model.setOutTradeNo(UUID.randomUUID().toString()); + model.setSubject("支付测试"); + model.setTotalAmount("0.01"); + model.setBody("支付测试,共0.01元"); + model.setProductCode(productCode); + + AlipayTradePagePayRequest pagePayRequest =new AlipayTradePagePayRequest(); + pagePayRequest.setReturnUrl("http://s9v2cw.natappfree.cc/alipay/page/returnUrl"); + pagePayRequest.setNotifyUrl(alipayProperties.getNotifyUrl()); + pagePayRequest.setBizModel(model); + + // 调用SDK生成表单, 并直接将完整的表单html输出到页面 + String form = alipayClient.pageExecute(pagePayRequest).getBody(); + response.setContentType("text/html;charset=" + alipayProperties.getCharset()); + response.getWriter().write(form); + response.getWriter().flush(); + response.getWriter().close(); + } + + @RequestMapping("/returnUrl") + public String returnUrl(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, AlipayApiException { + response.setContentType("text/html;charset=" + alipayProperties.getCharset()); + + boolean verifyResult = alipayController.rsaCheckV1(request); + if(verifyResult){ + //验证成功 + //请在这里加上商户的业务逻辑程序代码,如保存支付宝交易号 + //商户订单号 + String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8"); + //支付宝交易号 + String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8"); + + return "pagePaySuccess"; + + }else{ + return "pagePayFail"; + + } + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayWAPPayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayWAPPayController.java new file mode 100644 index 0000000..cacd628 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/AlipayWAPPayController.java @@ -0,0 +1,116 @@ +package com.example.pay.controller; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.example.pay.configuration.AlipayProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.UUID; + +/** + * 支付宝-手机网站支付. + */ +@Slf4j +@Controller +@RequestMapping("/alipay/wap") +public class AlipayWAPPayController { + + @Autowired + private AlipayProperties alipayProperties; + + @Autowired + private AlipayClient alipayClient; + + /** + * 去支付 + * + * 支付宝返回一个form表单,并自动提交,跳转到支付宝页面 + * + * @param response + * @throws Exception + */ + @PostMapping("/gotoPayPage") + public void gotoPayPage(HttpServletResponse response) throws AlipayApiException, IOException { + // 订单模型 + String productCode="QUICK_WAP_WAY"; + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + model.setOutTradeNo(UUID.randomUUID().toString()); + model.setSubject("支付测试"); + model.setTotalAmount("0.01"); + model.setBody("支付测试,共0.01元"); + model.setTimeoutExpress("5m"); + model.setProductCode(productCode); + + AlipayTradeWapPayRequest wapPayRequest =new AlipayTradeWapPayRequest(); + wapPayRequest.setReturnUrl("http://yxep7y.natappfree.cc/alipay/wap/returnUrl"); + wapPayRequest.setNotifyUrl(alipayProperties.getNotifyUrl()); + wapPayRequest.setBizModel(model); + + // 调用SDK生成表单, 并直接将完整的表单html输出到页面 + String form = alipayClient.pageExecute(wapPayRequest).getBody(); + System.out.println(form); + response.setContentType("text/html;charset=" + alipayProperties.getCharset()); + response.getWriter().write(form); + response.getWriter().flush(); + response.getWriter().close(); + } + + + /** + * 支付宝页面跳转同步通知页面 + * @param request + * @return + * @throws UnsupportedEncodingException + * @throws AlipayApiException + */ + @RequestMapping("/returnUrl") + public String returnUrl(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, AlipayApiException { + response.setContentType("text/html;charset=" + alipayProperties.getCharset()); + + //获取支付宝GET过来反馈信息 + Map params = new HashMap<>(); + Map requestParams = request.getParameterMap(); + for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) { + String name = (String) iter.next(); + String[] values = (String[]) requestParams.get(name); + String valueStr = ""; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] + : valueStr + values[i] + ","; + } + //乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化 + valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8"); + params.put(name, valueStr); + } + + boolean verifyResult = AlipaySignature.rsaCheckV1(params, alipayProperties.getAlipayPublicKey(), alipayProperties.getCharset(), "RSA2"); + if(verifyResult){ + //验证成功 + //请在这里加上商户的业务逻辑程序代码,如保存支付宝交易号 + //商户订单号 + String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8"); + //支付宝交易号 + String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8"); + + return "wapPaySuccess"; + + }else{ + return "wapPayFail"; + + } + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayController.java new file mode 100644 index 0000000..8e2b7ec --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayController.java @@ -0,0 +1,166 @@ +package com.example.pay.controller; + +import com.example.pay.configuration.MyWXPayConfig; +import com.example.pay.configuration.WXPayClient; +import com.github.wxpay.sdk.WXPay; +import com.github.wxpay.sdk.WXPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付 - 通用API. + * + *

+ * 类似支付宝中的条码支付. + */ +@Slf4j +@RestController +@RequestMapping("/wxpay") +public class WXPayController { + + @Autowired + private WXPay wxPay; + + @Autowired + private WXPayClient wxPayClient; + + @Autowired + private MyWXPayConfig wxPayConfig; + + + + /** + * 订单查询 + * @param orderNo + * @return + * @throws Exception + */ + @GetMapping("/orderQuery") + public Object orderQuery(String orderNo) throws Exception { + Map data = new HashMap<>(); + data.put("out_trade_no", orderNo); + Map result = wxPay.orderQuery(data); + + return result; + } + + /** + * 退款 + * 注意:调用申请退款、撤销订单接口需要商户证书 + * 注意:沙箱环境响应结果可能会是"沙箱支付金额不正确,请确认验收case",但是正式环境不会报这个错误 + * 微信支付的最小金额是0.1元,所以在测试支付时金额必须大于0.1元,否则会提示微信支付配置错误,可以将microPay的total_fee大于1再退款 + */ + @PostMapping("/refund") + public Object refund(String orderNo) throws Exception { + Map reqData = new HashMap<>(); + // 商户订单号 + reqData.put("out_trade_no", orderNo); + // 授权码 + reqData.put("out_refund_no", orderNo); + // 订单总金额,单位为分,只能为整数 + reqData.put("total_fee", "1010"); + //退款金额 + reqData.put("refund_fee", "2"); + // 退款异步通知地址 + reqData.put("notify_url", wxPayConfig.getNotifyUrl()); + reqData.put("refund_fee_type", "CNY"); + reqData.put("op_user_id", wxPayConfig.getMchID()); + + Map resultMap = wxPay.refund(reqData); + log.info(resultMap.toString()); + + return resultMap; + } + + + /** + * 退款结果通知 + * + * 特别说明:退款结果对重要的数据进行了加密,商户需要用商户秘钥进行解密后才能获得结果通知的内容 + * @param request + * @throws Exception + */ + @RequestMapping("/refund/notify") + public String refundNotify(HttpServletRequest request) throws Exception { + +// Map notifyMap = new HashMap<>(); +// notifyMap.put("nonce_str", "9b4e428ae262d5dca96178027e849fa9"); +// notifyMap.put("req_info", "VKGj8c81RwQ45LcyWEVBE9/HsNfADGbgmbIAQZ2ydcbIFhIIcJFKFQwGfcSGgFGtQlWvg6KDNsRjmCjN+PvipJ3roynJ7cME0LOFG50VGtk4EYHqdjFzUVANI7GpT+i6Ok+ZWivH0MwoGK2fsz3WG+bYs2XJBwav/K89tKjFhZGitCKKBeGqcP99fa/gAL0swNXXNQHmL806Zi+QcACzL3E89BtP9FlXM2Gi+wPQafvPr+/iE+LrPdMlNUa5LiZnenZXUF24kMdhaTafczcKL4sZjRXQHEfEyc/pIZPbIjcNIETvHsskyzKuHVr/SAFkxaM6RR1Kl9pyWZGUjkH5SOeqsT8uL7YQmTlDXrnXmno3AvZdnepTGL5w69yIOmQNPeMqsd01ES9WX36GZYOigfi2+BJ9RRXjIffmpB/MFF+zryyvLTaJE2obCwFSHrmOD8YbaJqrZXOUvWZQrn7wIQgaCypo8V57cD3w5d2RSgIHNrdnEDYlbRcLNYgKuL+T9+1HPhU/frowZgwPN9IB53OahZV3p1Yvos23kvhqPCLn3BYgUihRbey6QhEtL2QyifiQ9e8WVLzWpRZ+DOa8VrhYvSuTfjRdjoNanqHFvXGP6uEsEa+DETqnexpB7xOS9m/CdmlNCwbdUplPEVzNQQdzYT4kybi00Y8A+EdairxfVyK9A7MAYAMtAO9yxV2ht0bn3SofFyZe/YSzdJgxdtcxBf1CVYN6x+yHcSueCSgq4cM/2VCwh4J1+pUVmNpEm0OVcdKbV5USkaxJR0h7Yd+n5FTz5Q2S/qvyDo202cUzLFPI5UqQm5X+FOrWDAkmmr5yVcDQIm3dAdb31jkz0X2TPYt5g7ciQ1h9heyVxJ67FexKvEM4pKCCubtWx6nyxcOUghHMrh8DSoBtewtNjbnwGVIbLsSb6X9MIYAkWIDbqNVP1f63GiZU+cJlhBmvcb8aeQUdTTj7EX5pOTIVSVv5D6SkKmpGU4FGvV+WjufuGX4ZRZo+01p6xl0sfZVmucG1UtxhX6bMCJb06yDwxpv7tGwkwS4TCK4SQp40Xe0="); +// notifyMap.put("appid", "xxx"); +// notifyMap.put("mch_id", "xxx"); +// notifyMap.put("return_code", "SUCCESS"); + + // 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 + // 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。 + // 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 + // TODO 处理业务 + Map requstInfoMap = wxPayClient.decodeRefundNotify(request); + + // 商户处理退款通知参数后同步返回给微信参数 + Map responseMap = new HashMap<>(); + responseMap.put("return_code", "SUCCESS"); + responseMap.put("return_msg", "OK"); + String responseXml = WXPayUtil.mapToXml(responseMap); + return responseXml; + } + + /** + * 退款查询 + * @param orderNo + * @return + * @throws Exception + */ + @GetMapping("/refundQuery") + public Object refundQuery(String orderNo) throws Exception { + Map reqData = new HashMap<>(); + reqData.put("out_trade_no", orderNo); + Map result = wxPay.refundQuery(reqData); + + return result; + } + + + /** + * 下载对账单 + * 注意: + * 微信在次日9点启动生成前一天的对账单,建议商户10点后再获取; + * 对账单接口只能下载三个月以内的账单。 + * @throws Exception + */ + @PostMapping("/downloadBill") + public Object downloadBill(String billDate) throws Exception { + Map reqData = new HashMap<>(); + reqData.put("bill_date", billDate); + reqData.put("bill_type", "ALL"); + Map resultMap = wxPay.downloadBill(reqData); + + return resultMap; + } + + + + + /** + * 获取沙箱环境API秘钥, + * + * 这里只是为了获取,可以放在main方法下运行,这里作为api来运行的,实际情况下不应该暴露出来 + * @return + * @throws Exception + */ + @GetMapping("/sandbox/getSignKey") + public Object getSignKey() throws Exception { + Map signKey = wxPayClient.getSignKey(); + log.info(signKey.toString()); + + return signKey; + } + +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayH5PayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayH5PayController.java new file mode 100644 index 0000000..a822d27 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayH5PayController.java @@ -0,0 +1,106 @@ +package com.example.pay.controller; + +import com.example.pay.configuration.WXPayClient; +import com.github.wxpay.sdk.WXPay; +import com.github.wxpay.sdk.WXPayConstants; +import com.github.wxpay.sdk.WXPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付-H5支付. + */ +@Slf4j +@RestController +@RequestMapping("/wxpay/h5pay") +public class WXPayH5PayController { + + @Autowired + private WXPay wxPay; + + @Autowired + private WXPayClient wxPayClient; + + /** + * 使用沙箱支付的金额必须是用例中指定的金额,也就是 1.01 元,1.02元等,不能是你自己的商品的实际价格,必须是这个数。 + * 否则会报错:沙箱支付金额(2000)无效,请检查需要验收的case + * @return + * @throws Exception + */ + @PostMapping("/order") + public Object h5pay() throws Exception { + Map reqData = new HashMap<>(); + reqData.put("out_trade_no", String.valueOf(System.nanoTime())); + reqData.put("trade_type", "MWEB"); + reqData.put("product_id", "1"); + reqData.put("body", "商户下单"); + // 订单总金额,单位为分 + reqData.put("total_fee", "101"); + // APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 + reqData.put("spbill_create_ip", "14.23.150.211"); + // 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 + reqData.put("notify_url", "http://3sbqi7.natappfree.cc/wxpay/h5pay/notify"); + // 自定义参数, 可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" + reqData.put("device_info", ""); + // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 + reqData.put("attach", ""); + reqData.put("scene_info", "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"http://3sbqi7.natappfree.cc\",\"wap_name\": \"腾讯充值\"}}"); + + Map responseMap = wxPay.unifiedOrder(reqData); + log.info(responseMap.toString()); + String returnCode = responseMap.get("return_code"); + String resultCode = responseMap.get("result_code"); + if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { + // 预支付交易会话标识 + String prepayId = responseMap.get("prepay_id"); + // 支付跳转链接(前端需要在该地址上拼接redirect_url,该参数不是必须的) + // 正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数,来指定回调页面 + // 需对redirect_url进行urlencode处理 + + // TODO 正常情况下这里应该是普通的链接,不知道这里为何是weixin://这样的链接,不知道是不是微信公众平台上的配置少配置了; + // 由于没有实际账号,还没找到为啥不是普通链接的原因 + String mwebUrl = responseMap.get("mweb_url"); + } + + return responseMap; + } + + /** + * 注意:如果是沙箱环境,一提交订单就会立即异步通知,而无需拉起微信支付收银台的中间页面 + * @param request + * @throws Exception + */ + @RequestMapping("/notify") + public void payNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{ + Map reqData = wxPayClient.getNotifyParameter(request); + log.info(reqData.toString()); + + + String returnCode = reqData.get("return_code"); + String resultCode = reqData.get("result_code"); + if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { + boolean signatureValid = wxPay.isPayResultNotifySignatureValid(reqData); + + if (signatureValid) { + // TODO 业务处理 + + Map responseMap = new HashMap<>(2); + responseMap.put("return_code", "SUCCESS"); + responseMap.put("return_msg", "OK"); + String responseXml = WXPayUtil.mapToXml(responseMap); + + response.setContentType("text/xml"); + response.getWriter().write(responseXml); + response.flushBuffer(); + } + } + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayMicroPayController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayMicroPayController.java new file mode 100644 index 0000000..2d7fa65 --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayMicroPayController.java @@ -0,0 +1,57 @@ +package com.example.pay.controller; + +import com.example.pay.configuration.WXPayClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付-刷卡支付. + */ +@Slf4j +@RestController +@RequestMapping("/wxpay/microPay") +public class WXPayMicroPayController { + + @Autowired + private WXPayClient wxPayClient; + + /** + * 刷卡支付(类似支付宝的条码支付) + * + * 和支付宝的好像不一样,支付宝有支付通知,但是微信好像没,微信有退款通知 + * + * 微信支付后台系统收到支付请求,根据验证密码规则判断是否验证用户的支付密码,不需要验证密码的交易直接发起扣款, + * 需要验证密码的交易会弹出密码输入框。支付成功后微信端会弹出成功页面,支付失败会弹出错误提示 + * 注意该接口有可能返回错误码为USERPAYING用户支付中 + * + * 验证密码规则 + * ◆ 支付金额>1000元的交易需要验证用户支付密码 + * ◆ 用户账号每天最多有5笔交易可以免密,超过后需要验证密码 + * ◆ 微信支付后台判断用户支付行为有异常情况,符合免密规则的交易也会要求验证密码 + * + * 用户刷卡条形码规则:18位纯数字,以10、11、12、13、14、15开头 + */ + @PostMapping("") + public Object microPay(String authCode) throws Exception { + Map reqData = new HashMap<>(); + // 商户订单号 + reqData.put("out_trade_no", String.valueOf(System.nanoTime())); + // 订单总金额,单位为分,只能为整数 + reqData.put("total_fee", "1010"); + // 授权码 + reqData.put("auth_code", authCode); + // 商品描述 + reqData.put("body", "测试"); + Map resultMap = wxPayClient.microPayWithPOS(reqData); + log.info(resultMap.toString()); + + return resultMap; + } + +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayPrecreateController.java b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayPrecreateController.java new file mode 100644 index 0000000..4e5a6eb --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/controller/WXPayPrecreateController.java @@ -0,0 +1,139 @@ +package com.example.pay.controller; + +import com.example.pay.configuration.WXPayClient; +import com.example.pay.util.PayUtil; +import com.github.wxpay.sdk.WXPay; +import com.github.wxpay.sdk.WXPayConstants; +import com.github.wxpay.sdk.WXPayUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付-扫码支付. + */ +@Slf4j +@RestController +@RequestMapping("/wxpay/precreate") +public class WXPayPrecreateController { + @Autowired + private WXPay wxPay; + + @Autowired + private WXPayClient wxPayClient; + + /** + * 扫码支付 - 统一下单 + * 相当于支付不的电脑网站支付 + * + * 扫码支付API + */ + @PostMapping("/order") + public void precreate(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map reqData = new HashMap<>(); + reqData.put("out_trade_no", String.valueOf(System.nanoTime())); + reqData.put("trade_type", "NATIVE"); + reqData.put("product_id", "1"); + reqData.put("body", "商户下单"); + // 订单总金额,单位为分 + reqData.put("total_fee", "2"); + // APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 + reqData.put("spbill_create_ip", "14.23.150.211"); + // 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 + reqData.put("notify_url", "http://3sbqi7.natappfree.cc/wxpay/precreate/notify"); + // 自定义参数, 可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB" + reqData.put("device_info", ""); + // 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用。 + reqData.put("attach", ""); + + /** + * { + * code_url=weixin://wxpay/bizpayurl?pr=vvz4xwC, + * trade_type=NATIVE, + * return_msg=OK, + * result_code=SUCCESS, + * return_code=SUCCESS, + * prepay_id=wx18111952823301d9fa53ab8e1414642725 + * } + */ + Map responseMap = wxPay.unifiedOrder(reqData); + log.info(responseMap.toString()); + String returnCode = responseMap.get("return_code"); + String resultCode = responseMap.get("result_code"); + if (WXPayConstants.SUCCESS.equals(returnCode) && WXPayConstants.SUCCESS.equals(resultCode)) { + String prepayId = responseMap.get("prepay_id"); + String codeUrl = responseMap.get("code_url"); + + BufferedImage image = PayUtil.getQRCodeImge(codeUrl); + + response.setContentType("image/jpeg"); + response.setHeader("Pragma","no-cache"); + response.setHeader("Cache-Control","no-cache"); + response.setIntHeader("Expires",-1); + ImageIO.write(image, "JPEG", response.getOutputStream()); + } + + } + + /** + * + * @param request + * @return + * @throws Exception + */ + @RequestMapping("/notify") + public void precreateNotify(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map reqData = wxPayClient.getNotifyParameter(request); + + /** + * { + * transaction_id=4200000138201806180751222945, + * nonce_str=aaaf3fe4d3aa44d8b245bc6c97bda7a8, + * bank_type=CFT, + * openid=xxx, + * sign=821A5F42F5E180ED9EF3743499FBCF13, + * fee_type=CNY, + * mch_id=xxx, + * cash_fee=1, + * out_trade_no=186873223426017, + * appid=xxx, + * total_fee=1, + * trade_type=NATIVE, + * result_code=SUCCESS, + * time_end=20180618131247, + * is_subscribe=N, + * return_code=SUCCESS + * } + */ + log.info(reqData.toString()); + + // 特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。 + boolean signatureValid = wxPay.isPayResultNotifySignatureValid(reqData); + if (signatureValid) { + /** + * 注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 + * 推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态, + * 判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。 + * 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。 + */ + + Map responseMap = new HashMap<>(2); + responseMap.put("return_code", "SUCCESS"); + responseMap.put("return_msg", "OK"); + String responseXml = WXPayUtil.mapToXml(responseMap); + + response.setContentType("text/xml"); + response.getWriter().write(responseXml); + response.flushBuffer(); + } + } +} diff --git a/springboot-pay-example/src/main/java/com/example/pay/util/PayUtil.java b/springboot-pay-example/src/main/java/com/example/pay/util/PayUtil.java new file mode 100644 index 0000000..849aa3f --- /dev/null +++ b/springboot-pay-example/src/main/java/com/example/pay/util/PayUtil.java @@ -0,0 +1,38 @@ +package com.example.pay.util; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import java.awt.image.BufferedImage; +import java.util.Hashtable; +import java.util.Map; + +public class PayUtil { + + /** + * 根据url生成二位图片对象 + * + * @param codeUrl + * @return + * @throws WriterException + */ + public static BufferedImage getQRCodeImge(String codeUrl) throws WriterException { + Map hints = new Hashtable(); + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); + hints.put(EncodeHintType.CHARACTER_SET, "UTF8"); + int width = 256; + BitMatrix bitMatrix = (new MultiFormatWriter()).encode(codeUrl, BarcodeFormat.QR_CODE, width, width, hints); + BufferedImage image = new BufferedImage(width, width, 1); + for(int x = 0; x < width; ++x) { + for(int y = 0; y < width; ++y) { + image.setRGB(x, y, bitMatrix.get(x, y) ? -16777216 : -1); + } + } + + return image; + } +} diff --git a/springboot-pay-example/src/main/resources/application.yml b/springboot-pay-example/src/main/resources/application.yml new file mode 100644 index 0000000..3bc4dd9 --- /dev/null +++ b/springboot-pay-example/src/main/resources/application.yml @@ -0,0 +1,25 @@ +# 沙箱账号 +pay: + alipay: + gatewayUrl: https://openapi.alipaydev.com/gateway.do + appid: 2016091400508498 + appPrivateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtXKWs+trRSuCxEUvlsEeSAuLWs3B/Dh74R8223BnfzoA29aGeoycAqfKlBMcbzU2G1KayESvZKGpwBLeejSbecRYjgZsQDyEaDimQQJtGFvZVV6u4XNnvIJ72eQzEaEIQfuiorjBTLm6DQuds4R0GxftqON6QFoIZkWB9ZrZKd02cuy16dW+UqtLVGGAHcCIAkB63zUiKSNfweMddneZ7MVs3lvu3xhMnD+5us/+n2Vp4qhfmpYLcdqIW6InU4GypeoOpyUTrfUGpgdR0l924vHy/GQJZEKFaRcK3cYK+ECyKpSIoqaJJFLHbkqsliuPpMUG+rM3jiqeIAH4psAznAgMBAAECggEBAJ5jyEbbxrJzrAh7GhHX1fwUQPYSadTbrPYAfHX2cHlnrQMJtsk+nTLhEv0r+VJwZ8WpYkfMong8kcqYtL7ajcmsHqMAFhE9EWxBxj2ymWsXLabZe93sj30IG9Rq0nxcGQgDO0RqKWLGSFgK93Al2KRInKT3InkY53K+vR61ihVLmGf7+GwPtIZteBy+tuAyvcj2RlkYvjiFi3ySyZ1wA3sJIlgrGiixd6fj20XFGNE3CnAwu0BJgXXbP/S9J4C0RRa3ZXj8fX7oONhVxz2xKxn7AT4e8OWjt7J41H2LRct8Fgl9pqgz2FJYv/WfbkG8x9fGiKYYvPXoxjjrk/tkewkCgYEA8f9Lcu5JPrE9rpw9zlwhm5cOO81xLxdwL5R5/1bRP48BZGIYuqlCbVvjJVqtO8eTnLhUwH7fG8B7cmoeO9bGr9GQrtfyCqz6FtVymTBieJlfgZDVhtzyv2qKOBMIFE8jsbSBK/NHHMvykJ+XdQ1riwCeQDdXICRuYTTFwGk2OsUCgYEAt2SoN95tVmVrvKG6ATLNEtge/ozeVywA4GjltrSw/G9vqp+DkkT2pY19uROuzMazoTzKWpPho2q/qzNlv/ANbOFM2GEmKamQ7CO88JgRxMsPTvc/HxCLU/ClMJU8LcOf9LfP2KYZpPwuheKJoF4vDGj8NsbFmccJyYSdpkNEk7sCgYBJlL2FMaz1sgC2Ue19DIhvfaunRV1P20mSPgwmNmijccETm7w3LXX0OIdFeV/JGHLqqSWj7i+6iXk/ncKZoUGCfi8G6sQ+uL/GJ5qTt6GJV+ExTS+PtSjeSO/EAw1m13Vb+C16hpstx1l23f+4aJ81gbecgPct38XsKpaiXZtOnQKBgQCMsN7QRYYxwoq9YoDUzIlAzKYyeBVWYL6najHYUZR5hG/xQIBqZRem9/4cTvpJxKInrwA6LrrqaEl0aHDFp75U6h7O3PCvA5PXZK9dD/yJsZIj7U/yX/nTQokn1UUegrYiwiTkusBvrrtuINWePsLvTVc4GpObHnPmsiNTWsWwYwKBgENaeTNOCHV2km/ysXQSEIhKbtlAMQPsgWHCt/bzHlF9m18izb1LrJyjzcSsd+Zy78R+pv4G50Q27c3e/DFPz/wYxN/yHWRbyLBA8ipJbCtMtPEdS9krpmN6cChIdLGbz4CVUqOPSRzNb9lhhgPCcCNRq6DG3HBceb1Se9VnO3zk + alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApFQKccMq+wPoWh93DcX3XYrnT7FJ3gntJA/jEwgk6Jd3iEVS2CyUCCgFVcQn8xjXT81YbZHYvoC50IBuu+A+Ey+J8VIgsBm5g9uwbOLRf66GrZjuKOlalHm5gHXjcL2gZRMBJEStOxstcO2YQriqhQzdL3EKp+HQc9u14IOVFpZdR8qq1l7CzKHn1vQo/1fUPfUrTLQqSuQTU9Ospv/QHFqOJA7DPetUQ+jnaZ082f3clr4ITw4EE8A6IWqTXcETTx5j/udCGP84g2Y4j+8i9DqYGyD5ePVgt4G0ICBX1bi1qNlylfxRg8Y3c1DFrRGyr0NpKQxSVXkYaVNvrCoudQIDAQAB + returnUrl: http://s9v2cw.natappfree.cc/alipay/return + notifyUrl: http://s9v2cw.natappfree.cc/alipay/notify + wxpay: + appID: wxab8acb865bb1637e + mchID: 11473623 + key: 2ab9071b06b9f739b950ddb41db2690d + sandboxKey: 3639bc1370e105aa65f10cd4fef2a3ef + certPath: /var/local/cert/apiclient_cert.p12 + notifyUrl: http://65ta5j.natappfree.cc/wxpay/refund/notify + useSandbox: true +spring: + thymeleaf: + prefix: classpath:/templates/ + suffix: .html + mode: HTML5 + encoding: UTF-8 + + diff --git a/springboot-pay-example/src/main/resources/templates/gotoH5Page.html b/springboot-pay-example/src/main/resources/templates/gotoH5Page.html new file mode 100644 index 0000000..f8b547a --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/gotoH5Page.html @@ -0,0 +1,34 @@ + + + + + Title + + + +

购买商品:iphone

+

价格:8888

+

数量:1个

+ + + + + + + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/gotoPagePay.html b/springboot-pay-example/src/main/resources/templates/gotoPagePay.html new file mode 100644 index 0000000..8b9d02a --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/gotoPagePay.html @@ -0,0 +1,17 @@ + + + + + Title + + + +
+

购买商品:iphone

+

价格:8888

+

数量:1个

+ +
+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/gotoWapPay.html b/springboot-pay-example/src/main/resources/templates/gotoWapPay.html new file mode 100644 index 0000000..583d18d --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/gotoWapPay.html @@ -0,0 +1,18 @@ + + + + + Title + + + +
+

购买商品:iphone

+

价格:8888

+

数量:1个

+ + +
+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/h5PaySuccess.html b/springboot-pay-example/src/main/resources/templates/h5PaySuccess.html new file mode 100644 index 0000000..efc3a19 --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/h5PaySuccess.html @@ -0,0 +1,12 @@ + + + + + Title + + + +

微信支付-H5支付成功

+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/pagePayFail.html b/springboot-pay-example/src/main/resources/templates/pagePayFail.html new file mode 100644 index 0000000..6bf70c9 --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/pagePayFail.html @@ -0,0 +1,12 @@ + + + + + Title + + + +

电脑网站支付失败,请重新支付

+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/pagePaySuccess.html b/springboot-pay-example/src/main/resources/templates/pagePaySuccess.html new file mode 100644 index 0000000..7a8a92f --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/pagePaySuccess.html @@ -0,0 +1,12 @@ + + + + + Title + + + +

电脑网站支付成功

+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/wapPayFail.html b/springboot-pay-example/src/main/resources/templates/wapPayFail.html new file mode 100644 index 0000000..dd70097 --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/wapPayFail.html @@ -0,0 +1,12 @@ + + + + + Title + + + +

手机网站支付失败,请重新支付

+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/main/resources/templates/wapPaySuccess.html b/springboot-pay-example/src/main/resources/templates/wapPaySuccess.html new file mode 100644 index 0000000..882eff3 --- /dev/null +++ b/springboot-pay-example/src/main/resources/templates/wapPaySuccess.html @@ -0,0 +1,12 @@ + + + + + Title + + + +

手机网站支付成功

+ + + \ No newline at end of file diff --git a/springboot-pay-example/src/test/java/com/example/pay/SpringbootPayExampleApplicationTests.java b/springboot-pay-example/src/test/java/com/example/pay/SpringbootPayExampleApplicationTests.java new file mode 100644 index 0000000..0df7aa8 --- /dev/null +++ b/springboot-pay-example/src/test/java/com/example/pay/SpringbootPayExampleApplicationTests.java @@ -0,0 +1,16 @@ +package com.example.pay; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SpringbootPayExampleApplicationTests { + + @Test + public void contextLoads() { + } + +}