• 微信支付项目实战、创建订单到支付退款代码详解


    1、微信支付产品介绍

    微信支付开发文档:

    微信支付-开发者文档 (qq.com)

    源码地址:前端后端都有

    链接: https://pan.baidu.com/s/1Nx-jLJ1gaZD0rmoGOw9MhA

    提取码: qwer 

    1.1、付款码支付

    用户展示微信钱包内的 付款码 给商家,商家扫描后直接完成支付,适用于线下面对面收银的场景。

    1.2JSAPI支付

    线下场所:商户展示一个支付二维码,用户使用微信扫描二维码后,输入需要支付的金额,完成支
    付。
    公众号场景:用户在微信内进入商家公众号,打开某个页面,选择某个产品,完成支付。
    PC 网站场景:在网站中展示二维码,用户使用微信扫描二维码,输入需要支付的金额,完成支
    付。
    特点:用户在客户端输入支付金额

    1.3、小程序支付

    在微信小程序平台内实现支付的功能。

    1.4Native支付

    Native 支付是指商户展示支付二维码,用户再用微信 扫一扫 完成支付的模式。这种方式适用于 PC
    站。
    特点:商家预先指定支付金额

    1.5APP支付

    商户通过在移动端独立的 APP 应用程序中集成微信支付模块,完成支付。

    1.6、刷脸支付

    用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式。

    2、接入指引

    2.1、获取商户号

    微信商户平台: https://pay.weixin.qq.com/
    场景: Native 支付
    步骤:提交资料 => 签署协议 => 获取商户号

    2.2、获取APPID

    APIV2、APIV3一个模式

    步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 设置 API 密钥
    随机密码生成工具: https://suijimimashengcheng.bmcx.com/

    2.5、申请商户API证书

    APIv3 版本的所有接口都需要; APIv2 版本的高级接口需要(如:退款、企业红包、企业付款等)
    步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API 安全 => 申请 API 证书

    2.6、获取微信平台证书

    可以预先下载,也可以通过编程的方式获取。后面的课程中,我们会通过编程的方式来获取。
    注意:以上所有 API 秘钥和证书需妥善保管防止泄露

    3.微信APIv3证书

    商户证书
    商户 API 证书是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
    商户证书在商户后台申请: https://pay.weixin.qq.com/index.php/core/cert/api_cert#/

    平台证书(微信支付平台):
    微信支付平台证书是指由 微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使
    用平台证书中的公钥进行验签。
    平台证书的获取: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml
    使用微信支付商户平台证书工具进行生成
    都是对称加密需要使用的加密和解密密钥,一定要保管好,不能泄露。
    API 密钥对应 V2 版本的 API
    APIv3 密钥对应 V3 版本的 API

     

     

    项目实现

    依赖

    1. <project xmlns="http://maven.apache.org/POM/4.0.0"
    2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0modelVersion>
    5. <packaging>jarpackaging>
    6. <groupId>org.examplegroupId>
    7. <artifactId>weixinZFartifactId>
    8. <version>1.0-SNAPSHOTversion>
    9. <properties>
    10. <maven.compiler.source>8maven.compiler.source>
    11. <maven.compiler.target>8maven.compiler.target>
    12. properties>
    13. <parent>
    14. <groupId>org.springframework.bootgroupId>
    15. <artifactId>spring-boot-starter-parentartifactId>
    16. <version>2.1.9.RELEASEversion>
    17. <relativePath/>
    18. parent>
    19. <dependencies>
    20. <dependency>
    21. <groupId>io.springfoxgroupId>
    22. <artifactId>springfox-swagger2artifactId>
    23. <version>2.7.0version>
    24. dependency>
    25. <dependency>
    26. <groupId>io.springfoxgroupId>
    27. <artifactId>springfox-swagger-uiartifactId>
    28. <version>2.7.0version>
    29. dependency>
    30. <dependency>
    31. <groupId>log4jgroupId>
    32. <artifactId>log4jartifactId>
    33. <version>1.2.14version>
    34. dependency>
    35. <dependency>
    36. <groupId>com.github.wechatpay-apiv3groupId>
    37. <artifactId>wechatpay-apache-httpclientartifactId>
    38. <version>0.3.0version>
    39. dependency>
    40. <dependency>
    41. <groupId>com.alipay.sdkgroupId>
    42. <artifactId>alipay-sdk-javaartifactId>
    43. <version>4.31.65.ALLversion>
    44. dependency>
    45. <dependency>
    46. <groupId>org.springframework.bootgroupId>
    47. <artifactId>spring-boot-dependenciesartifactId>
    48. <version>2.1.9.RELEASEversion>
    49. <type>pomtype>
    50. <scope>importscope>
    51. dependency>
    52. <dependency>
    53. <groupId>com.alibabagroupId>
    54. <artifactId>fastjsonartifactId>
    55. dependency>
    56. <dependency>
    57. <groupId>mysqlgroupId>
    58. <artifactId>mysql-connector-javaartifactId>
    59. dependency>
    60. <dependency>
    61. <groupId>com.baomidougroupId>
    62. <artifactId>mybatis-plus-boot-starterartifactId>
    63. <version>3.4.3version>
    64. dependency>
    65. <dependency>
    66. <groupId>org.springframework.bootgroupId>
    67. <artifactId>spring-boot-starter-webartifactId>
    68. dependency>
    69. <dependency>
    70. <groupId>org.springframework.bootgroupId>
    71. <artifactId>spring-boot-configuration-processorartifactId>
    72. <optional>trueoptional>
    73. dependency>
    74. <dependency>
    75. <groupId>org.projectlombokgroupId>
    76. <artifactId>lombokartifactId>
    77. <scope>providedscope>
    78. dependency>
    79. <dependency>
    80. <groupId>org.springframework.bootgroupId>
    81. <artifactId>spring-boot-starter-testartifactId>
    82. <exclusions>
    83. <exclusion>
    84. <groupId>org.junit.jupitergroupId>
    85. <artifactId>junit-jupiter-engineartifactId>
    86. exclusion>
    87. exclusions>
    88. dependency>
    89. <dependency>
    90. <groupId>com.alibabagroupId>
    91. <artifactId>fastjsonartifactId>
    92. <version>1.2.60version>
    93. <scope>compilescope>
    94. dependency>
    95. <dependency>
    96. <groupId>com.google.code.gsongroupId>
    97. <artifactId>gsonartifactId>
    98. dependency>
    99. <dependency>
    100. <groupId>org.springframework.bootgroupId>
    101. <artifactId>spring-boot-autoconfigureartifactId>
    102. dependency>
    103. dependencies>
    104. <build>
    105. <plugins>
    106. <plugin>
    107. <groupId>org.springframework.bootgroupId>
    108. <artifactId>spring-boot-maven-pluginartifactId>
    109. <version>2.1.9.RELEASEversion>
    110. <executions>
    111. <execution>
    112. <goals>
    113. <goal>repackagegoal>
    114. goals>
    115. execution>
    116. executions>
    117. <configuration>
    118. <includeSystemScope>trueincludeSystemScope>
    119. <mainClass>com.wx.WXapplicationmainClass>
    120. configuration>
    121. plugin>
    122. plugins>
    123. build>
    124. project>

    配置yml

    1. server:
    2. port: 8090
    3. spring:
    4. application:
    5. name: weixinZF
    6. datasource:
    7. #高版本驱动使用
    8. driver-class-name: com.mysql.cj.jdbc.Driver
    9. url: jdbc:mysql://127.0.0.1:3306/payment_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    10. #设定用户名和密码
    11. username: root
    12. password: root
    13. jackson:
    14. date-format: yyyy-MM-dd HH:mm:ss
    15. time-zone: GMT+8
    16. #SpringBoot整合Mybatis
    17. mybatis-plus:
    18. #指定别名包
    19. type-aliases-package: com.jt.pojo
    20. #扫描指定路径下的映射文件
    21. mapper-locations: classpath:/mapper/*.xml
    22. #开启驼峰映射
    23. configuration:
    24. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志
    25. map-underscore-to-camel-case: true
    26. # 一二级缓存默认开始 所以可以简化
    27. #打印mysql日志
    28. logging:
    29. level:
    30. com.jt.mapper: debug

    数据库创建

    订单表

    1. CREATE TABLE `t_order_info` (
    2. `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '订单id',
    3. `title` varchar(256) DEFAULT NULL COMMENT '订单标题',
    4. `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
    5. `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
    6. `product_id` bigint(20) DEFAULT NULL COMMENT '支付产品id',
    7. `total_fee` int(11) DEFAULT NULL COMMENT '订单金额(分)',
    8. `code_url` varchar(50) DEFAULT NULL COMMENT '订单二维码连接',
    9. `order_status` varchar(10) DEFAULT NULL COMMENT '订单状态',
    10. `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
    11. `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
    12. `payment_type` varchar(255) DEFAULT NULL COMMENT '支付类型(支付宝~微信)',
    13. PRIMARY KEY (`id`) USING BTREE
    14. ) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

    记录表

    1. CREATE TABLE `t_payment_info` (
    2. `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '支付记录id',
    3. `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
    4. `transaction_id` varchar(50) DEFAULT NULL COMMENT '支付系统交易编号',
    5. `payment_type` varchar(20) DEFAULT NULL COMMENT '支付类型',
    6. `trade_type` varchar(20) DEFAULT NULL COMMENT '交易类型',
    7. `trade_state` varchar(50) DEFAULT NULL COMMENT '交易状态',
    8. `payer_total` int(11) DEFAULT NULL COMMENT '支付金额(分)',
    9. `content` text DEFAULT NULL COMMENT '通知参数',
    10. `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
    11. `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
    12. PRIMARY KEY (`id`) USING BTREE
    13. ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

    商品表

    1. CREATE TABLE `t_product` (
    2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',
    3. `title` varchar(20) DEFAULT NULL COMMENT '商品名称',
    4. `price` int(11) DEFAULT NULL COMMENT '价格(分)',
    5. `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
    6. `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
    7. PRIMARY KEY (`id`) USING BTREE
    8. ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

    退款表

    1. CREATE TABLE `t_refund_info` (
    2. `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '退款单id',
    3. `order_no` varchar(50) DEFAULT NULL COMMENT '商户订单编号',
    4. `refund_no` varchar(50) DEFAULT NULL COMMENT '商户退款单编号',
    5. `refund_id` varchar(50) DEFAULT NULL COMMENT '支付系统退款单号',
    6. `total_fee` int(11) DEFAULT NULL COMMENT '原订单金额(分)',
    7. `refund` int(11) DEFAULT NULL COMMENT '退款金额(分)',
    8. `reason` varchar(50) DEFAULT NULL COMMENT '退款原因',
    9. `refund_status` varchar(10) DEFAULT NULL COMMENT '退款状态',
    10. `content_return` text DEFAULT NULL COMMENT '申请退款返回参数',
    11. `content_notify` text DEFAULT NULL COMMENT '退款结果通知参数',
    12. `create_time` datetime DEFAULT current_timestamp() COMMENT '创建时间',
    13. `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间',
    14. PRIMARY KEY (`id`) USING BTREE
    15. ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

    配置微信相关支付参数

    wxpay.properties

    1. # 微信支付相关参数
    2. # 商户号
    3. wxpay.mch-id=11111111
    4. # 商户API证书序列号
    5. wxpay.mch-serial-no=1111111111111111
    6. # 商户私钥文件
    7. wxpay.private-key-path=apiclient_key.pem
    8. # APIv3密钥
    9. wxpay.api-v3-key=111111111111
    10. # APPID
    11. wxpay.appid=111111111111111
    12. # 微信服务器地址
    13. wxpay.domain=https://api.mch.weixin.qq.com
    14. # 接收结果通知地址 使用内网穿透工具获取
    15. wxpay.notify-domain=http://pw46ia.natappfree.cc

    将私钥放置项目下

    配置Util工具类

    http请求客户端工具类

    1. package com.wx.util;
    2. import org.apache.http.Consts;
    3. import org.apache.http.HttpEntity;
    4. import org.apache.http.NameValuePair;
    5. import org.apache.http.client.ClientProtocolException;
    6. import org.apache.http.client.entity.UrlEncodedFormEntity;
    7. import org.apache.http.client.methods.*;
    8. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    9. import org.apache.http.conn.ssl.SSLContextBuilder;
    10. import org.apache.http.conn.ssl.TrustStrategy;
    11. import org.apache.http.entity.StringEntity;
    12. import org.apache.http.impl.client.CloseableHttpClient;
    13. import org.apache.http.impl.client.HttpClients;
    14. import org.apache.http.message.BasicNameValuePair;
    15. import org.apache.http.util.EntityUtils;
    16. import javax.net.ssl.SSLContext;
    17. import java.io.IOException;
    18. import java.security.cert.CertificateException;
    19. import java.security.cert.X509Certificate;
    20. import java.text.ParseException;
    21. import java.util.HashMap;
    22. import java.util.LinkedList;
    23. import java.util.List;
    24. import java.util.Map;
    25. /**
    26. * http请求客户端
    27. */
    28. public class HttpClientUtils {
    29. private String url;
    30. private Map param;
    31. private int statusCode;
    32. private String content;
    33. private String xmlParam;
    34. private boolean isHttps;
    35. public boolean isHttps() {
    36. return isHttps;
    37. }
    38. public void setHttps(boolean isHttps) {
    39. this.isHttps = isHttps;
    40. }
    41. public String getXmlParam() {
    42. return xmlParam;
    43. }
    44. public void setXmlParam(String xmlParam) {
    45. this.xmlParam = xmlParam;
    46. }
    47. public HttpClientUtils(String url, Map param) {
    48. this.url = url;
    49. this.param = param;
    50. }
    51. public HttpClientUtils(String url) {
    52. this.url = url;
    53. }
    54. public void setParameter(Map map) {
    55. param = map;
    56. }
    57. public void addParameter(String key, String value) {
    58. if (param == null)
    59. param = new HashMap();
    60. param.put(key, value);
    61. }
    62. public void post() throws ClientProtocolException, IOException {
    63. HttpPost http = new HttpPost(url);
    64. setEntity(http);
    65. execute(http);
    66. }
    67. public void put() throws ClientProtocolException, IOException {
    68. HttpPut http = new HttpPut(url);
    69. setEntity(http);
    70. execute(http);
    71. }
    72. public void get() throws ClientProtocolException, IOException {
    73. if (param != null) {
    74. StringBuilder url = new StringBuilder(this.url);
    75. boolean isFirst = true;
    76. for (String key : param.keySet()) {
    77. if (isFirst) {
    78. url.append("?");
    79. isFirst = false;
    80. }else {
    81. url.append("&");
    82. }
    83. url.append(key).append("=").append(param.get(key));
    84. }
    85. this.url = url.toString();
    86. }
    87. HttpGet http = new HttpGet(url);
    88. execute(http);
    89. }
    90. /**
    91. * set http post,put param
    92. */
    93. private void setEntity(HttpEntityEnclosingRequestBase http) {
    94. if (param != null) {
    95. List nvps = new LinkedList();
    96. for (String key : param.keySet())
    97. nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
    98. http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
    99. }
    100. if (xmlParam != null) {
    101. http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
    102. }
    103. }
    104. private void execute(HttpUriRequest http) throws ClientProtocolException,
    105. IOException {
    106. CloseableHttpClient httpClient = null;
    107. try {
    108. if (isHttps) {
    109. SSLContext sslContext = new SSLContextBuilder()
    110. .loadTrustMaterial(null, new TrustStrategy() {
    111. // 信任所有
    112. public boolean isTrusted(X509Certificate[] chain,
    113. String authType)
    114. throws CertificateException {
    115. return true;
    116. }
    117. }).build();
    118. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
    119. sslContext);
    120. httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
    121. .build();
    122. } else {
    123. httpClient = HttpClients.createDefault();
    124. }
    125. CloseableHttpResponse response = httpClient.execute(http);
    126. try {
    127. if (response != null) {
    128. if (response.getStatusLine() != null)
    129. statusCode = response.getStatusLine().getStatusCode();
    130. HttpEntity entity = response.getEntity();
    131. // 响应内容
    132. content = EntityUtils.toString(entity, Consts.UTF_8);
    133. }
    134. } finally {
    135. response.close();
    136. }
    137. } catch (Exception e) {
    138. e.printStackTrace();
    139. } finally {
    140. httpClient.close();
    141. }
    142. }
    143. public int getStatusCode() {
    144. return statusCode;
    145. }
    146. public String getContent() throws ParseException, IOException {
    147. return content;
    148. }
    149. }

    参数转换字符串工具类

    1. package com.wx.util;
    2. import javax.servlet.http.HttpServletRequest;
    3. import java.io.BufferedReader;
    4. import java.io.IOException;
    5. public class HttpUtils {
    6. /**
    7. * 将通知参数转化为字符串
    8. * @param request
    9. * @return
    10. */
    11. public static String readData(HttpServletRequest request) {
    12. BufferedReader br = null;
    13. try {
    14. StringBuilder result = new StringBuilder();
    15. br = request.getReader();
    16. for (String line; (line = br.readLine()) != null; ) {
    17. if (result.length() > 0) {
    18. result.append("\n");
    19. }
    20. result.append(line);
    21. }
    22. return result.toString();
    23. } catch (IOException e) {
    24. throw new RuntimeException(e);
    25. } finally {
    26. if (br != null) {
    27. try {
    28. br.close();
    29. } catch (IOException e) {
    30. e.printStackTrace();
    31. }
    32. }
    33. }
    34. }
    35. }

    订单号工具类:我们需要为我们的订单生成一个编号

    1. package com.wx.util;
    2. import java.text.SimpleDateFormat;
    3. import java.util.Date;
    4. import java.util.Random;
    5. /**
    6. * 订单号工具类
    7. *
    8. * @author qy
    9. * @since 1.0
    10. */
    11. public class OrderNoUtils {
    12. /**
    13. * 获取订单编号
    14. * @return
    15. */
    16. public static String getOrderNo() {
    17. return "ORDER_" + getNo();
    18. }
    19. /**
    20. * 获取退款单编号
    21. * @return
    22. */
    23. public static String getRefundNo() {
    24. return "REFUND_" + getNo();
    25. }
    26. /**
    27. * 获取编号
    28. * @return
    29. */
    30. public static String getNo() {
    31. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    32. String newDate = sdf.format(new Date());
    33. String result = "";
    34. Random random = new Random();
    35. for (int i = 0; i < 3; i++) {
    36. result += random.nextInt(10);
    37. }
    38. return newDate + result;
    39. }
    40. }

    微信验签应答工具类

    1. package com.wx.util;
    2. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
    3. import org.apache.http.HttpEntity;
    4. import org.apache.http.client.methods.CloseableHttpResponse;
    5. import org.apache.http.util.EntityUtils;
    6. import org.slf4j.Logger;
    7. import org.slf4j.LoggerFactory;
    8. import javax.servlet.http.HttpServletRequest;
    9. import java.io.IOException;
    10. import java.nio.charset.StandardCharsets;
    11. import java.time.DateTimeException;
    12. import java.time.Duration;
    13. import java.time.Instant;
    14. import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
    15. /**
    16. * @author xy-peng
    17. */
    18. public class WechatPay2ValidatorForRequest {
    19. protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
    20. /**
    21. * 应答超时时间,单位为分钟
    22. */
    23. protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    24. protected final Verifier verifier;
    25. protected final String requestId;
    26. protected final String body;
    27. public WechatPay2ValidatorForRequest(Verifier verifier, String requestId, String body) {
    28. this.verifier = verifier;
    29. this.requestId = requestId;
    30. this.body = body;
    31. }
    32. protected static IllegalArgumentException parameterError(String message, Object... args) {
    33. message = String.format(message, args);
    34. return new IllegalArgumentException("parameter error: " + message);
    35. }
    36. protected static IllegalArgumentException verifyFail(String message, Object... args) {
    37. message = String.format(message, args);
    38. return new IllegalArgumentException("signature verify fail: " + message);
    39. }
    40. public final boolean validate(HttpServletRequest request) throws IOException {
    41. try {
    42. //处理请求参数
    43. validateParameters(request);
    44. //构造验签名串
    45. String message = buildMessage(request);
    46. String serial = request.getHeader(WECHAT_PAY_SERIAL);
    47. String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
    48. //验签
    49. if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
    50. throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
    51. serial, message, signature, requestId);
    52. }
    53. } catch (IllegalArgumentException e) {
    54. log.warn(e.getMessage());
    55. return false;
    56. }
    57. return true;
    58. }
    59. protected final void validateParameters(HttpServletRequest request) {
    60. // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
    61. String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
    62. String header = null;
    63. for (String headerName : headers) {
    64. header = request.getHeader(headerName);
    65. if (header == null) {
    66. throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
    67. }
    68. }
    69. //判断请求是否过期
    70. String timestampStr = header;
    71. try {
    72. Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
    73. // 拒绝过期请求
    74. if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
    75. throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
    76. }
    77. } catch (DateTimeException | NumberFormatException e) {
    78. throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
    79. }
    80. }
    81. protected final String buildMessage(HttpServletRequest request) throws IOException {
    82. String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
    83. String nonce = request.getHeader(WECHAT_PAY_NONCE);
    84. return timestamp + "\n"
    85. + nonce + "\n"
    86. + body + "\n";
    87. }
    88. protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
    89. HttpEntity entity = response.getEntity();
    90. return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
    91. }
    92. }

    创建统一结果R类

    1. package com.wx.vo;
    2. import lombok.Data;
    3. import lombok.experimental.Accessors;
    4. import java.util.HashMap;
    5. import java.util.Map;
    6. @Data
    7. @Accessors(chain = true)
    8. public class R {
    9. private Integer code; //响应码
    10. private String message; //响应消息
    11. private Map data = new HashMap<>();
    12. public static R ok(){
    13. R r = new R();
    14. r.setCode(200);
    15. r.setMessage("成功");
    16. return r;
    17. }
    18. public static R error(){
    19. R r = new R();
    20. r.setCode(201);
    21. r.setMessage("失败");
    22. return r;
    23. }
    24. public R data(String key, Object value){
    25. this.data.put(key, value);
    26. return this;
    27. }
    28. }

    定义实体类

    1. package com.wx.entity;
    2. import com.baomidou.mybatisplus.annotation.IdType;
    3. import com.baomidou.mybatisplus.annotation.TableId;
    4. import lombok.Data;
    5. import java.util.Date;
    6. @Data
    7. public class BaseEntity {
    8. //定义主键策略:跟随数据库的主键自增
    9. @TableId(value = "id", type = IdType.AUTO)
    10. private String id; //主键
    11. private Date createTime;//创建时间
    12. private Date updateTime;//更新时间
    13. }
    1. package com.wx.entity;
    2. import com.baomidou.mybatisplus.annotation.TableName;
    3. import lombok.Data;
    4. @Data
    5. @TableName("t_order_info")
    6. public class OrderInfo extends BaseEntity{
    7. private String title;//订单标题
    8. private String orderNo;//商户订单编号
    9. private Long userId;//用户id
    10. private Long productId;//支付产品id
    11. private Integer totalFee;//订单金额(分)
    12. private String codeUrl;//订单二维码连接
    13. private String orderStatus;//订单状态
    14. }
    1. package com.wx.entity;
    2. import com.baomidou.mybatisplus.annotation.TableName;
    3. import lombok.Data;
    4. @Data
    5. @TableName("t_payment_info")
    6. public class PaymentInfo extends BaseEntity{
    7. private String orderNo;//商品订单编号
    8. private String transactionId;//支付系统交易编号
    9. private String paymentType;//支付类型
    10. private String tradeType;//交易类型
    11. private String tradeState;//交易状态
    12. private Integer payerTotal;//支付金额(分)
    13. private String content;//通知参数
    14. }
    1. package com.wx.entity;
    2. import com.baomidou.mybatisplus.annotation.TableName;
    3. import lombok.Data;
    4. @Data
    5. @TableName("t_product")
    6. public class Product extends BaseEntity{
    7. private String title; //商品名称
    8. private Integer price; //价格(分)
    9. }
    1. package com.wx.entity;
    2. import com.baomidou.mybatisplus.annotation.TableName;
    3. import lombok.Data;
    4. @Data
    5. @TableName("t_refund_info")
    6. public class RefundInfo extends BaseEntity{
    7. private String orderNo;//商品订单编号
    8. private String refundNo;//退款单编号
    9. private String refundId;//支付系统退款单号
    10. private Integer totalFee;//原订单金额(分)
    11. private Integer refund;//退款金额(分)
    12. private String reason;//退款原因
    13. private String refundStatus;//退款单状态
    14. private String contentReturn;//申请退款返回参数
    15. private String contentNotify;//退款结果通知参数
    16. }

    定义枚举

    为了开发方便,我们预先在项目中定义一些枚举。枚举中定义的内容包括接口地址,支付状态等信息

    API接口地址,封装了微信支付的所有接口

    1. package com.wx.enums.wxpay;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. /**
    5. * api接口地址
    6. */
    7. @AllArgsConstructor
    8. @Getter
    9. public enum WxApiType {
    10. /**
    11. * Native下单
    12. */
    13. NATIVE_PAY("/v3/pay/transactions/native"),
    14. /**
    15. * Native下单
    16. */
    17. NATIVE_PAY_V2("/pay/unifiedorder"),
    18. /**
    19. * 查询订单
    20. */
    21. ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),
    22. /**
    23. * 关闭订单
    24. */
    25. CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),
    26. /**
    27. * 申请退款
    28. */
    29. DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),
    30. /**
    31. * 查询单笔退款
    32. */
    33. DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),
    34. /**
    35. * 申请交易账单
    36. */
    37. TRADE_BILLS("/v3/bill/tradebill"),
    38. /**
    39. * 申请资金账单
    40. */
    41. FUND_FLOW_BILLS("/v3/bill/fundflowbill");
    42. /**
    43. * 类型
    44. */
    45. private final String type;
    46. }

    封装了通知接口地址

    1. package com.wx.enums.wxpay;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. /**
    5. * 封装了通知接口地址
    6. */
    7. @AllArgsConstructor
    8. @Getter
    9. public enum WxNotifyType {
    10. /**
    11. * 支付通知
    12. */
    13. NATIVE_NOTIFY("/api/wx-pay/native/notify"),
    14. /**
    15. * 支付通知
    16. */
    17. NATIVE_NOTIFY_V2("/api/wx-pay-v2/native/notify"),
    18. /**
    19. * 退款结果通知
    20. */
    21. REFUND_NOTIFY("/api/wx-pay/refunds/notify");
    22. /**
    23. * 类型
    24. */
    25. private final String type;
    26. }

    退款类型

    1. package com.wx.enums.wxpay;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. /**
    5. * 退款
    6. */
    7. @AllArgsConstructor
    8. @Getter
    9. public enum WxRefundStatus {
    10. /**
    11. * 退款成功
    12. */
    13. SUCCESS("SUCCESS"),
    14. /**
    15. * 退款关闭
    16. */
    17. CLOSED("CLOSED"),
    18. /**
    19. * 退款处理中
    20. */
    21. PROCESSING("PROCESSING"),
    22. /**
    23. * 退款异常
    24. */
    25. ABNORMAL("ABNORMAL");
    26. /**
    27. * 类型
    28. */
    29. private final String type;
    30. }

    支付类型

    1. package com.wx.enums.wxpay;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. /**
    5. * 支付订单状态
    6. */
    7. @AllArgsConstructor
    8. @Getter
    9. public enum WxTradeState {
    10. /**
    11. * 支付成功
    12. */
    13. SUCCESS("SUCCESS"),
    14. /**
    15. * 未支付
    16. */
    17. NOTPAY("NOTPAY"),
    18. /**
    19. * 已关闭
    20. */
    21. CLOSED("CLOSED"),
    22. /**
    23. * 转入退款
    24. */
    25. REFUND("REFUND");
    26. /**
    27. * 类型
    28. */
    29. private final String type;
    30. }

    支付状态

    1. package com.wx.enums;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. @AllArgsConstructor
    5. @Getter
    6. public enum OrderStatus {
    7. /**
    8. * 未支付
    9. */
    10. NOTPAY("未支付"),
    11. /**
    12. * 支付成功
    13. */
    14. SUCCESS("支付成功"),
    15. /**
    16. * 已关闭
    17. */
    18. CLOSED("超时已关闭"),
    19. /**
    20. * 已取消
    21. */
    22. CANCEL("用户已取消"),
    23. /**
    24. * 退款中
    25. */
    26. REFUND_PROCESSING("退款中"),
    27. /**
    28. * 已退款
    29. */
    30. REFUND_SUCCESS("已退款"),
    31. /**
    32. * 退款异常
    33. */
    34. REFUND_ABNORMAL("退款异常");
    35. /**
    36. * 类型
    37. */
    38. private final String type;
    39. }

    付款类型

    1. package com.wx.enums;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. @AllArgsConstructor
    5. @Getter
    6. public enum PayType {
    7. /**
    8. * 微信
    9. */
    10. WXPAY("微信"),
    11. /**
    12. * 支付宝
    13. */
    14. ALIPAY("支付宝");
    15. /**
    16. * 类型
    17. */
    18. private final String type;
    19. }

    定义MyBatis-Plus的配置文件

    confifig 包中创建配置文件 MybatisPlusConfifig
    1. package com.wx.config;
    2. import org.mybatis.spring.annotation.MapperScan;
    3. import org.mybatis.spring.annotation.MapperScans;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.transaction.annotation.EnableTransactionManagement;
    6. @Configuration
    7. @MapperScan("com.wx.mapper")
    8. @EnableTransactionManagement
    9. public class MybatisPlusConfig {
    10. }

    定义Mapper层

    继承BaseMapper<>

    定义业务层

    定义业务层接口继承 IService<>
    定义业务层接口的实现类,并继承 ServiceImpl<,>

    以上配置完成才可进行业务实现!!!!!!!!!!!!!!!!!!!

    业务代码实现

    获取商品列表接口

    public class ProductController 中添加一个方法
    1. package com.wx.controller;
    2. import com.wx.entity.Product;
    3. import com.wx.service.ProductService;
    4. import com.wx.vo.R;
    5. import io.swagger.annotations.Api;
    6. import io.swagger.annotations.ApiOperation;
    7. import org.springframework.web.bind.annotation.CrossOrigin;
    8. import org.springframework.web.bind.annotation.GetMapping;
    9. import org.springframework.web.bind.annotation.RequestMapping;
    10. import org.springframework.web.bind.annotation.RestController;
    11. import javax.annotation.Resource;
    12. import java.util.List;
    13. @CrossOrigin
    14. @RestController
    15. @RequestMapping("/api/product")
    16. @Api(tags = "商品管理")
    17. public class ProductController {
    18. @Resource
    19. private ProductService productService;
    20. @GetMapping("/list")
    21. public R list(){
    22. List list = productService.list();
    23. return R.ok().data("productList",list);
    24. }
    25. }

    结果:

     定义读取支付参数配置WxPayConfig

    1. package com.wx.config;
    2. import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
    3. import com.wechat.pay.contrib.apache.httpclient.auth.*;
    4. import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
    5. import lombok.Data;
    6. import lombok.extern.slf4j.Slf4j;
    7. import org.apache.http.impl.client.CloseableHttpClient;
    8. import org.springframework.boot.context.properties.ConfigurationProperties;
    9. import org.springframework.context.annotation.Bean;
    10. import org.springframework.context.annotation.Configuration;
    11. import org.springframework.context.annotation.PropertySource;
    12. import java.io.FileInputStream;
    13. import java.io.FileNotFoundException;
    14. import java.nio.charset.StandardCharsets;
    15. import java.security.PrivateKey;
    16. @Configuration
    17. @PropertySource("classpath:wxpay.properties") //读取配置文件
    18. @ConfigurationProperties(prefix="wxpay") //读取wxpay节点
    19. @Data //使用set方法将wxpay节点中的值填充到当前类的属性中
    20. @Slf4j
    21. public class WxPayConfig {
    22. // 商户号
    23. private String mchId;
    24. // 商户API证书序列号
    25. private String mchSerialNo;
    26. // 商户私钥文件
    27. private String privateKeyPath;
    28. // APIv3密钥
    29. private String apiV3Key;
    30. // APPID
    31. private String appid;
    32. // 微信服务器地址
    33. private String domain;
    34. // 接收结果通知地址
    35. private String notifyDomain;
    36. // APIv2密钥
    37. private String partnerKey;
    38. /**
    39. * 获取商户的私钥文件
    40. * @param filename
    41. * @return
    42. */
    43. private PrivateKey getPrivateKey(String filename){
    44. try {
    45. return PemUtil.loadPrivateKey(new FileInputStream(filename));
    46. } catch (FileNotFoundException e) {
    47. throw new RuntimeException("私钥文件不存在", e);
    48. }
    49. }
    50. /**
    51. * 获取签名验证器.定时更新签名证书
    52. * @return
    53. */
    54. @Bean
    55. public ScheduledUpdateCertificatesVerifier getVerifier(){
    56. log.info("获取签名验证器");
    57. //获取商户私钥
    58. PrivateKey privateKey = getPrivateKey(privateKeyPath);
    59. //私钥签名对象
    60. PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);
    61. //身份认证对象
    62. WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
    63. // 使用定时更新的签名验证器,不需要传入证书
    64. ScheduledUpdateCertificatesVerifier verifier = new ScheduledUpdateCertificatesVerifier(
    65. wechatPay2Credentials,
    66. apiV3Key.getBytes(StandardCharsets.UTF_8));//商户对称加密的秘钥
    67. return verifier;
    68. }
    69. /**
    70. * 获取http请求对象
    71. * @param verifier
    72. * @return
    73. */
    74. @Bean(name = "wxPayClient")
    75. public CloseableHttpClient getWxPayClient(ScheduledUpdateCertificatesVerifier verifier){
    76. log.info("获取httpClient");
    77. //获取商户私钥
    78. PrivateKey privateKey = getPrivateKey(privateKeyPath);
    79. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
    80. .withMerchant(mchId, mchSerialNo, privateKey)
    81. .withValidator(new WechatPay2Validator(verifier));
    82. // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
    83. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
    84. CloseableHttpClient httpClient = builder.build();
    85. return httpClient;
    86. }
    87. /**
    88. * 获取HttpClient,无需进行应答签名验证,跳过验签的流程
    89. */
    90. @Bean(name = "wxPayNoSignClient")
    91. public CloseableHttpClient getWxPayNoSignClient(){
    92. //获取商户私钥
    93. PrivateKey privateKey = getPrivateKey(privateKeyPath);
    94. //用于构造HttpClient
    95. WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
    96. //设置商户信息
    97. .withMerchant(mchId, mchSerialNo, privateKey)
    98. //无需进行签名验证、通过withValidator((response) -> true)实现
    99. .withValidator((response) -> true);
    100. // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
    101. CloseableHttpClient httpClient = builder.build();
    102. log.info("== getWxPayNoSignClient END ==");
    103. return httpClient;
    104. }
    105. }

    测试获取支付参数:

    1. import com.wx.config.WxPayConfig;
    2. import com.wx.vo.R;
    3. import io.swagger.annotations.Api;
    4. import org.springframework.web.bind.annotation.GetMapping;
    5. import org.springframework.web.bind.annotation.RequestMapping;
    6. import org.springframework.web.bind.annotation.RestController;
    7. import javax.annotation.Resource;
    8. import java.security.PrivateKey;
    9. @Api(tags = "测试控制器")
    10. @RestController
    11. @RequestMapping("/api/test")
    12. public class TestController {
    13. @Resource
    14. private WxPayConfig wxPayConfig;
    15. @GetMapping
    16. public R getWxPayConfig(){
    17. String mchId = wxPayConfig.getMchId();
    18. String privateKeyPath = wxPayConfig.getPrivateKeyPath();
    19. // PrivateKey privateKey = wxPayConfig.getPrivateKey("apiclient_key.pem");
    20. // System.out.println("privateKey = " + privateKey);
    21. return R.ok().data("mchId",mchId).data("privateKeyPath",privateKeyPath);
    22. }
    23. }

    swagger测试获取

    1.引入SDK(以下均已在前面配置完成。此处讲解)

    前面的依赖是完整的,已经添加

    https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml
    我们可以使用官方提供的 SDK ,帮助我们完成开发。实现了请求签名的生成和应答签名的验证。
    1. <dependency>
    2. <groupId>com.github.wechatpay-apiv3groupId>
    3. <artifactId>wechatpay-apache-httpclientartifactId>
    4. <version>0.3.0version>
    5. dependency>

     2.获取商户私钥

    https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (如何加载商户私钥)
    上面测试中注释的部门就是获取商户私钥

    3、获取签名验证器和HttpClient

    证书密钥使用说明:上面的配置中已经建立,此处再说明一下

    https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay3_0.shtml

     获取签名验证器

    https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
    平台证书:平台证书封装了微信的公钥,商户可以使用平台证书中的公钥进行验签。
    签名验证器:帮助我们进行验签工作,我们单独将它定义出来,方便后面的开发。
    获取 HttpClient 对象
    https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient (定时更新平台证书功能)
    HttpClient 对象:是建立远程连接的基础,我们通过 SDK 创建这个对象。

    4API字典和相关工具

    https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_7_3.shtml
    我们的项目中要实现以下所有 API 的功能。

     接口规则

    https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay2_0.shtml
    微信支付 APIv3 使用 JSON 作为消息体的数据交换格式。
    1. <dependency>
    2. <groupId>com.google.code.gsongroupId>
    3. <artifactId>gsonartifactId>
    4. dependency>

    业务功能接口代码实现

    Controller

    1. package com.wx.controller;
    2. import com.google.gson.JsonSyntaxException;
    3. import com.wx.service.WxPayService;
    4. import com.wx.util.HttpUtils;
    5. import com.wx.util.WechatPay2ValidatorForRequest;
    6. import com.wx.vo.R;
    7. import com.google.gson.Gson;
    8. import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
    9. import io.swagger.annotations.Api;
    10. import io.swagger.annotations.ApiOperation;
    11. import lombok.extern.slf4j.Slf4j;
    12. import org.springframework.web.bind.annotation.*;
    13. import javax.annotation.Resource;
    14. import javax.servlet.http.HttpServletRequest;
    15. import javax.servlet.http.HttpServletResponse;
    16. import java.io.IOException;
    17. import java.security.GeneralSecurityException;
    18. import java.util.HashMap;
    19. import java.util.Map;
    20. import java.util.concurrent.TimeUnit;
    21. @CrossOrigin
    22. @RestController
    23. @RequestMapping("/api/wx-pay")
    24. @Api(tags = "网站微信支付API")
    25. @Slf4j
    26. public class WxPayController {
    27. @Resource
    28. private WxPayService wxPayService;
    29. @Resource
    30. private Verifier verifier;
    31. @ApiOperation("调用统一下单API,生成支付二维码")
    32. @PostMapping("native/{productId}")
    33. public R nativePay(@PathVariable Long productId) throws Exception {//传递商品id
    34. log.info("发起支付请求");
    35. //返回支付二维码链接和订单号
    36. Map map = wxPayService.nativePay(productId);
    37. return R.ok().setData(map);
    38. }
    39. /**
    40. * 接收微信的通知,支付成功处理,失败处理
    41. * @param request
    42. * @param response
    43. * @return
    44. */
    45. @PostMapping("/native/notify")
    46. public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
    47. Gson gson = new Gson();
    48. Map map = new HashMap<>();//
    49. try {
    50. //处理通知参数
    51. String body = HttpUtils.readData(request);
    52. Map bodyMap = gson.fromJson(body, HashMap.class);
    53. log.info("支付通知的id =====》 {}",bodyMap.get("id"));
    54. log.info("支付通知的完整数据 =====》 {}",body);
    55. String requestId = bodyMap.get("id").toString();
    56. // 签名的验证 针对请求的 因为与微信交互,传递信息需要进行验证
    57. WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
    58. = new WechatPay2ValidatorForRequest(verifier, requestId,body);
    59. if (!wechatPay2ValidatorForRequest.validate(request)){//判断验签是否成功
    60. log.error("通知验签失败");
    61. //失败应答
    62. response.setStatus(500);
    63. map.put("code","ERROR");
    64. map.put("message","通知验签失败");
    65. return gson.toJson(map);
    66. }
    67. log.info("通知验签成功");
    68. //处理订单 将具有密文数据的bodyMap进行解密获取参数。并存入数据库,存入日志
    69. wxPayService.processOrder(bodyMap);
    70. //成功应答
    71. response.setStatus(200);
    72. map.put("code","SUCCESS");
    73. map.put("message","成功");
    74. return gson.toJson(map);
    75. } catch (JsonSyntaxException | IOException | GeneralSecurityException e) {
    76. e.printStackTrace();
    77. //失败应答
    78. response.setStatus(500);
    79. map.put("code","ERROR");
    80. map.put("message","失败");
    81. return gson.toJson(map);
    82. }
    83. }
    84. @ApiOperation("用户取消订单")
    85. @PostMapping("/cancel/{orderNo}")
    86. public R cancel(@PathVariable String orderNo) throws Exception {
    87. log.info("取消订单");
    88. wxPayService.canceOrder(orderNo);
    89. return R.ok().setMessage("订单已经取消");
    90. }
    91. @ApiOperation("微信支付查询订单")
    92. @GetMapping("/query/{orderNo}")
    93. public R queryOrder(@PathVariable String orderNo) throws IOException {
    94. log.info("查询订单");
    95. String result = wxPayService.queryOrder(orderNo);
    96. return R.ok().setMessage("查询成功").data("result",result);
    97. }
    98. @ApiOperation("申请退款")
    99. @PostMapping("/refunds/{orderNo}/{reason}")
    100. public R refunds(@PathVariable String orderNo, @PathVariable String reason) throws Exception {
    101. log.info("申请退款");
    102. wxPayService.refund(orderNo, reason);
    103. return R.ok();
    104. }
    105. /**
    106. * 查询退款
    107. * @param refundNo
    108. * @return
    109. * @throws Exception
    110. */
    111. @ApiOperation("查询退款")
    112. @GetMapping("/query-refund/{refundNo}")
    113. public R queryRefund(@PathVariable String refundNo) throws Exception {
    114. log.info("查询退款");
    115. String result = wxPayService.queryRefund(refundNo);
    116. return R.ok().setMessage("查询成功").data("result", result);
    117. }
    118. /**
    119. * 退款结果通知
    120. * 退款状态改变后,微信会把相关退款结果发送给商户。
    121. */
    122. @ApiOperation("退款结果通知")
    123. @PostMapping("/refunds/notify")
    124. public String refundsNotify(HttpServletRequest request, HttpServletResponse response){
    125. log.info("退款通知执行");
    126. Gson gson = new Gson();
    127. Map map = new HashMap<>();//应答对象
    128. try {
    129. //处理通知参数
    130. String body = HttpUtils.readData(request);
    131. Map bodyMap = gson.fromJson(body, HashMap.class);
    132. String requestId = (String)bodyMap.get("id");
    133. log.info("支付通知的id ===> {}", requestId);
    134. //签名的验证
    135. WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
    136. = new WechatPay2ValidatorForRequest(verifier, requestId, body);
    137. if(!wechatPay2ValidatorForRequest.validate(request)){
    138. log.error("通知验签失败");
    139. //失败应答
    140. response.setStatus(500);
    141. map.put("code", "ERROR");
    142. map.put("message", "通知验签失败");
    143. return gson.toJson(map);
    144. }
    145. log.info("通知验签成功");
    146. //处理退款单
    147. wxPayService.processRefund(bodyMap);
    148. //成功应答
    149. response.setStatus(200);
    150. map.put("code", "SUCCESS");
    151. map.put("message", "成功");
    152. return gson.toJson(map);
    153. } catch (Exception e) {
    154. e.printStackTrace();
    155. //失败应答
    156. response.setStatus(500);
    157. map.put("code", "ERROR");
    158. map.put("message", "失败");
    159. return gson.toJson(map);
    160. }
    161. }
    162. @ApiOperation("获取账单url")
    163. @GetMapping("/querybill/{billDate}/{type}")
    164. public R queryTradeBill(
    165. @PathVariable String billDate,
    166. @PathVariable String type) throws Exception {
    167. log.info("获取账单url");
    168. String downloadUrl = wxPayService.queryBill(billDate, type);
    169. return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
    170. }
    171. @ApiOperation("下载账单")
    172. @GetMapping("/downloadbill/{billDate}/{type}")
    173. public R downloadBill(
    174. @PathVariable String billDate,
    175. @PathVariable String type) throws Exception {
    176. log.info("下载账单");
    177. String result = wxPayService.downloadBill(billDate, type);
    178. return R.ok().data("result", result);
    179. }
    180. }
    1. package com.wx.controller;
    2. import com.wx.entity.OrderInfo;
    3. import com.wx.enums.OrderStatus;
    4. import com.wx.service.OrderInfoService;
    5. import com.wx.vo.R;
    6. import io.swagger.annotations.Api;
    7. import io.swagger.annotations.ApiOperation;
    8. import org.springframework.web.bind.annotation.*;
    9. import javax.annotation.Resource;
    10. import java.util.List;
    11. @CrossOrigin
    12. @Api(tags = "商品订单管理")
    13. @RestController
    14. @RequestMapping("/api/order-info")
    15. public class OrderInfoController {
    16. @Resource
    17. private OrderInfoService orderInfoService;
    18. @GetMapping("/list")
    19. public R list(){
    20. List list = orderInfoService.listOrderByCreateTimeDesc();
    21. return R.ok().data("list",list);
    22. }
    23. /**
    24. * 查询订单状态
    25. * @param orderNo
    26. * @return
    27. */
    28. @ApiOperation("查询订单状态")
    29. @GetMapping("/query-order-status/{orderNo}")
    30. public R queryOrderStatus(@PathVariable String orderNo){
    31. String orderStatus = orderInfoService.getOrderStatus(orderNo);
    32. if (OrderStatus.SUCCESS.getType().equals(orderStatus)){
    33. return R.ok().setCode(0).setMessage("支付成功");//支付成功
    34. }
    35. return R.ok().setCode(101).setMessage("支付中...");
    36. }
    37. }
    1. package com.wx.controller;
    2. import com.wx.entity.Product;
    3. import com.wx.service.ProductService;
    4. import com.wx.vo.R;
    5. import io.swagger.annotations.Api;
    6. import io.swagger.annotations.ApiOperation;
    7. import org.springframework.web.bind.annotation.CrossOrigin;
    8. import org.springframework.web.bind.annotation.GetMapping;
    9. import org.springframework.web.bind.annotation.RequestMapping;
    10. import org.springframework.web.bind.annotation.RestController;
    11. import javax.annotation.Resource;
    12. import java.util.List;
    13. @CrossOrigin
    14. @RestController
    15. @RequestMapping("/api/product")
    16. @Api(tags = "商品管理")
    17. public class ProductController {
    18. @Resource
    19. private ProductService productService;
    20. @GetMapping("/list")
    21. public R list(){
    22. List list = productService.list();
    23. return R.ok().data("productList",list);
    24. }
    25. }

    Service

    1. package com.wx.service;
    2. import com.baomidou.mybatisplus.extension.service.IService;
    3. import com.wx.entity.OrderInfo;
    4. import com.wx.enums.OrderStatus;
    5. import java.util.List;
    6. public interface OrderInfoService extends IService {
    7. OrderInfo createOrderByProductId(Long productId,String type);//获取订单信息并存入数据库中
    8. //订单号 二维码地址
    9. void saveCodeUrl(String orderNo,String codeUrl);//因为扫码有俩个小时的时间,所以进行数据库的更新
    10. List listOrderByCreateTimeDesc(); //查询订单列表,并倒序
    11. void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus);//更改订单状态
    12. String getOrderStatus(String orderNo);//处理重复的通知
    13. List getNopayOrderByDuration(int minutes,String type);//定时任务
    14. OrderInfo getOrderByOrderNo(String orderNo);// //根据订单号获取订单信息
    15. }
    1. package com.wx.service;
    2. public interface PaymentInfoService {
    3. void createPaymentInfo(String plainText);//记录支付日志
    4. }
    1. package com.wx.service;
    2. import com.baomidou.mybatisplus.extension.service.IService;
    3. import com.wx.entity.Product;
    4. public interface ProductService extends IService {
    5. }
    1. package com.wx.service;
    2. import com.baomidou.mybatisplus.extension.service.IService;
    3. import com.wx.entity.RefundInfo;
    4. import com.wx.enums.PayType;
    5. import java.util.List;
    6. public interface RefundInfoService extends IService {
    7. RefundInfo createRefundByOrderNo(String orderNo, String reason);//根据订单编号创建退款
    8. void updateRefund(String content);//更新退款单
    9. List getNoRefundOrderByDuration(int minutes, String type);//找出申请退款超过5分钟并且未成功的退款单
    10. RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason);
    11. void updateRefundForAliPay(String refundNo, String content, String refundStatus);
    12. }
    1. package com.wx.service;
    2. import java.io.IOException;
    3. import java.security.GeneralSecurityException;
    4. import java.util.Map;
    5. public interface WxPayService {
    6. Map nativePay(Long productId) throws Exception;
    7. //解密
    8. void processOrder(Map bodyMap) throws GeneralSecurityException;
    9. void canceOrder(String orderNo) throws Exception;//取消订单
    10. String queryOrder(String orderNo) throws IOException;//微信支付查询订单
    11. void checkOrderStatus(String orderNo) throws Exception;//查询核实订单状态
    12. void refund(String orderNo, String reason) throws Exception;//申请退款
    13. String queryRefund(String refundNo) throws Exception;//查询退款
    14. void processRefund(Map bodyMap) throws Exception;//退款结果通知
    15. void checkRefundStatus(String refundNo) throws Exception;//核实订单状态:调用微信支付查询退款接口
    16. String queryBill(String billDate, String type) throws Exception;//获取账单url
    17. String downloadBill(String billDate, String type) throws Exception;//下载账单
    18. }

    ServiceImpl

    1. package com.wx.service.impl;
    2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    4. import com.wx.entity.OrderInfo;
    5. import com.wx.entity.Product;
    6. import com.wx.enums.OrderStatus;
    7. import com.wx.mapper.OrderInfoMapper;
    8. import com.wx.mapper.ProductMapper;
    9. import com.wx.service.OrderInfoService;
    10. import com.wx.util.OrderNoUtils;
    11. import lombok.extern.slf4j.Slf4j;
    12. import org.springframework.stereotype.Service;
    13. import javax.annotation.Resource;
    14. import java.time.Duration;
    15. import java.time.Instant;
    16. import java.util.List;
    17. @Slf4j
    18. @Service
    19. public class OrderInfoServiceImpl extends ServiceImpl implements OrderInfoService {
    20. @Resource
    21. private ProductMapper productMapper;
    22. @Resource
    23. private OrderInfoMapper orderInfoMapper;
    24. /**
    25. * 生成订单到数据库中
    26. * @param productId
    27. * @return
    28. */
    29. @Override
    30. public OrderInfo createOrderByProductId(Long productId,String type) {
    31. //查找已存在但未支付的订单
    32. OrderInfo orderInfo = this.getNoPayOrderByProductId(productId,type);
    33. if (orderInfo != null){
    34. return orderInfo;
    35. }
    36. //获取商品信息
    37. Product product = productMapper.selectById(productId);
    38. //生成订单
    39. orderInfo = new OrderInfo();
    40. orderInfo.setTitle(product.getTitle());
    41. orderInfo.setOrderNo(OrderNoUtils.getOrderNo());//设置订单号
    42. orderInfo.setProductId(productId);
    43. orderInfo.setTotalFee(product.getPrice()); //设置订单金额类型 分
    44. orderInfo.setOrderStatus(OrderStatus.NOTPAY.getType()); //订单状态
    45. // orderInfo.setUserId(); 真实项目中需要存入用户的id,谁下的订单
    46. // orderInfo.setCodeUrl(); //二维码链接
    47. orderInfo.setPaymentType(type);
    48. baseMapper.insert(orderInfo);
    49. return orderInfo;
    50. }
    51. //查找已存在但未支付的订单.如果订单存在且没有支付则返回没支付的订单,防止重复创建订单对象
    52. private OrderInfo getNoPayOrderByProductId(Long productId,String type) {
    53. QueryWrapper queryWrapper = new QueryWrapper<>();
    54. queryWrapper.eq("product_id",productId);
    55. queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
    56. queryWrapper.eq("payment_type",type);
    57. // queryWrapper.eq("user_id",userId); //再根据用户的id获取用户的订单
    58. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
    59. return orderInfo;
    60. }
    61. /**
    62. * 存储订单二维码,在数据库中直接更改
    63. * @param orderNo
    64. * @param codeUrl
    65. */
    66. @Override //订单号 二维码
    67. public void saveCodeUrl(String orderNo, String codeUrl) {
    68. QueryWrapper queryWrapper = new QueryWrapper<>();
    69. queryWrapper.eq("order_no",orderNo);
    70. OrderInfo orderInfo = new OrderInfo();
    71. orderInfo.setCodeUrl(codeUrl);
    72. baseMapper.update(orderInfo,queryWrapper);
    73. }
    74. /**
    75. * 查询订单列表,并倒序查询
    76. * @return
    77. */
    78. @Override
    79. public List listOrderByCreateTimeDesc() { //订单管理
    80. QueryWrapper queryWrapper = new QueryWrapper<>();
    81. queryWrapper.orderByDesc("create_time");
    82. return baseMapper.selectList(queryWrapper);
    83. }
    84. /**
    85. * 根据订单号更新订单状态
    86. * @param orderNo
    87. * @param orderStatus
    88. */
    89. @Override
    90. public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus) {
    91. log.info("更新订单状态 ===》"+orderStatus.getType());
    92. QueryWrapper queryWrapper = new QueryWrapper<>();
    93. queryWrapper.eq("order_no", orderNo);
    94. OrderInfo orderInfo = new OrderInfo();
    95. orderInfo.setOrderStatus(orderStatus.getType());
    96. baseMapper.update(orderInfo,queryWrapper);
    97. }
    98. /**
    99. * 处理未支付的订单
    100. * @param orderNo
    101. * @return
    102. */
    103. @Override
    104. public String getOrderStatus(String orderNo) {
    105. QueryWrapper queryWrapper = new QueryWrapper<>();
    106. queryWrapper.eq("order_no", orderNo);
    107. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
    108. if (orderInfo == null){
    109. return null;
    110. }
    111. return orderInfo.getOrderStatus();
    112. }
    113. /**
    114. * 查询创建超过minutes分钟并且未支付的订单
    115. * @param minutes
    116. * @return
    117. */
    118. @Override
    119. public List getNopayOrderByDuration(int minutes,String type) {
    120. Instant instant = Instant.now().minus(Duration.ofMillis(minutes));//时间实例,用当前时间减去输入的时间
    121. QueryWrapper queryWrapper = new QueryWrapper<>();
    122. queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());
    123. queryWrapper.le("create_time",instant);//小于
    124. queryWrapper.eq("payment_type",type);//根据支付的类型查询 微信or支付宝
    125. List orderInfos = baseMapper.selectList(queryWrapper);
    126. return orderInfos;
    127. }
    128. /**
    129. * 根据订单号获取订单
    130. * @param orderNo
    131. * @return
    132. */
    133. @Override
    134. public OrderInfo getOrderByOrderNo(String orderNo) {
    135. QueryWrapper queryWrapper = new QueryWrapper<>();
    136. queryWrapper.eq("order_no", orderNo);
    137. OrderInfo orderInfo = baseMapper.selectOne(queryWrapper);
    138. return orderInfo;
    139. }
    140. }
    1. package com.wx.service.impl;
    2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    3. import com.google.gson.Gson;
    4. import com.wx.entity.PaymentInfo;
    5. import com.wx.enums.PayType;
    6. import com.wx.mapper.PaymentInfoMapper;
    7. import com.wx.service.PaymentInfoService;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.stereotype.Service;
    10. import java.math.BigDecimal;
    11. import java.util.HashMap;
    12. import java.util.Map;
    13. @Service
    14. @Slf4j
    15. public class PaymentInfoServiceImpl extends ServiceImpl implements PaymentInfoService {
    16. @Override
    17. public void createPaymentInfo(String plainText) {
    18. log.info("记录微信支付日志");
    19. Gson gson = new Gson();
    20. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
    21. //获取商户的订单号
    22. String orderNo = plainTextMap.get("out_trade_no").toString();
    23. //微信支付单号 如果支付有问题可以通过支付单号进行处理
    24. String transactionId = plainTextMap.get("transaction_id").toString();
    25. //支付类型,因为可能是通过网页,或者app及其他进行支付
    26. String tradeType = plainTextMap.get("trade_type").toString();
    27. //支付状态
    28. String tradeState = plainTextMap.get("trade_state").toString();
    29. //用户支付的金额
    30. Map amount = (Map) plainTextMap.get("amount");
    31. Integer payerTotal = ((Double) amount.get("payer_total")).intValue();
    32. PaymentInfo paymentInfo = new PaymentInfo();
    33. paymentInfo.setOrderNo(orderNo);
    34. paymentInfo.setPaymentType(PayType.WXPAY.getType());
    35. paymentInfo.setTransactionId(transactionId);
    36. paymentInfo.setTradeType(tradeType);
    37. paymentInfo.setTradeState(tradeState);
    38. paymentInfo.setPayerTotal(payerTotal);
    39. paymentInfo.setContent(plainText);
    40. baseMapper.insert(paymentInfo);//插入数据库中
    41. }
    42. /**
    43. * 记录支付宝日志
    44. * @param params
    45. */
    46. @Override
    47. public void createPaymentInfoForAlipay(Map params) {
    48. String orderNo = params.get("out_trade_no");//获取订单号
    49. String tradeNo = params.get("trade_no"); //业务编号
    50. String tradeStatus = params.get("trade_status");//交易状态
    51. String totalAmount = params.get("total_amount");
    52. int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();//交易金额
    53. PaymentInfo paymentInfo = new PaymentInfo();
    54. paymentInfo.setOrderNo(orderNo);
    55. paymentInfo.setPaymentType(PayType.ALIPAY.getType());
    56. paymentInfo.setTransactionId(tradeNo);
    57. paymentInfo.setTradeType("电脑网站支付");
    58. paymentInfo.setTradeState(tradeStatus);
    59. paymentInfo.setPayerTotal(totalAmoutInt);
    60. Gson gson = new Gson();
    61. String json = gson.toJson(params, HashMap.class);
    62. paymentInfo.setContent(json);
    63. baseMapper.insert(paymentInfo);//存储到日志表格中
    64. }
    65. }
    1. package com.wx.service.impl;
    2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    3. import com.wx.entity.Product;
    4. import com.wx.mapper.ProductMapper;
    5. import com.wx.service.ProductService;
    6. import org.springframework.stereotype.Service;
    7. @Service
    8. public class ProductServiceImpl extends ServiceImpl implements ProductService {
    9. }
    1. package com.wx.service.impl;
    2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    4. import com.google.gson.Gson;
    5. import com.wx.entity.OrderInfo;
    6. import com.wx.entity.RefundInfo;
    7. import com.wx.enums.PayType;
    8. import com.wx.enums.wxpay.WxRefundStatus;
    9. import com.wx.mapper.RefundInfoMapper;
    10. import com.wx.service.OrderInfoService;
    11. import com.wx.service.RefundInfoService;
    12. import com.wx.util.OrderNoUtils;
    13. import org.springframework.stereotype.Service;
    14. import javax.annotation.Resource;
    15. import java.time.Duration;
    16. import java.time.Instant;
    17. import java.util.HashMap;
    18. import java.util.List;
    19. import java.util.Map;
    20. @Service
    21. public class RefundInfoServiceImpl extends ServiceImpl implements RefundInfoService {
    22. @Resource
    23. private OrderInfoService orderInfoService;
    24. /**
    25. * 根据订单号创建退款订单
    26. * @param orderNo
    27. * @return
    28. */
    29. @Override
    30. public RefundInfo createRefundByOrderNo(String orderNo, String reason) {
    31. //根据订单号获取订单信息
    32. OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
    33. //根据订单号生成退款订单
    34. RefundInfo refundInfo = new RefundInfo();
    35. refundInfo.setOrderNo(orderNo);//订单编号
    36. refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
    37. refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
    38. refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
    39. refundInfo.setReason(reason);//退款原因
    40. //保存退款订单
    41. baseMapper.insert(refundInfo);
    42. return refundInfo;
    43. }
    44. /**
    45. * 记录退款记录
    46. * @param content
    47. */
    48. @Override
    49. public void updateRefund(String content) {
    50. //将json字符串转换成Map
    51. Gson gson = new Gson();
    52. Map resultMap = gson.fromJson(content, HashMap.class);
    53. //根据退款单编号修改退款单
    54. QueryWrapper queryWrapper = new QueryWrapper<>();
    55. queryWrapper.eq("refund_no", resultMap.get("out_refund_no"));
    56. //设置要修改的字段
    57. RefundInfo refundInfo = new RefundInfo();
    58. refundInfo.setRefundId(resultMap.get("refund_id"));//微信支付退款单号
    59. //查询退款和申请退款中的返回参数
    60. if(resultMap.get("status") != null){
    61. refundInfo.setRefundStatus(resultMap.get("status"));//退款状态
    62. refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
    63. }
    64. //退款回调中的回调参数
    65. if(resultMap.get("refund_status") != null){
    66. refundInfo.setRefundStatus(resultMap.get("refund_status"));//退款状态
    67. refundInfo.setContentNotify(content);//将全部响应结果存入数据库的content字段
    68. }
    69. //更新退款单
    70. baseMapper.update(refundInfo, queryWrapper);
    71. }
    72. /**
    73. * 找出申请退款超过minutes分钟并且未成功的退款单
    74. * @param minutes
    75. * @return
    76. */
    77. @Override
    78. public List getNoRefundOrderByDuration(int minutes,String tpye) {
    79. //minutes分钟之前的时间
    80. Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));
    81. QueryWrapper queryWrapper = new QueryWrapper<>();
    82. queryWrapper.eq("refund_status", WxRefundStatus.PROCESSING.getType());
    83. queryWrapper.eq("payment_type", PayType.WXPAY.getType());
    84. queryWrapper.le("create_time", instant);
    85. List refundInfoList = baseMapper.selectList(queryWrapper);
    86. return refundInfoList;
    87. }
    88. /**
    89. * 根据订单号创建退款订单
    90. * @param orderNo
    91. * @return
    92. */
    93. @Override
    94. public RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason) {
    95. //根据订单号获取订单信息
    96. OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
    97. //根据订单号生成退款订单
    98. RefundInfo refundInfo = new RefundInfo();
    99. refundInfo.setOrderNo(orderNo);//订单编号
    100. refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款单编号
    101. refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额(分)
    102. refundInfo.setRefund(orderInfo.getTotalFee());//退款金额(分)
    103. refundInfo.setReason(reason);//退款原因
    104. //保存退款订单
    105. baseMapper.insert(refundInfo);
    106. return refundInfo;
    107. }
    108. /**
    109. * 更新退款记录
    110. * @param refundNo
    111. * @param content
    112. * @param refundStatus
    113. */
    114. @Override
    115. public void updateRefundForAliPay(String refundNo, String content, String refundStatus) {
    116. //根据退款单编号修改退款单
    117. QueryWrapper queryWrapper = new QueryWrapper<>();
    118. queryWrapper.eq("refund_no", refundNo);
    119. //设置要修改的字段
    120. RefundInfo refundInfo = new RefundInfo();
    121. refundInfo.setRefundStatus(refundStatus);//退款状态
    122. refundInfo.setContentReturn(content);//将全部响应结果存入数据库的content字段
    123. //更新退款单
    124. baseMapper.update(refundInfo, queryWrapper);
    125. }
    126. }
    1. package com.wx.service.impl;
    2. import com.google.gson.Gson;
    3. import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
    4. import com.wx.config.WxPayConfig;
    5. import com.wx.entity.OrderInfo;
    6. import com.wx.entity.RefundInfo;
    7. import com.wx.enums.OrderStatus;
    8. import com.wx.enums.PayType;
    9. import com.wx.enums.wxpay.WxApiType;
    10. import com.wx.enums.wxpay.WxNotifyType;
    11. import com.wx.enums.wxpay.WxRefundStatus;
    12. import com.wx.enums.wxpay.WxTradeState;
    13. import com.wx.service.OrderInfoService;
    14. import com.wx.service.PaymentInfoService;
    15. import com.wx.service.RefundInfoService;
    16. import com.wx.service.WxPayService;
    17. import com.wx.util.OrderNoUtils;
    18. import lombok.extern.slf4j.Slf4j;
    19. import org.apache.http.client.methods.CloseableHttpResponse;
    20. import org.apache.http.client.methods.HttpGet;
    21. import org.apache.http.client.methods.HttpPost;
    22. import org.apache.http.entity.StringEntity;
    23. import org.apache.http.impl.client.CloseableHttpClient;
    24. import org.apache.http.util.EntityUtils;
    25. import org.springframework.stereotype.Service;
    26. import org.springframework.transaction.annotation.Transactional;
    27. import org.springframework.util.StringUtils;
    28. import javax.annotation.Resource;
    29. import java.io.IOException;
    30. import java.nio.charset.StandardCharsets;
    31. import java.security.GeneralSecurityException;
    32. import java.util.HashMap;
    33. import java.util.Map;
    34. import java.util.concurrent.locks.ReentrantLock;
    35. @Service
    36. @Slf4j
    37. public class WxPayServiceImpl implements WxPayService {
    38. @Resource //注入配置中的对象 保函了验签的过程
    39. private CloseableHttpClient wxPayClient;
    40. @Resource
    41. private WxPayConfig wxPayConfig;
    42. @Resource
    43. private OrderInfoService orderInfoService; //将订单存入数据库中
    44. @Resource
    45. private PaymentInfoService paymentInfoService;
    46. @Resource
    47. private RefundInfoService refundsInfoService;
    48. @Resource
    49. private CloseableHttpClient wxPayNoSignClient; //无需应答签名
    50. private final ReentrantLock lock = new ReentrantLock(); //可重入锁
    51. /**
    52. * 创建订单,调用Native支付接口
    53. * @param productId
    54. * @return code_url(二维码地址) 和 订单号
    55. * @throws Exception
    56. */
    57. @Override
    58. public Map nativePay(Long productId) throws Exception {
    59. /**
    60. * 如果调用不成功需要去商户平台登录该商户号,在产品中心-我的产品-开通“公众号支付”,这样就可以用于小程序支付了 。
    61. */
    62. log.info("生成订单");
    63. //生成订单 TODO : 存入数据库
    64. OrderInfo orderInfo= orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
    65. String codeUrl = orderInfo.getCodeUrl();
    66. if (orderInfo != null && codeUrl!=null){
    67. log.info("订单已保存,二维码已经存在");
    68. //如果第一次创建订单则不会进入,因为数据库没有相应的二维码数据
    69. //如果第二次调用则有数据,就直接进行返回数据库存储的数据
    70. //返回二维码
    71. Map map = new HashMap<>();
    72. map.put("codeUrl",codeUrl);
    73. map.put("orderNo",orderInfo.getOrderNo());
    74. return map;
    75. }
    76. log.info("调用统一下单API");
    77. //调用统一下单API
    78. HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));//放入远程链接地址
    79. // 请求body参数
    80. Gson gson = new Gson();
    81. Map paramsMap = new HashMap<>();
    82. paramsMap.put("appid",wxPayConfig.getAppid());//应用ID
    83. paramsMap.put("mchid",wxPayConfig.getMchId());//商户号
    84. paramsMap.put("description",orderInfo.getTitle());//商品描述 用了上面的title
    85. paramsMap.put("out_trade_no",orderInfo.getOrderNo());//订单号
    86. paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//通知地址
    87. Map amountMap = new HashMap<>();
    88. amountMap.put("total",orderInfo.getTotalFee());//金额
    89. amountMap.put("currency","CNY"); //货币类型
    90. paramsMap.put("amount",amountMap);
    91. String jsonParams = gson.toJson(paramsMap);//转换成json的格式
    92. log.info("请求参数:"+jsonParams);
    93. StringEntity entity = new StringEntity(jsonParams,"utf-8");
    94. entity.setContentType("application/json");
    95. httpPost.setEntity(entity);
    96. httpPost.setHeader("Accept", "application/json");
    97. //完成签名并执行请求
    98. CloseableHttpResponse response = wxPayClient.execute(httpPost);
    99. try {
    100. String bodyAsString = EntityUtils.toString(response.getEntity());//响应头
    101. int statusCode = response.getStatusLine().getStatusCode();//响应状态
    102. if (statusCode == 200) { //处理成功
    103. log.info("成功 = " + bodyAsString);
    104. } else if (statusCode == 204) { //处理成功,无返回Body
    105. System.out.println("成功");
    106. } else {
    107. System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
    108. throw new IOException("request failed");
    109. }
    110. //响应结果
    111. HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);
    112. //二维码
    113. codeUrl = resultMap.get("code_url");
    114. System.out.println("resultMap = " + resultMap);
    115. //保存新二维码
    116. String orderNo = orderInfo.getOrderNo();//订单号
    117. orderInfoService.saveCodeUrl(orderNo,codeUrl);
    118. Map map = new HashMap<>();
    119. map.put("codeUrl",codeUrl);
    120. map.put("orderNo",orderInfo.getOrderNo());
    121. return map;
    122. } finally {
    123. response.close();
    124. }
    125. }
    126. @Override
    127. public void processOrder(Map bodyMap) throws GeneralSecurityException {
    128. log.info("处理订单");
    129. //解密报文
    130. String plainText = decryptFromResource(bodyMap);
    131. //将明文转换成map
    132. Gson gson = new Gson();
    133. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
    134. String orderNo = plainTextMap.get("out_trade_no").toString();//获取商户订单号
    135. /**
    136. *在对业务数据进行状态检查和处理之前
    137. * 要采用数据锁进行并发控制
    138. * 以避免函数重入造成的数据混乱
    139. */
    140. //尝试获取锁,成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
    141. if (lock.tryLock()){
    142. try {
    143. //处理重复的通知 因为微信通知可能会出现重复的原因,所以进行处理一下
    144. String orderStatus = orderInfoService.getOrderStatus(orderNo);
    145. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)){//如果支付状态不等于未支付的
    146. return;
    147. }
    148. //更新订单状态,支付成功更改状态
    149. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
    150. //记录支付日志
    151. paymentInfoService.createPaymentInfo(plainText);
    152. } finally {
    153. //需要主动释放锁
    154. lock.unlock();
    155. }
    156. }
    157. }
    158. /**
    159. * 取消订单
    160. * @param orderNo
    161. */
    162. @Override
    163. public void canceOrder(String orderNo) throws Exception {
    164. //调用微信支付的关单接口
    165. this.closeOrder(orderNo);
    166. //更新商户端的订单状态
    167. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
    168. }
    169. /**
    170. * 微信支付查询订单
    171. * @param orderNo
    172. * @return
    173. */
    174. @Override
    175. public String queryOrder(String orderNo) throws IOException {
    176. log.info("查询订单接口调用 ===》");
    177. //因为路径中有占位符,所以进行替换
    178. String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(),orderNo);
    179. url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
    180. HttpGet httpGet = new HttpGet(url);
    181. httpGet.setHeader("Accept", "application/json");
    182. //完成签名并执行请求
    183. CloseableHttpResponse response = wxPayClient.execute(httpGet);
    184. try {
    185. String bodyAsString = EntityUtils.toString(response.getEntity());//响应头
    186. int statusCode = response.getStatusLine().getStatusCode();//响应状态
    187. if (statusCode == 200) { //处理成功
    188. log.info("成功 = " + bodyAsString);
    189. } else if (statusCode == 204) { //处理成功,无返回Body
    190. System.out.println("成功");
    191. } else {
    192. System.out.println("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
    193. throw new IOException("request failed");
    194. }
    195. return bodyAsString;
    196. } finally {
    197. response.close();
    198. }
    199. }
    200. /**
    201. * 根据订单号查询微信支付查单接口,核实订单状态
    202. * 如果订单已经支付,则更新商户端订单状态
    203. * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
    204. * @param orderNo
    205. */
    206. @Override
    207. public void checkOrderStatus(String orderNo) throws Exception {
    208. log.warn("根据订单号核实订单状态 ===》"+orderNo);
    209. //调用微信支付查单接口
    210. String result = this.queryOrder(orderNo);
    211. Gson gson = new Gson();
    212. HashMap resultMap = gson.fromJson(result, HashMap.class);
    213. //获取微信支付端的订单状态
    214. Object tradeState = resultMap.get("trade_state");
    215. //判断订单状态
    216. if (WxTradeState.SUCCESS.getType().equals(tradeState)){
    217. log.warn("核实订单已支付 === 》"+orderNo);
    218. //如果订单已经支付则更新订单状态
    219. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
    220. //记录支付日志
    221. paymentInfoService.createPaymentInfo(result);
    222. }
    223. if (WxTradeState.NOTPAY.getType().equals(tradeState)){
    224. log.warn("核实订单未支付 === 》"+orderNo);
    225. //如果订单未支付,则调用关闭订单接口
    226. this.closeOrder(orderNo);
    227. //更新本地订单状态 不用记录日志
    228. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
    229. }
    230. }
    231. /**
    232. * 退款
    233. * @param orderNo
    234. * @param reason
    235. * @throws IOException
    236. */
    237. @Override
    238. public void refund(String orderNo, String reason) throws Exception {
    239. log.info("创建退款单记录");
    240. //根据订单编号创建退款单
    241. RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);
    242. log.info("调用退款API");
    243. //调用统一下单API
    244. String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
    245. HttpPost httpPost = new HttpPost(url);
    246. // 请求body参数
    247. Gson gson = new Gson();
    248. Map paramsMap = new HashMap();
    249. paramsMap.put("out_trade_no", orderNo);//订单编号
    250. paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
    251. paramsMap.put("reason",reason);//退款原因
    252. paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址
    253. Map amountMap = new HashMap();
    254. amountMap.put("refund", refundsInfo.getRefund());//退款金额
    255. amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
    256. amountMap.put("currency", "CNY");//退款币种
    257. paramsMap.put("amount", amountMap);
    258. //将参数转换成json字符串
    259. String jsonParams = gson.toJson(paramsMap);
    260. log.info("请求参数 ===> {}" + jsonParams);
    261. StringEntity entity = new StringEntity(jsonParams,"utf-8");
    262. entity.setContentType("application/json");//设置请求报文格式
    263. httpPost.setEntity(entity);//将请求报文放入请求对象
    264. httpPost.setHeader("Accept", "application/json");//设置响应报文格式
    265. //完成签名并执行请求,并完成验签
    266. CloseableHttpResponse response = wxPayClient.execute(httpPost);
    267. try {
    268. //解析响应结果
    269. String bodyAsString = EntityUtils.toString(response.getEntity());
    270. int statusCode = response.getStatusLine().getStatusCode();
    271. if (statusCode == 200) {
    272. log.info("成功, 退款返回结果 = " + bodyAsString);
    273. } else if (statusCode == 204) {
    274. log.info("成功");
    275. } else {
    276. throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
    277. }
    278. //更新订单状态
    279. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
    280. //更新退款单
    281. refundsInfoService.updateRefund(bodyAsString);
    282. } finally {
    283. response.close();
    284. }
    285. }
    286. /**
    287. * 查询退款接口调用
    288. * @param refundNo
    289. * @return
    290. */
    291. @Override
    292. public String queryRefund(String refundNo) throws Exception {
    293. log.info("查询退款接口调用 ===> {}", refundNo);
    294. String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
    295. url = wxPayConfig.getDomain().concat(url);
    296. //创建远程Get 请求对象
    297. HttpGet httpGet = new HttpGet(url);
    298. httpGet.setHeader("Accept", "application/json");
    299. //完成签名并执行请求
    300. CloseableHttpResponse response = wxPayClient.execute(httpGet);
    301. try {
    302. String bodyAsString = EntityUtils.toString(response.getEntity());
    303. int statusCode = response.getStatusLine().getStatusCode();
    304. if (statusCode == 200) {
    305. log.info("成功, 查询退款返回结果 = " + bodyAsString);
    306. } else if (statusCode == 204) {
    307. log.info("成功");
    308. } else {
    309. throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
    310. }
    311. return bodyAsString;
    312. } finally {
    313. response.close();
    314. }
    315. }
    316. /**
    317. * 根据退款单号核实退款单状态
    318. * @param refundNo
    319. * @return
    320. */
    321. @Override
    322. public void checkRefundStatus(String refundNo) throws Exception {
    323. log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);
    324. //调用查询退款单接口
    325. String result = this.queryRefund(refundNo);
    326. //组装json请求体字符串
    327. Gson gson = new Gson();
    328. Map resultMap = gson.fromJson(result, HashMap.class);
    329. //获取微信支付端退款状态
    330. String status = resultMap.get("status");
    331. String orderNo = resultMap.get("out_trade_no");
    332. if (WxRefundStatus.SUCCESS.getType().equals(status)) {
    333. log.warn("核实订单已退款成功 ===> {}", refundNo);
    334. //如果确认退款成功,则更新订单状态
    335. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
    336. //更新退款单
    337. refundsInfoService.updateRefund(result);
    338. }
    339. if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
    340. log.warn("核实订单退款异常 ===> {}", refundNo);
    341. //如果确认退款成功,则更新订单状态
    342. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
    343. //更新退款单
    344. refundsInfoService.updateRefund(result);
    345. }
    346. }
    347. /**
    348. * 申请账单
    349. * @param billDate
    350. * @param type
    351. * @return
    352. * @throws Exception
    353. */
    354. @Override
    355. public String queryBill(String billDate, String type) throws Exception {
    356. log.warn("申请账单接口调用 {}", billDate);
    357. String url = "";
    358. if("tradebill".equals(type)){
    359. url = WxApiType.TRADE_BILLS.getType();
    360. }else if("fundflowbill".equals(type)){
    361. url = WxApiType.FUND_FLOW_BILLS.getType();
    362. }else{
    363. throw new RuntimeException("不支持的账单类型");
    364. }
    365. url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);
    366. //创建远程Get 请求对象
    367. HttpGet httpGet = new HttpGet(url);
    368. httpGet.addHeader("Accept", "application/json");
    369. //使用wxPayClient发送请求得到响应
    370. CloseableHttpResponse response = wxPayClient.execute(httpGet);
    371. try {
    372. String bodyAsString = EntityUtils.toString(response.getEntity());
    373. int statusCode = response.getStatusLine().getStatusCode();
    374. if (statusCode == 200) {
    375. log.info("成功, 申请账单返回结果 = " + bodyAsString);
    376. } else if (statusCode == 204) {
    377. log.info("成功");
    378. } else {
    379. throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
    380. }
    381. //获取账单下载地址
    382. Gson gson = new Gson();
    383. Map resultMap = gson.fromJson(bodyAsString, HashMap.class);
    384. return resultMap.get("download_url");
    385. } finally {
    386. response.close();
    387. }
    388. }
    389. /**
    390. * 下载账单
    391. * @param billDate
    392. * @param type
    393. * @return
    394. * @throws Exception
    395. */
    396. @Override
    397. public String downloadBill(String billDate, String type) throws Exception {
    398. log.warn("下载账单接口调用 {}, {}", billDate, type);
    399. //获取账单url地址
    400. String downloadUrl = this.queryBill(billDate, type);
    401. //创建远程Get 请求对象
    402. HttpGet httpGet = new HttpGet(downloadUrl);
    403. httpGet.addHeader("Accept", "application/json");
    404. //使用wxPayClient发送请求得到响应
    405. CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
    406. try {
    407. String bodyAsString = EntityUtils.toString(response.getEntity());
    408. int statusCode = response.getStatusLine().getStatusCode();
    409. if (statusCode == 200) {
    410. log.info("成功, 下载账单返回结果 = " + bodyAsString);
    411. } else if (statusCode == 204) {
    412. log.info("成功");
    413. } else {
    414. throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
    415. }
    416. return bodyAsString;
    417. } finally {
    418. response.close();
    419. }
    420. }
    421. /**
    422. * 处理退款单 退款通知
    423. */
    424. @Override
    425. public void processRefund(Map bodyMap) throws Exception {
    426. log.info("退款单");
    427. //解密报文
    428. String plainText = decryptFromResource(bodyMap);
    429. //将明文转换成map
    430. Gson gson = new Gson();
    431. HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
    432. String orderNo = (String)plainTextMap.get("out_trade_no");//获取订单信息
    433. if(lock.tryLock()){
    434. try {
    435. String orderStatus = orderInfoService.getOrderStatus(orderNo);
    436. //判断订单是否接收订单回调
    437. if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
    438. return;
    439. }
    440. //如果当前订单是正在退款的状态下
    441. //更新订单状态
    442. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
    443. //更新退款单
    444. refundsInfoService.updateRefund(plainText);
    445. } finally {
    446. //要主动释放锁
    447. lock.unlock();
    448. }
    449. }
    450. }
    451. /**
    452. * 关单接口的调用
    453. * @param orderNo
    454. */
    455. private void closeOrder(String orderNo) throws Exception {
    456. log.info("关单接口的调用,订单号 ===》{}",orderNo);
    457. //创建远程关闭订单地址
    458. String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);//地址中含有占位符 所以进行替换掉
    459. url = wxPayConfig.getDomain().concat(url);
    460. HttpPost httpPost = new HttpPost(url);
    461. //组装json请求体
    462. Gson gson = new Gson();
    463. Map paramMap = new HashMap<>();
    464. paramMap.put("mchid",wxPayConfig.getMchId());
    465. String jsonParams = gson.toJson(paramMap);
    466. log.info("请求参数 === 》 {}",jsonParams);
    467. //将请求参数设置到请求对象中
    468. StringEntity entity = new StringEntity(jsonParams,"utf-8");
    469. entity.setContentType("application/json");
    470. httpPost.setEntity(entity);
    471. httpPost.setHeader("Accept", "application/json");
    472. CloseableHttpResponse response = wxPayClient.execute(httpPost);
    473. try {
    474. int statusCode = response.getStatusLine().getStatusCode();//响应状态
    475. if (statusCode == 200) { //处理成功
    476. log.info("成功200" );
    477. } else if (statusCode == 204) { //处理成功,无返回Body
    478. System.out.println("成功204");
    479. } else {
    480. System.out.println("Native下单失败,响应码 = " + statusCode);
    481. throw new IOException("request failed");
    482. }
    483. } finally {
    484. response.close();
    485. }
    486. }
    487. /**
    488. * 对称解密
    489. * @param bodyMap
    490. * @return
    491. */
    492. private String decryptFromResource(Map bodyMap) throws GeneralSecurityException {
    493. log.info("密文解密");
    494. //通知数据
    495. Map resourceMap = (Map)bodyMap.get("resource");
    496. //获取数据中的密文
    497. String ciphertext = resourceMap.get("ciphertext");
    498. //随机串
    499. String nonce = resourceMap.get("nonce");
    500. //附加数据
    501. String associatedData = resourceMap.get("associated_data");
    502. //解密工具
    503. AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
    504. String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8)
    505. , nonce.getBytes(StandardCharsets.UTF_8)
    506. , ciphertext);
    507. log.info("明文===》{}",plainText);
    508. return plainText;
    509. }
    510. }

    创建定时任务进行定时查询订单状态

    1. package com.wx.task;
    2. import com.wx.entity.OrderInfo;
    3. import com.wx.entity.RefundInfo;
    4. import com.wx.enums.PayType;
    5. import com.wx.service.OrderInfoService;
    6. import com.wx.service.RefundInfoService;
    7. import com.wx.service.WxPayService;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.scheduling.annotation.Scheduled;
    10. import org.springframework.stereotype.Component;
    11. import javax.annotation.Resource;
    12. import java.io.IOException;
    13. import java.util.List;
    14. @Slf4j
    15. @Component
    16. public class WxPayTask {
    17. @Resource
    18. private OrderInfoService orderInfoService;
    19. @Resource
    20. private WxPayService wxPayService;
    21. @Resource
    22. private RefundInfoService refundInfoService;
    23. /**
    24. * 从第0秒开始,每隔30秒执行一次,查询创建超过五分钟,并且未支付的订单
    25. */
    26. //@Scheduled(cron = "0/30 * * * * ?")
    27. public void orderConfirm() throws Exception {
    28. log.info("定时任务启动====");
    29. List orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.WXPAY.getType());
    30. for (OrderInfo orderInfo : orderInfoList){
    31. String orderNo = orderInfo.getOrderNo();
    32. log.warn("超时订单 === > {}", orderNo);
    33. //核实订单状态:调用微信支付查单接口
    34. wxPayService.checkOrderStatus(orderNo);
    35. }
    36. }
    37. /**
    38. * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未成功的退款单
    39. * */
    40. //@Scheduled(cron = "0/30 * * * * ?")
    41. public void refundConfirm() throws Exception {
    42. log.info("refundConfirm 被执行......");
    43. //找出申请退款超过5分钟并且未成功的退款单
    44. List refundInfoList = refundInfoService.getNoRefundOrderByDuration(5, PayType.WXPAY.getType());
    45. for (RefundInfo refundInfo : refundInfoList) {
    46. String refundNo = refundInfo.getRefundNo();
    47. log.warn("超时未退款的退款单号 ===> {}", refundNo);
    48. //核实订单状态:调用微信支付查询退款接口
    49. wxPayService.checkRefundStatus(refundNo);
    50. }
    51. }
    52. }

    测试

     

    支付宝支付

    AlipayClientConfig

    引入参数文件,获取参数并组装到client中。后面方便调用

    1. package com.wx.config;
    2. import com.alipay.api.*;
    3. import org.springframework.context.annotation.Bean;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.context.annotation.PropertySource;
    6. import org.springframework.core.env.Environment;
    7. import javax.annotation.Resource;
    8. /**
    9. * 加载支付宝配置参数文件
    10. */
    11. @Configuration
    12. //加载配置文件
    13. @PropertySource("classpath:alipay-sandbox.properties")
    14. public class AlipayClientConfig {
    15. @Resource
    16. private Environment config;//注入此对象,方便读取配置文件中数据
    17. @Bean
    18. public AlipayClient alipayClient() throws AlipayApiException {
    19. AlipayConfig alipayConfig = new AlipayConfig();
    20. //设置网关地址
    21. alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
    22. //设置应用Id
    23. alipayConfig.setAppId(config.getProperty("alipay.app-id"));
    24. //设置应用私钥
    25. alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
    26. //设置请求格式,固定值json
    27. alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
    28. //设置字符集
    29. alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
    30. //设置支付宝公钥
    31. alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
    32. //设置签名类型
    33. alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
    34. //构造client
    35. AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
    36. return alipayClient;
    37. }
    38. }

    alipay-sandbox.properties

    支付宝参数:此处用的是沙箱模式

    1. # 支付宝支付相关参数
    2. # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
    3. alipay.app-id=2021000121632501
    4. # 商户PID,卖家支付宝账号ID
    5. alipay.seller-id=2088621987731295
    6. # 支付宝网关
    7. alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
    8. # 商户私钥,您的PKCS8格式RSA2私钥
    9. alipay.merchant-private-key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq309YMdt/Kt2leisVuMbA6fTSmc2s9iY6wtuCDSbqz3RK187qsZepa2S7l6J16BWKXak0QIus70ZCGZ61U//ToQqDXc3JKlKvp19Pcq8YpvzByv0Z2FdtvGi9tbjX1icB2Xt/6uO9BYuixi9d3e1kzx9/M2RDiVuPmTSPvmIGJLYSmjsmJO1FOCyZQa5X/d0no5Ko4vKtV/DanqoqWNsOGpoU7bCFA/Y+PtS4xSEUgnsSWjymEQlfSublENadXhSLEP144ZrHKRDdFwTrua64KbQVFR5dnXvcVd4ERCD5C2Vtl+b3qx1puYlCxFPXp/dgC6f4iqQNZCj+W4m3NqmVAgMBAAECggEASVD34ofB/paN8+qvgep+nVfFTHfh4EzdqmjhdrPd9vJ8m4BtsBXzVSZXWoZ9lsm2NGBrsZfgVpt0Mfh8OKGKK2v17tfY7G/Uern+E0DKEHHWEfDfGK/TE6q75mqKnVGt+wUuEHzgqsIuX/FZcZU/vvmAMjwC0Vemib7a5rJxrOBvP40siA/e9se4PwmQHqfXH5J6vyJna6dH1r4f+sxhWdCb4O1VxZgI52J7rMStYGqwnEMKv5h7aB2zpq6BQbcblvNw6hBA80sn+F+LJM0Auebqk+HX/wZXHKsJVoRYEtCUNhl4YoNo5V3U3WYci1JXPJ+Op6PMI8n4iJZSTj6YIQKBgQDnDh2DkPR5RCLjJ1F+Kq5EotDNwLA21/xibLHE/gTT9kdxfKdSckZjOVp+nlMQ2Z+L8khD3YfRDD4sUheL8fKA22G9GnY31/4c2/XsjWPogr0BpgxFRt954OyPIoL+FQLkZnH05MOY5bq9N9/gfuuF3txCTJgMUYEWTba3Q52hkwKBgQC9Ud2wwwhVED5x80Tl4z4QVKro3ubbdat+IiCLOAOoW1IyRG+HV3CbG82DMT0F6h3YRBaRtC/UUoeh/YFpsYjhH30SghiM7N9l4Sk4X9z4eMvYklE02P81TOOukTmJzugHxtwb6k2YZC4LOu7+S2sRc0kTmUfX1CgmZ2L07ochNwKBgQCf4m6d6iKh/3o8wapsqdApgpkGp73IVbE50ok5DaX9nsBVUbLfJGB8rOVoFNraIB19U8yZ2aPwDo6/UJcmqefrLuP1XWhMwFQBWFxWsoheDooHqAV5ss9VoUVQzsriU1vK/PECS4LmPKH56b4rtOf5nPvBjQryCzxOWLyFGG7trQKBgElOCakH25IUWBmHOIZLFxz7q7G/nWQci+qrDC7b4Y6uzYTpOsYM9W0Zttm1lwtTO3sh4htIybxMuHfg0Ns8AuQobSVdemQW0+l+5ZcOh2EuZL/W59quqyLYQtC1KrJRi0Z3mYK1lpYLNEjk6OVODocTPJh6IXdQjrtQDOEJ+wjBAoGBAJopA4T5OnunBZkvYZPXya747m6U4nJFIcS0lJfM9ZGP2hw/mCOhXb2khL789v4G78kwUgfnVAcPPyyG1jjFtPiWb9+DrANZijBWb6HxnvTRytjeROlxIgXiLmb7MqTZHLujyb3H80rhTu3rqk8Jp1Yx26sEKKJXz3rc5KkSVOFt
    10. # 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
    11. alipay.alipay-public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgjkP008ZFSXVqWoSClXeKDTX7leNDVabMQcexv3TxZY4KYNUSd091BeYlE59feUaGTcehHc9r3N48jaqbZJyX8M2ogdqFlA/8iep22WcQu5ybIEUX45L40ClYqiqKLYpj/uuPFrekEKdZrS1DxaawDaazGypFFzpz/Lf6ijjbDeQhVsSqaPDAZEqmGWUo6oF1bahCpYJb9q/orqaihqA1vb7oRm7k3n8e76H6O1xxDVNenIsi4tit0wlZ6XneOVxnzEgsk0NAGa8BEH2gKrkVycVgBAUxjr7yWVyJuL0pYJkHnQbg6WxLDaDhe8iqGC1faSGqlB4PcIJp+pXHwv0DwIDAQAB
    12. # 接口内容加密秘钥,对称秘钥
    13. alipay.content-key=DNxJbSgGPbQwXL3jnKw42A==
    14. # 页面跳转同步通知页面路径
    15. alipay.return-url=http://localhost:8080/#/success
    16. # 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    17. # 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
    18. alipay.notify-url=http://ddme2g.natappfree.cc/api/ali-pay/trade/notify

    AliPayController

    1. package com.wx.controller;
    2. import com.alipay.api.AlipayApiException;
    3. import com.alipay.api.AlipayConstants;
    4. import com.alipay.api.internal.util.AlipaySignature;
    5. import com.wx.entity.OrderInfo;
    6. import com.wx.service.AliPayService;
    7. import com.wx.service.OrderInfoService;
    8. import com.wx.vo.R;
    9. import io.swagger.annotations.Api;
    10. import io.swagger.annotations.ApiOperation;
    11. import lombok.extern.slf4j.Slf4j;
    12. import org.springframework.core.env.Environment;
    13. import org.springframework.web.bind.annotation.*;
    14. import javax.annotation.Resource;
    15. import java.io.IOException;
    16. import java.math.BigDecimal;
    17. import java.util.Map;
    18. @RestController
    19. @CrossOrigin
    20. @RequestMapping("/api/ali-pay")
    21. @Api(tags = "支付宝支付")
    22. @Slf4j
    23. public class AliPayController {
    24. @Resource
    25. private AliPayService aliPayService;
    26. @Resource
    27. private Environment config;
    28. @Resource
    29. private OrderInfoService orderInfoService;
    30. @ApiOperation("统一收单下单支付页面接口调用")
    31. @PostMapping("/trade/page/pay/{productId}")
    32. public R tradePage(@PathVariable Long productId){
    33. log.info("统一收单下单支付页面接口调用");
    34. //支付报开放平台接受 request 请求对象后
    35. //会被开放者生成一个html形式的from表单,包含自动提交的脚本
    36. String formStr = aliPayService.tradeCteate(productId);
    37. //我们将from表单字符串返回给前端程序.之后前端将会调用自动提交脚本,进行表单的提交
    38. //此时,表单会自动提交的action属性所执行的支付宝开放平台中,从而为用户展示一个支付页面
    39. return R.ok().data("formStr",formStr);
    40. }
    41. @ApiOperation("支付通知")
    42. @PostMapping("/trade/notify")
    43. public String tradeNotify(@RequestParam Map params){
    44. log.info("支付通知正在进行");
    45. log.info("通知参数 ===》 :"+params);
    46. String result = "failure";
    47. try {
    48. //异步通知验签
    49. boolean signVerified = AlipaySignature.rsaCheckV1(
    50. params,
    51. config.getProperty("alipay.alipay-public-key"),
    52. AlipayConstants.CHARSET_UTF8,
    53. AlipayConstants.SIGN_TYPE_RSA2);//调用SDK验证签名
    54. if(!signVerified){
    55. //验签失败则记录异常日志,并在response中返回failure.
    56. log.error("异步通知验签失败");
    57. return result;
    58. }
    59. //验签成功后
    60. log.info("支付成功异步通知验签成功!");
    61. //按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验
    62. //1.商家需要验证该通知数据中的 out_trade_no 是否为商家系统中创建的订单号。
    63. String outTradeNo = params.get("out_trade_no");
    64. OrderInfo oeder = orderInfoService.getOrderByOrderNo(outTradeNo);
    65. if (oeder == null){
    66. log.error("订单不存在");
    67. return result;
    68. }
    69. //2.判断 total_amount 是否确实为该订单的实际金额(即商家订单创建时的金额)。
    70. String totalAmount = params.get("total_amount");
    71. int totalAmoutInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
    72. int totalFeeInt = oeder.getTotalFee().intValue();
    73. if (totalAmoutInt != totalFeeInt){
    74. log.error("金额校验失败");
    75. return result;
    76. }
    77. //3.校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商家可能有多个 seller_id/seller_email)。
    78. String sellerId = params.get("seller_id");
    79. String sellerIdPro = config.getProperty("alipay.seller-id");
    80. if (!sellerId.equals(sellerIdPro)){
    81. log.error("商家pid校验失败");
    82. return result;
    83. }
    84. //4.验证 app_id 是否为该商家本身。
    85. String appId = params.get("app_id");
    86. String appIdProperty = config.getProperty("alipay.app-id");
    87. if (!appId.equals(appIdProperty)){
    88. log.error("appid校验失败");
    89. return result;
    90. }
    91. //只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。
    92. String tradeStatus = params.get("trade_status");
    93. if (!"TRADE_SUCCESS".equals(tradeStatus)){
    94. log.error("支付未成功");
    95. return result;
    96. }
    97. //处理业务,修改订单状态,记录支付日志
    98. aliPayService.processOrder(params);
    99. //校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
    100. //向支付宝返回成功的标识,否则会一直不间断的发送通知给我们
    101. result = "success";
    102. } catch (AlipayApiException e) {
    103. e.printStackTrace();
    104. }
    105. return result;
    106. }
    107. /**
    108. * 用户取消订单
    109. * @param orderNo
    110. * @return
    111. */
    112. @ApiOperation("用户取消订单")
    113. @PostMapping("/trade/close/{orderNo}")
    114. public R cancel(@PathVariable String orderNo){
    115. log.info("用户取消订单");
    116. aliPayService.cancelOrder(orderNo);
    117. return R.ok().setMessage("用户已取消订单");
    118. }
    119. /**
    120. * 查询订单
    121. * @param orderNo
    122. * @return
    123. */
    124. @ApiOperation("支付宝支付查询订单")
    125. @GetMapping("/trade/query/{orderNo}")
    126. public R queryOrder(@PathVariable String orderNo){
    127. log.info("查询订单");
    128. String result = aliPayService.queryOrder(orderNo);
    129. return R.ok().setMessage("查询成功").data("result",result);
    130. }
    131. /**
    132. * 申请退款
    133. * @param orderNo
    134. * @param reason
    135. * @return
    136. */
    137. @ApiOperation("申请退款")
    138. @PostMapping("/trade/refund/{orderNo}/{reason}")
    139. public R refunds(@PathVariable String orderNo, @PathVariable String reason){
    140. log.info("申请退款");
    141. aliPayService.refund(orderNo, reason);
    142. return R.ok();
    143. }
    144. /**
    145. * 查询退款
    146. * @param orderNo
    147. * @return
    148. * @throws Exception
    149. */
    150. @ApiOperation("查询退款")
    151. @GetMapping("/trade/fastpay/refund/{orderNo}")
    152. public R queryRefund(@PathVariable String orderNo) throws Exception {
    153. log.info("查询退款");
    154. String result = aliPayService.queryRefund(orderNo);
    155. return R.ok().setMessage("查询成功").data("result", result);
    156. }
    157. /**
    158. * 根据账单类型和日期获取账单url地址
    159. *
    160. * @param billDate
    161. * @param type
    162. * @return
    163. */
    164. @ApiOperation("获取账单url")
    165. @GetMapping("/bill/downloadurl/query/{billDate}/{type}")
    166. public R queryTradeBill(
    167. @PathVariable String billDate,
    168. @PathVariable String type) {
    169. log.info("获取账单url");
    170. String downloadUrl = aliPayService.queryBill(billDate, type);
    171. return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);
    172. }
    173. }

    AliPayTradeState

    支付类型

    1. package com.wx.enums.wxpay;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. @AllArgsConstructor
    5. @Getter
    6. public enum AliPayTradeState {
    7. /**
    8. * 支付成功
    9. */
    10. SUCCESS("TRADE_SUCCESS"),
    11. /**
    12. * 未支付
    13. */
    14. NOTPAY("WAIT_BUYER_PAY"),
    15. /**
    16. * 已关闭
    17. */
    18. CLOSED("TRADE_CLOSED"),
    19. /**
    20. * 退款成功
    21. */
    22. REFUND_SUCCESS("REFUND_SUCCESS"),
    23. /**
    24. * 退款失败
    25. */
    26. REFUND_ERROR("REFUND_ERROR");
    27. /**
    28. * 类型
    29. */
    30. private final String type;
    31. }

    AliPayServiceImpl

    1. package com.wx.service.impl;
    2. import com.alibaba.fastjson.JSONObject;
    3. import com.alipay.api.AlipayApiException;
    4. import com.alipay.api.AlipayClient;
    5. import com.alipay.api.DefaultAlipayClient;
    6. import com.alipay.api.request.*;
    7. import com.alipay.api.response.*;
    8. import com.google.gson.Gson;
    9. import com.google.gson.JsonSyntaxException;
    10. import com.google.gson.internal.LinkedTreeMap;
    11. import com.wx.entity.OrderInfo;
    12. import com.wx.entity.RefundInfo;
    13. import com.wx.enums.OrderStatus;
    14. import com.wx.enums.PayType;
    15. import com.wx.enums.wxpay.AliPayTradeState;
    16. import com.wx.service.AliPayService;
    17. import com.wx.service.OrderInfoService;
    18. import com.wx.service.PaymentInfoService;
    19. import com.wx.service.RefundInfoService;
    20. import lombok.extern.slf4j.Slf4j;
    21. import org.springframework.core.env.Environment;
    22. import org.springframework.stereotype.Service;
    23. import org.springframework.transaction.annotation.Transactional;
    24. import javax.annotation.Resource;
    25. import java.math.BigDecimal;
    26. import java.util.HashMap;
    27. import java.util.LinkedHashMap;
    28. import java.util.Map;
    29. import java.util.concurrent.locks.ReentrantLock;
    30. @Service
    31. @Slf4j
    32. public class AliPayServiceImpl implements AliPayService {
    33. @Resource
    34. private OrderInfoService orderInfoService;
    35. @Resource
    36. private AlipayClient alipayClient;
    37. @Resource
    38. private Environment config;
    39. @Resource
    40. private PaymentInfoService paymentInfoService;
    41. @Resource
    42. private RefundInfoService refundsInfoService;
    43. private final ReentrantLock lock = new ReentrantLock();
    44. /**
    45. * 统一收单下单支付页面接口调用
    46. * @param productId
    47. * @return
    48. */
    49. @Transactional(rollbackFor = Exception.class)
    50. @Override
    51. public String tradeCteate(Long productId) {
    52. try {
    53. //生成订单
    54. log.info("生成订单");
    55. OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());
    56. //调用支付宝接口
    57. AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
    58. //配置需要的公共请求参数
    59. //支付完成后,支付宝发起异步通知的地址
    60. request.setNotifyUrl(config.getProperty("alipay.notify-url"));
    61. //支付完成后,我们想让页面跳转回成功的页面,配置returnUrl
    62. request.setReturnUrl(config.getProperty("alipay.return-url"));
    63. //组装当前业务方法的请求参数
    64. JSONObject bizContent = new JSONObject();
    65. bizContent.put("out_trade_no", orderInfo.getOrderNo());
    66. //因为微信是分,这里支付宝是元,所以进行更改
    67. BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));
    68. bizContent.put("total_amount", total);
    69. bizContent.put("subject", orderInfo.getTitle());
    70. bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
    71. request.setBizContent(bizContent.toString());
    72. //执行请求,调用支付宝接口
    73. AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
    74. if(response.isSuccess()){
    75. log.info("调用成功,返回结果 ===> " + response.getBody());
    76. return response.getBody();
    77. } else {
    78. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    79. throw new RuntimeException("创建支付交易失败");
    80. }
    81. } catch (AlipayApiException e) {
    82. e.printStackTrace();
    83. throw new RuntimeException("创建支付交易失败");
    84. }
    85. }
    86. /**
    87. * 处理业务,修改订单状态,记录支付日志
    88. * @param params
    89. */
    90. @Transactional(rollbackFor = Exception.class)
    91. @Override
    92. public void processOrder(Map params) {
    93. log.info("处理订单");
    94. //获取订单号
    95. String orderNo = params.get("out_trade_no");
    96. /**
    97. * 在对业务数据进行状态检查和处理之前
    98. * 要采用数据锁进行控制
    99. * 以避免函数重入造成数据混乱
    100. */
    101. if (lock.tryLock()) {
    102. try {
    103. //处理重复的通知
    104. //接口调用的幂等性:无论接口被调用多少次,以下业务只执行一次
    105. String orderStatus = orderInfoService.getOrderStatus(orderNo);
    106. if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {//如果不等于未支付
    107. return;
    108. }
    109. //更新订单状态
    110. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
    111. //记录订单日志
    112. paymentInfoService.createPaymentInfoForAlipay(params);
    113. }finally {
    114. //释放锁
    115. lock.unlock();
    116. }
    117. }
    118. }
    119. /**
    120. * 用户取消支付宝订单
    121. * @param orderNo
    122. */
    123. @Override
    124. public void cancelOrder(String orderNo) {
    125. //调用支付宝提供的统一收单交易关闭接口
    126. this.closeOrder(orderNo);
    127. //更新用户订单状态
    128. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
    129. }
    130. /**
    131. * 支付宝查询订单
    132. * @param orderNo
    133. * @return 返回订单查询结果 如果返回null则表示订单不存在
    134. */
    135. @Override
    136. public String queryOrder(String orderNo) {
    137. try {
    138. log.info("查询接口调用 === 》"+orderNo);
    139. AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
    140. JSONObject bizContent = new JSONObject();
    141. bizContent.put("out_trade_no", orderNo);
    142. request.setBizContent(bizContent.toString());
    143. AlipayTradeQueryResponse response = alipayClient.execute(request);
    144. if(response.isSuccess()){
    145. log.info("调用成功,返回结果 ===> " + response.getBody());
    146. return response.getBody();
    147. } else {
    148. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    149. //throw new RuntimeException("查询接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
    150. return null;
    151. }
    152. } catch (AlipayApiException e) {
    153. e.printStackTrace();
    154. throw new RuntimeException("查询接口调用失败");
    155. }
    156. }
    157. /**
    158. * 根据订单号查询支付宝支付查单接口,核实订单状态
    159. * 如果订单未创建,则直接更新商户端订单状态
    160. * 如果订单已经支付,则更新商户端订单状态
    161. * 如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
    162. * @param orderNo
    163. */
    164. @Override
    165. public void checkOrderStatus(String orderNo) {
    166. log.warn("根据订单号核实订单状态 ===》"+orderNo);
    167. String result = this.queryOrder(orderNo);
    168. //订单未创建
    169. if (result == null){
    170. log.warn("核实订单未创建 ===》"+orderNo);
    171. //更新本地订单状态
    172. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
    173. }
    174. try {
    175. //解析查单响应结果
    176. Gson gson = new Gson();
    177. HashMap resultMap = gson.fromJson(result, HashMap.class);
    178. LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");
    179. String tradeStatus = alipayTradeQueryResponse.get("trade_status").toString();//获取订单状态
    180. //判断订单是否是未支付的订单
    181. if (AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){
    182. log.info("未支付订单==== 》"+orderNo);
    183. //如果订单未支付,则调用关单接口关闭订单,并更新商户端订单状态
    184. this.closeOrder(orderNo);
    185. //并更新商户端订单状态
    186. orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);
    187. log.info("更改订单状态==== 》"+OrderStatus.CLOSED);
    188. }
    189. if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)) {
    190. log.warn("核实订单已支付 ===> {}", orderNo);
    191. //如果订单已支付,则更新商户端订单状态
    192. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
    193. //并记录支付日志
    194. paymentInfoService.createPaymentInfoForAlipay(alipayTradeQueryResponse);
    195. }
    196. } catch (NullPointerException e) {
    197. log.info("支付宝未创建订单,改为超时已关闭"+orderNo);
    198. }
    199. }
    200. /**
    201. * 关单接口的调用
    202. * @param orderNo
    203. */
    204. private void closeOrder(String orderNo) {
    205. try {
    206. log.info("关单接口的调用,订单号:"+orderNo);
    207. AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
    208. JSONObject bizContent = new JSONObject();
    209. bizContent.put("out_trade_no", orderNo);
    210. request.setBizContent(bizContent.toString());
    211. //调用关单接口
    212. AlipayTradeCloseResponse response = alipayClient.execute(request);
    213. if(response.isSuccess()){
    214. log.info("调用成功,返回结果 ===> " + response.getBody());
    215. } else {
    216. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    217. // throw new RuntimeException("关单接口调用失败");//因为会出现只点击但未扫码,支付宝没有创建订单的情况,就会报错。所以直接取消。这样直接把这个订单改成取消
    218. }
    219. } catch (AlipayApiException e) {
    220. e.printStackTrace();
    221. throw new RuntimeException("关单接口调用失败");
    222. }
    223. }
    224. /**
    225. * 退款
    226. * @param orderNo
    227. * @param reason
    228. */
    229. @Transactional(rollbackFor = Exception.class)
    230. @Override
    231. public void refund(String orderNo, String reason) {
    232. try {
    233. log.info("调用退款API");
    234. //创建退款单
    235. RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);
    236. //调用统一收单交易退款接口
    237. AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();
    238. //组装当前业务方法的请求参数
    239. JSONObject bizContent = new JSONObject();
    240. bizContent.put("out_trade_no", orderNo);//订单编号
    241. BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
    242. //BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
    243. bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
    244. bizContent.put("refund_reason", reason);//退款原因(可选)
    245. request.setBizContent(bizContent.toString());
    246. //执行请求,调用支付宝接口
    247. AlipayTradeRefundResponse response = alipayClient.execute(request);
    248. if(response.isSuccess()){
    249. log.info("调用成功,返回结果 ===> " + response.getBody());
    250. //更新订单状态
    251. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
    252. //更新退款单
    253. refundsInfoService.updateRefundForAliPay(
    254. refundInfo.getRefundNo(),
    255. response.getBody(),
    256. AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功
    257. } else {
    258. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    259. //更新订单状态
    260. orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
    261. //更新退款单
    262. refundsInfoService.updateRefundForAliPay(
    263. refundInfo.getRefundNo(),
    264. response.getBody(),
    265. AliPayTradeState.REFUND_ERROR.getType()); //退款失败
    266. }
    267. } catch (AlipayApiException e) {
    268. e.printStackTrace();
    269. throw new RuntimeException("创建退款申请失败");
    270. }
    271. }
    272. /**
    273. * 查询退款
    274. * @param orderNo
    275. * @return
    276. */
    277. @Override
    278. public String queryRefund(String orderNo) {
    279. try {
    280. log.info("查询退款接口调用 ===> {}", orderNo);
    281. AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
    282. JSONObject bizContent = new JSONObject();
    283. bizContent.put("out_trade_no", orderNo);
    284. bizContent.put("out_request_no", orderNo);
    285. request.setBizContent(bizContent.toString());
    286. AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
    287. if(response.isSuccess()){
    288. log.info("调用成功,返回结果 ===> " + response.getBody());
    289. return response.getBody();
    290. } else {
    291. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    292. //throw new RuntimeException("查单接口的调用失败");
    293. return null;//订单不存在
    294. }
    295. } catch (AlipayApiException e) {
    296. e.printStackTrace();
    297. throw new RuntimeException("查单接口的调用失败");
    298. }
    299. }
    300. /**
    301. * 申请账单
    302. * @param billDate
    303. * @param type
    304. * @return
    305. */
    306. @Override
    307. public String queryBill(String billDate, String type) {
    308. try {
    309. AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
    310. JSONObject bizContent = new JSONObject();
    311. bizContent.put("bill_type", type);
    312. bizContent.put("bill_date", billDate);
    313. request.setBizContent(bizContent.toString());
    314. AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
    315. if(response.isSuccess()){
    316. log.info("调用成功,返回结果 ===> " + response.getBody());
    317. //获取账单下载地址
    318. Gson gson = new Gson();
    319. HashMap resultMap = gson.fromJson(response.getBody(), HashMap.class);
    320. LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
    321. String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");
    322. return billDownloadUrl;
    323. } else {
    324. log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
    325. throw new RuntimeException("申请账单失败");
    326. }
    327. } catch (AlipayApiException e) {
    328. e.printStackTrace();
    329. throw new RuntimeException("申请账单失败");
    330. }
    331. }
    332. }

    AliPayService

    1. package com.wx.service;
    2. import com.alipay.api.AlipayApiException;
    3. import java.util.Map;
    4. public interface AliPayService {
    5. String tradeCteate(Long productId);//统一收单下单支付页面接口调用
    6. void processOrder(Map params);//处理业务,修改订单状态,记录支付日志
    7. void cancelOrder(String orderNo);//支付宝用户取消订单
    8. String queryOrder(String orderNo);//支付宝查询订单
    9. void checkOrderStatus(String orderNo);//处理超时的订单
    10. void refund(String orderNo, String reason);//申请退款
    11. String queryRefund(String orderNo);
    12. String queryBill(String billDate, String type);//下载账单
    13. }

    AliPayTask

    1. package com.wx.task;
    2. import com.google.gson.Gson;
    3. import com.wx.entity.OrderInfo;
    4. import com.wx.enums.PayType;
    5. import com.wx.service.AliPayService;
    6. import com.wx.service.OrderInfoService;
    7. import lombok.extern.slf4j.Slf4j;
    8. import org.springframework.scheduling.annotation.Scheduled;
    9. import org.springframework.stereotype.Component;
    10. import javax.annotation.Resource;
    11. import java.util.List;
    12. @Slf4j
    13. @Component
    14. public class AliPayTask {
    15. @Resource
    16. private OrderInfoService orderInfoService;
    17. @Resource
    18. private AliPayService aliPayService;
    19. /**
    20. * 从第0秒开始每隔30秒执行1次,查询创建超过5分钟,并且未支付的订单
    21. * @throws Exception
    22. */
    23. @Scheduled(cron = "0/30 * * * * ?")
    24. public void orderConfirm() throws Exception {
    25. log.info("支付宝定时任务启动====");
    26. List orderInfoList = orderInfoService.getNopayOrderByDuration(5, PayType.ALIPAY.getType());
    27. for (OrderInfo orderInfo : orderInfoList){
    28. String orderNo = orderInfo.getOrderNo();
    29. log.warn("超时订单 === > {}", orderNo);
    30. //核实订单状态:调用支付宝支付查单接口
    31. aliPayService.checkOrderStatus(orderNo);
    32. }
    33. }
    34. }

    支付宝信息总结

    开放平台账号注册

     

    常规接⼊流程

    创建应⽤:选择应⽤类型、填写应⽤基本信息、添加应⽤功能、配置应⽤环境(获取⽀付宝公
    钥、应⽤公钥、应⽤私钥、⽀付宝⽹关地址,配置接⼝内容加密⽅式)、查看 APPID
    绑定应⽤:将开发者账号中的 APPID 和商家账号 PID 进⾏绑定
    配置秘钥:即创建应⽤中的 配置应⽤环境 步骤
    上线应⽤:将应⽤提交审核
    签约功能:在商家中⼼上传营业执照、已备案⽹站信息等,提交审核进⾏签约

    使⽤沙箱

    沙箱环境配置: https://opendocs.alipay.com/common/02kkv7
    沙箱版⽀付宝的下载和登录: https://open.alipay.com/platform/appDaily.htm?tab=tool

     

    引⼊依赖

    参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 服务端 SDK => Java => 通⽤版 => Maven 项⽬依赖
    https://search.maven.org/artifact/com.alipay.sdk/alipay-sdk-java

    创建客⼾端连接对象

    创建带数据签名的客⼾端对象
    参考⽂档:开放平台 => ⽂档 => 开发⼯具 => 技术接⼊指南 => 数据签名 
    https://opendocs.alipay.com/common/02kf5q
    参考⽂档中 公钥方式 完善 AlipayClientConfig 类,添加 alipayClient() ⽅法 初始化 AlipayClient 对象

    ⽀付调⽤流程

    https://opendocs.alipay.com/open/270/105899

    接⼝说明

    alipay.trade.page.pay(统一收单下单并支付页面接口)

    https://opendocs.alipay.com/apis/028r8t?scene=22
    公共请求参数:所有接⼝都需要的参数
    请求参数:当前接⼝需要的参数
    公共响应参数:所有接⼝的响应中都包含的数据
    响应参数:当前接⼝的响应中包含的数据

    内网穿透工具:

    NATAPP -icon-default.png?t=M666https://natapp.cn/login内网穿透教程:

    NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具icon-default.png?t=M666https://natapp.cn/article/natapp_newbie

  • 相关阅读:
    Rxjs map, mergeMap 和 switchMap 的区别和联系
    华纳云:Linux文件不存在无法删除如何解决
    高校教室预约使用管理系统(PHP+Mysql)毕业论文+项目源码+数据库sql文件
    web前端期末大作业 HTML游戏资讯网页设计制作 简单静态HTML网页作品 DW游戏资讯网页作业成品 游戏网站模板
    【DaVinci Developer工具实战】02 - 软件设计编辑器
    开发成本类似快递出入库寄收件成本多少
    Java预习43
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java网上私厨到家服务平台dp28s
    WordPress供求插件API文档:用户登录
    HCIA-datacom 4.3 实验三:网络地址转换配置实验
  • 原文地址:https://blog.csdn.net/Java_Mr_Jin/article/details/125907192