支付模块【暂】

微信支付

微信支付平台:https://pay.weixin.qq.com/

1. 准备工作

获取标识商户身份的信息、商户的证书和私钥、微信支付的证书、微信支付API的URL

1.2 获取商户号

微信商户平台:https://pay.weixin.qq.com/

步骤:申请成为商户 => 提交资料 => 签署协议 => 获取商户号

1.2 获取AppID

微信公众平台:https://mp.weixin.qq.com/

步骤:注册服务号 => 服务号认证 => 获取APPID => 绑定商户号

1.3 申请商户证书

步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 申请API证书 包括商户证书和商户私钥

1.4 获取微信的证书

可以预先下载,也可以通过编程的方式获取。

1.5 获取APIv3秘钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)

步骤:登录商户平台 => 选择 账户中心 => 安全中心 => API安全 => 设置APIv3密钥

1.6 编程准备

1.6.1 引入依赖
<!--微信支付SDK-->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-apache-httpclient</artifactId>
    <version>0.4.7</version>
</dependency>
1.6.2 配置和配置类

基本配置

# 微信支付相关参数
# 用户标识
# 商户号
wxpay.mch-id=xxxxxxxxxxxxxxx
# APPID
wxpay.appid=xxxxxxxxxxxxx

# 确保SSL(内容未作任何加密,只做了签名.)
# 商户API证书序列号
wxpay.mch-serial-no=xxxxxxxxxxxxxxx
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem

# APIv3密钥(在微信支付回调通知和商户获取平台证书使用APIv3密钥)
wxpay.api-v3-key=xxxxxxxxxxxxxxx

# 相关地址
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址(本地可远程访问地址,用与微信服务器通信,测试阶段建议穿透一下)
wxpay.notify-domain=https://xxxxxxxxxxxxxxx

基本配置类

import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.tomcat.util.net.jsse.PEMFile;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;


@Configuration
@PropertySource("classpath:wxpay.properties") //读取配置文件
@ConfigurationProperties(prefix = "wxpay") //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
@Slf4j
public class WxPayConfig {

    // 商户号
    private String mchId;

    // 商户API证书序列号
    private String mchSerialNo;

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

    /**
     * 获取商户的私钥文件
     *
     * @param filename
     * @return
     */
    private PrivateKey getPrivateKey(String filename) {

        try {
            return PemUtil.loadPrivateKey(
                    new FileInputStream(filename));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("私钥文件不存在", e);
        }
    }

    /**
     * 获取签名验证器
     * @return
     */
    @Bean
    public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {

        log.info("获取签名验证器");

        // 获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        // 私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, privateKey);

        // 获取证书管理器实例
        CertificatesManager certificatesManager = CertificatesManager.getInstance();

        // 身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);

        // 向证书管理器增加需要自动更新平台证书的商户信息
        certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
        // ... 若有多个商户号,可继续调用putMerchant添加商户信息

        // 从证书管理器中获取verifier
        Verifier verifier = certificatesManager.getVerifier(mchId);
        return verifier;
    }

    /**
     * 根据verifier签名验证器来获取http请求对象
     * @param verifier
     * @return
     */
    @Bean(name = "wxPayClient")
    public CloseableHttpClient getWxPayClient(Verifier verifier) {

        log.info("获取httpClient");

        // 获取商户私钥
        PrivateKey privateKey = getPrivateKey(privateKeyPath);

        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(mchId, mchSerialNo, privateKey)
                .withValidator(new WechatPay2Validator(verifier));
        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

        // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
        CloseableHttpClient httpClient = builder.build();

        return httpClient;
    }

}
1.6.3 工具类

Http工具类

package com.atguigu.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;


public class HttpUtils {

    /**
     * 将通知参数转化为字符串
     * @param request
     * @return
     */
    public static String readData(HttpServletRequest request) {
        BufferedReader br = null;
        try {
            StringBuilder result = new StringBuilder();
            br = request.getReader();
            for (String line; (line = br.readLine()) != null; ) {
                if (result.length() > 0) {
                    result.append("\n");
                }
                result.append(line);
            }
            return result.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

生成订单编号工具类

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 订单号工具类
 *
 * @author qy
 * @since 1.0
 */
public class OrderNoUtils {

    /**
     * 获取订单编号
     * @return
     */
    public static String getOrderNo() {
        return "ORDER_" + getNo();
    }

    /**
     * 获取退款单编号
     * @return
     */
    public static String getRefundNo() {
        return "REFUND_" + getNo();
    }

    /**
     * 获取编号
     * @return
     */
    public static String getNo() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String newDate = sdf.format(new Date());
        String result = "";
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            result += random.nextInt(10);
        }
        return newDate + result;
    }

}

后端校验微信发送消息真伪的验证签名【简称验签】工具类

import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;

import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;


public class WechatPay2ValidatorForRequest{

    protected static final Logger log = LoggerFactory.getLogger(WechatPay2ValidatorForRequest.class);
    /**
     * 应答超时时间,单位为分钟
     */
    protected static final long RESPONSE_EXPIRED_MINUTES = 5;
    protected final Verifier verifier;
    protected final String body;
    protected final String requestId;

    public WechatPay2ValidatorForRequest(Verifier verifier, String body, String requestId) {
        this.verifier = verifier;
        this.body = body;
        this.requestId = requestId;
    }

    protected static IllegalArgumentException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }

    protected static IllegalArgumentException verifyFail(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("signature verify fail: " + message);
    }

    public final boolean validate(HttpServletRequest request) throws IOException {
        try {
            validateParameters(request);

            String message = buildMessage(request);
            String serial = request.getHeader(WECHAT_PAY_SERIAL);
            String signature = request.getHeader(WECHAT_PAY_SIGNATURE);

            if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
                throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
                        serial, message, signature, requestId);
            }
        } catch (IllegalArgumentException e) {
            log.warn(e.getMessage());
            return false;
        }

        return true;
    }

    protected final void validateParameters(HttpServletRequest request) {

        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};

        String header = null;
        for (String headerName : headers) {
            header = request.getHeader(headerName);
            if (header == null) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }

        String timestampStr = header;
        try {
            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
            // 拒绝过期应答
            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
            }
        } catch (DateTimeException | NumberFormatException e) {
            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
        }
    }

    protected final String buildMessage(HttpServletRequest request) throws IOException {
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        return timestamp + "\n"
                + nonce + "\n"
                + body + "\n";
    }
}
1.6.4 枚举类

支付类型枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum PayType {
    /**
     * 微信
     */
    WXPAY("微信"),


    /**
     * 支付宝
     */
    ALIPAY("支付宝");

    /**
     * 类型
     */
    private final String type;
}

后端订单状态枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum OrderStatus {
    /**
     * 未支付
     */
    NOTPAY("未支付"),


    /**
     * 支付成功
     */
    SUCCESS("支付成功"),

    /**
     * 已关闭
     */
    CLOSED("超时已关闭"),

    /**
     * 已取消
     */
    CANCEL("用户已取消"),

    /**
     * 退款中
     */
    REFUND_PROCESSING("退款中"),

    /**
     * 已退款
     */
    REFUND_SUCCESS("已退款"),

    /**
     * 退款异常
     */
    REFUND_ABNORMAL("退款异常");

    /**
     * 类型
     */
    private final String type;
}

微信订单状态枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxTradeState {

    /**
     * 支付成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 未支付
     */
    NOTPAY("NOTPAY"),

    /**
     * 已关闭
     */
    CLOSED("CLOSED"),

    /**
     * 转入退款
     */
    REFUND("REFUND");

    /**
     * 类型
     */
    private final String type;
}

微信Native API接口枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxApiType {

   /**
    * Native下单
    */
   NATIVE_PAY("/v3/pay/transactions/native"),

   /**
    * 查询订单
    */
   ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),

   /**
    * 关闭订单
    */
   CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),

   /**
    * 申请退款
    */
   DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),

   /**
    * 查询单笔退款
    */
   DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),

   /**
    * 申请交易账单
    */
   TRADE_BILLS("/v3/bill/tradebill"),

   /**
    * 申请资金账单
    */
   FUND_FLOW_BILLS("/v3/bill/fundflowbill");


   /**
    * 类型
    */
   private final String type;
}

微信通知 API 接口枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxNotifyType {

   /**
    * 支付通知
    */
   NATIVE_NOTIFY("/api/wx-pay/native/notify"),


   /**
    * 退款结果通知
    */
   REFUND_NOTIFY("/api/wx-pay/refunds/notify");

   /**
    * 类型
    */
   private final String type;
}

微信退单枚举类

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum WxRefundStatus {

    /**
     * 退款成功
     */
    SUCCESS("SUCCESS"),

    /**
     * 退款关闭
     */
    CLOSED("CLOSED"),

    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING"),

    /**
     * 退款异常
     */
    ABNORMAL("ABNORMAL");

    /**
     * 类型
     */
    private final String type;
}

1.7 读懂业务流程时序图

适用对象:直连商户

img

1.7.1 角色说明

微信支付用户:付款用户,也就是人【以下简称用户】

微信客户端:可以理解为前端系统【以下简称前端】

商户后台系统:可以理解为后端负责支付功能的系统或模块【以下简称后端】

微信支付系统:微信服务器中负责处理支付相关业务的系统【以下简称微信】

1.7.2 业务流程时序图解释

(1)后端根据用户选购的商品 生成订单

(2)用户确认支付后由后端调起【Native下单API】生成预支付交易;

(3)微信收到请求后 生成预支付交易单,并返回交易会话的二维码链接code_url。

(4)前端或后端只需要根据返回的code_url 生成二维码 展示给用户。

(5)用户打开微信客户端“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信。

(6)微信收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。

(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。

(8)微信根据用户授权完成支付交易。

(9)微信完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。

(10)微信支付系统通过发送 异步消息 通知后端支付结果。后端需回复接收情况,通知微信不再发送该单的支付通知。

(11)未收到支付通知的情况,后端调用【查询订单API】。

(12)商户确认订单已支付后给用户发货。

【9-11 并发操作】

2. 使用

2.1 支付

2.1.1 创建支付订单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml

/**
 * 创建订单,调用Native支付接口
 * @param productId
 * @return code_url 和 订单号
 * @throws Exception
 */
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {
    log.info("生成订单");
    // 生成订单...
    // OrderInfo orderInfo...

    log.info("调用统一下单API");

    // 调用统一下单API
    HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
    // 请求body参数
    Gson gson = new Gson();
    Map paramsMap = new HashMap();
    paramsMap.put("appid", wxPayConfig.getAppid());
    paramsMap.put("mchid", wxPayConfig.getMchId());
    paramsMap.put("description", orderInfo.getTitle());
    paramsMap.put("out_trade_no", orderInfo.getOrderNo());
    paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

    Map amountMap = new HashMap();
    amountMap.put("total", orderInfo.getTotalFee());
    amountMap.put("currency", "CNY");

    paramsMap.put("amount", amountMap);

    //将参数转换成json字符串
    String jsonParams = gson.toJson(paramsMap);
    log.info("请求参数: " + jsonParams);

    StringEntity entity = new StringEntity(jsonParams,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpPost);

    try {
        int statusCode = response.getStatusLine().getStatusCode();// 响应状态码
        String bodyAsString = EntityUtils.toString(response.getEntity());// 响应体
        if (statusCode == 200) { //处理成功
            log.info("成功返回结果: " + bodyAsString);
        } else if (statusCode == 204) { //处理成功,无返回Body
            log.info("成功");
        } else {
            log.info("Native下单失败,响应码: " + statusCode+ ",返回结果: " + bodyAsString);
            throw new IOException("请求失败");
        }

        // 响应结果
        Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
        // 二维码链接
        codeUrl = resultMap.get("code_url");

        // 保存二维码链接
        orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);

        // 返回二维码链接
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());

        return map;
    } finally {
        response.close();
    }
}
2.1.2 支付通知

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_5.shtml

@RestController
@CrossOrigin  // 跨域
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付APIv3")
@Slf4j
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @Resource
    private Verifier verifier;

    /**
     * 支付通知
     * 微信支付通过支付通知接口将用户支付成功消息通知给商户
     */
    @ApiOperation("支付通知")
    @PostMapping("/native/notify")
    public String nativeNotify(HttpServletRequest request, HttpServletResponse response){
		// 收到微信通知回调
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象

        try {

            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);
            //log.info("支付通知的完整数据 ===> {}", body);
            //int a = 9 / 0;

            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            //健壮性检查
            if(!wechatPay2ValidatorForRequest.validate(request)){

                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");

            //处理订单【由此深入】
            wxPayService.processOrder(bodyMap);

            //应答超时
            //模拟接收微信端的重复通知
//            TimeUnit.SECONDS.sleep(5);

            //成功应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);

        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }

    }
}
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundInfoService refundsInfoService;

    @Resource
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名

    private final ReentrantLock lock = new ReentrantLock();

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("处理订单");

        //解密报文
        String plainText = decryptFromResource(bodyMap);

        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");


        /*在对业务数据进行状态检查和处理之前,
        要采用数据锁进行并发控制,
        以避免函数重入造成的数据混乱*/
        //尝试获取锁:
        // 成功获取则立即返回true,获取失败则立即返回false。不必一直等待锁的释放
        if(lock.tryLock()){
            // 锁内部是业务相关方法,可以先不用管
            try {
                //处理重复的通知
                //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
                String orderStatus = orderInfoService.getOrderStatus(orderNo);//根据订单号获取订单状态
                if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
                    return;
                }

                //模拟通知并发
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

                //记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("密文解密");

        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        log.info("密文 ===> {}", ciphertext);
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info("明文 ===> {}", plainText);

        return plainText;
    }
}
2.1.3 查询支付订单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml

public String queryOrder(String orderNo) throws Exception {

    log.info("查单接口调用 ===> {}", orderNo);

    //组装数据
    String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
    url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());

    HttpGet httpGet = new HttpGet(url);
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpGet);

    try {
        //解析响应
        String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
        int statusCode = response.getStatusLine().getStatusCode();//响应状态码
        if (statusCode == 200) { //处理成功
            log.info("成功, 返回结果 = " + bodyAsString);
        } else if (statusCode == 204) { //处理成功,无返回Body
            log.info("成功");
        } else {
            log.info("查单接口调用,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
            throw new IOException("request failed");
        }

        return bodyAsString;

    } finally {
        response.close();
    }

}
2.1.4 取消支付订单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml

/**
 * 用户取消订单
 * @param orderNo
 */
@Override
public void cancelOrder(String orderNo) throws Exception {

    //调用微信支付的关单接口
    this.closeOrder(orderNo);

    //更新商户端的订单状态
    orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
}
/**
 * 关单接口的调用
 * @param orderNo
 */
private void closeOrder(String orderNo) throws Exception {

    log.info("关单接口的调用,订单号 ===> {}", orderNo);

    //创建远程请求对象
    String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
    url = wxPayConfig.getDomain().concat(url);
    HttpPost httpPost = new HttpPost(url);

    //组装json请求体
    Gson gson = new Gson();
    Map<String, String> paramsMap = new HashMap<>();
    paramsMap.put("mchid", wxPayConfig.getMchId());
    String jsonParams = gson.toJson(paramsMap);
    log.info("请求参数 ===> {}", jsonParams);

    //将请求参数设置到请求对象中
    StringEntity entity = new StringEntity(jsonParams,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpPost);

    try {
        int statusCode = response.getStatusLine().getStatusCode();//响应状态码
        if (statusCode == 200) { //处理成功
            log.info("成功200");
        } else if (statusCode == 204) { //处理成功,无返回Body
            log.info("成功204");
        } else {
            log.info("Native下单失败,响应码 = " + statusCode);
            throw new IOException("request failed");
        }

    } finally {
        response.close();
    }
}

2.2 退款

2.2.1 创建退款订单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_9.shtml

/**
 * 退款,几乎与创建支付订单一样【除了业务方法】
 * @param orderNo
 * @param reason
 * @throws IOException
 */
@Transactional(rollbackFor = Exception.class)
@Override
public void refund(String orderNo, String reason) throws Exception {

    log.info("创建退款单记录");
    //根据订单编号创建退款单
    RefundInfo refundsInfo = refundsInfoService.createRefundByOrderNo(orderNo, reason);

    log.info("调用退款API");

    //调用统一下单API
    String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
    HttpPost httpPost = new HttpPost(url);

    // 请求body参数
    Gson gson = new Gson();
    Map paramsMap = new HashMap();
    paramsMap.put("out_trade_no", orderNo);//订单编号
    paramsMap.put("out_refund_no", refundsInfo.getRefundNo());//退款单编号
    paramsMap.put("reason",reason);//退款原因
    paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址

    Map amountMap = new HashMap();
    amountMap.put("refund", refundsInfo.getRefund());//退款金额
    amountMap.put("total", refundsInfo.getTotalFee());//原订单金额
    amountMap.put("currency", "CNY");//退款币种
    paramsMap.put("amount", amountMap);

    //将参数转换成json字符串
    String jsonParams = gson.toJson(paramsMap);
    log.info("请求参数 ===> {}" + jsonParams);

    StringEntity entity = new StringEntity(jsonParams,"utf-8");
    entity.setContentType("application/json");//设置请求报文格式
    httpPost.setEntity(entity);//将请求报文放入请求对象
    httpPost.setHeader("Accept", "application/json");//设置响应报文格式

    //完成签名并执行请求,并完成验签
    CloseableHttpResponse response = wxPayClient.execute(httpPost);

    try {

        //解析响应结果
        String bodyAsString = EntityUtils.toString(response.getEntity());
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) {
            log.info("成功, 退款返回结果 = " + bodyAsString);
        } else if (statusCode == 204) {
            log.info("成功");
        } else {
            throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
        }

        //更新订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);

        //更新退款单
        refundsInfoService.updateRefund(bodyAsString);

    } finally {
        response.close();
    }
}

退款状态说明

img

2.2.2 退款结果通知

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_11.shtml

@RestController
@CrossOrigin  // 跨域
@RequestMapping("/api/wx-pay")
@Api(tags = "网站微信支付APIv3")
@Slf4j
public class WxPayController {

    @Resource
    private WxPayService wxPayService;

    @Resource
    private Verifier verifier;

    /**
     * 退款结果通知【几乎与支付通知一致,除了业务方法】
     * 退款状态改变后,微信会把相关退款结果发送给商户。
     */
    @ApiOperation("退款结果通知")
    @PostMapping("/refunds/notify")
    public String refundsNotify(HttpServletRequest request, HttpServletResponse response){

        log.info("退款通知执行");
        Gson gson = new Gson();
        Map<String, String> map = new HashMap<>();//应答对象

        try {
            //处理通知参数
            String body = HttpUtils.readData(request);
            Map<String, Object> bodyMap = gson.fromJson(body, HashMap.class);
            String requestId = (String)bodyMap.get("id");
            log.info("支付通知的id ===> {}", requestId);

            //签名的验证
            WechatPay2ValidatorForRequest wechatPay2ValidatorForRequest
                    = new WechatPay2ValidatorForRequest(verifier, requestId, body);
            if(!wechatPay2ValidatorForRequest.validate(request)){

                log.error("通知验签失败");
                //失败应答
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "通知验签失败");
                return gson.toJson(map);
            }
            log.info("通知验签成功");

            //处理退款单
            wxPayService.processRefund(bodyMap);

            //成功应答
            response.setStatus(200);
            map.put("code", "SUCCESS");
            map.put("message", "成功");
            return gson.toJson(map);

        } catch (Exception e) {
            e.printStackTrace();
            //失败应答
            response.setStatus(500);
            map.put("code", "ERROR");
            map.put("message", "失败");
            return gson.toJson(map);
        }
    }
}
package com.atguigu.service.impl;

import com.atguigu.config.WxPayConfig;
import com.atguigu.entity.OrderInfo;
import com.atguigu.entity.RefundInfo;
import com.atguigu.enums.OrderStatus;
import com.atguigu.enums.wxpay.WxApiType;
import com.atguigu.enums.wxpay.WxNotifyType;
import com.atguigu.enums.wxpay.WxRefundStatus;
import com.atguigu.enums.wxpay.WxTradeState;
import com.atguigu.service.OrderInfoService;
import com.atguigu.service.PaymentInfoService;
import com.atguigu.service.RefundInfoService;
import com.atguigu.service.WxPayService;
import com.atguigu.util.OrderNoUtils;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName: WxPayServiceImpl
 * @Author: 团子tz
 * @CreateTime: 2022/07/30 23:16
 * @Description: 想想此类的作用,写在这里吧。
 */
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundInfoService refundsInfoService;

    @Resource
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名

    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 处理退款单
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {

        log.info("退款单");

        //解密报文
        String plainText = decryptFromResource(bodyMap);

        //将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");

        if(lock.tryLock()){
            try {

                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }

                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

                //更新退款单
                refundsInfoService.updateRefund(plainText);

            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("密文解密");

        //通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        //数据密文
        String ciphertext = resourceMap.get("ciphertext");
        //随机串
        String nonce = resourceMap.get("nonce");
        //附加数据
        String associatedData = resourceMap.get("associated_data");

        log.info("密文 ===> {}", ciphertext);
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        log.info("明文 ===> {}", plainText);

        return plainText;
    }
}
2.2.3 查询退款

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_10.shtml

/**
 * 查询退款接口调用【与查询支付订单几乎一样】
 * @param refundNo
 * @return
 */
@Override
public String queryRefund(String refundNo) throws Exception {

    log.info("查询退款接口调用 ===> {}", refundNo);

    String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
    url = wxPayConfig.getDomain().concat(url);

    //创建远程Get 请求对象
    HttpGet httpGet = new HttpGet(url);
    httpGet.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpGet);

    try {
        String bodyAsString = EntityUtils.toString(response.getEntity());
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) {
            log.info("成功, 查询退款返回结果 = " + bodyAsString);
        } else if (statusCode == 204) {
            log.info("成功");
        } else {
            throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
        }

        return bodyAsString;

    } finally {
        response.close();
    }
}

2.3 账单

2.3.1 查询交易账单

(注重交易双方)提供下载URL

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_6.shtml

/**
 * 申请账单【交易账单type=tradebill,流水账单type=fundflowbill】
 * @param billDate 格式:yyyy-MM-dd 5.6日的账单记录的时间为05-06 9:00到05-07 9:00,并且在05-07 9:00后才能查到.
 * @param type
 * @return
 * @throws Exception
 */
@Override
public String queryBill(String billDate, String type) throws Exception {
    log.warn("申请账单接口调用 {}", billDate);

    String url = "";
    if("tradebill".equals(type)){
        url =  WxApiType.TRADE_BILLS.getType();
    }else if("fundflowbill".equals(type)){
        url =  WxApiType.FUND_FLOW_BILLS.getType();
    }else{
        throw new RuntimeException("不支持的账单类型");
    }

    url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

    //创建远程Get 请求对象
    HttpGet httpGet = new HttpGet(url);
    httpGet.addHeader("Accept", "application/json");

    //使用wxPayClient发送请求得到响应
    CloseableHttpResponse response = wxPayClient.execute(httpGet);

    try {

        String bodyAsString = EntityUtils.toString(response.getEntity());

        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) {
            log.info("成功, 申请账单返回结果 = " + bodyAsString);
        } else if (statusCode == 204) {
            log.info("成功");
        } else {
            throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
        }

        //获取账单下载地址
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
        return resultMap.get("download_url");

    } finally {
        response.close();
    }
}
2.3.2 查询流水账单

(只注重商户)提供下载URL

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml

2.3.3 获取账单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml

/**
 * 下载账单 【查询账单所提供的URL由前端直接访问是无效的,必须由后端代劳】
 * @param billDate
 * @param type
 * @return
 * @throws Exception
 */
@Override
public String downloadBill(String billDate, String type) throws Exception {
    log.warn("下载账单接口调用 {}, {}", billDate, type);

    //获取账单url地址
    String downloadUrl = this.queryBill(billDate, type);
    //创建远程Get 请求对象
    HttpGet httpGet = new HttpGet(downloadUrl);
    httpGet.addHeader("Accept", "application/json");

    //使用wxPayClient发送请求得到响应
    CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

    try {

        String bodyAsString = EntityUtils.toString(response.getEntity());

        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) {
            log.info("成功, 下载账单返回结果 = " + bodyAsString);
        } else if (statusCode == 204) {
            log.info("成功");
        } else {
            throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
        }

        return bodyAsString;

    } finally {
        response.close();
    }
}
Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
    return resultMap.get("download_url");

} finally {
    response.close();
}

}




#### 2.3.2 查询流水账单 

(只注重商户)提供下载URL

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_7.shtml



#### 2.3.3 获取账单

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_8.shtml

```java
/**
 * 下载账单 【查询账单所提供的URL由前端直接访问是无效的,必须由后端代劳】
 * @param billDate
 * @param type
 * @return
 * @throws Exception
 */
@Override
public String downloadBill(String billDate, String type) throws Exception {
    log.warn("下载账单接口调用 {}, {}", billDate, type);

    //获取账单url地址
    String downloadUrl = this.queryBill(billDate, type);
    //创建远程Get 请求对象
    HttpGet httpGet = new HttpGet(downloadUrl);
    httpGet.addHeader("Accept", "application/json");

    //使用wxPayClient发送请求得到响应
    CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

    try {

        String bodyAsString = EntityUtils.toString(response.getEntity());

        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == 200) {
            log.info("成功, 下载账单返回结果 = " + bodyAsString);
        } else if (statusCode == 204) {
            log.info("成功");
        } else {
            throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
        }

        return bodyAsString;

    } finally {
        response.close();
    }
}

支付宝支付

1. 准备工作

1.1 获取支付宝授权参数

正式环境:https://opendocs.alipay.com/open/03lo17

  • 创建应用
  • 绑定应⽤
  • 配置秘钥
  • 上线应⽤
  • 签约功能

沙箱环境:https://open.alipay.com/develop/sandbox/app

支付宝已经为每个开通沙箱环境的用户提供了各种参数和账号以供测试。

1.2 编程准备

1.2.1 引入依赖
<!--支付宝 SDK-->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.31.72.ALL</version>
</dependency>
1.2.2 配置和配置类

基本配置

# 支付宝支付相关参数

# 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
alipay.app-id=xxxxxxxxxxxxxxx

# 商户PID,卖家支付宝账号ID
alipay.seller-id=xxxxxxxxxxxxxxx

# 支付宝网关,此处是沙箱环境的支付宝网关
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do

# 商户私钥,您的PKCS8格式RSA2私钥
alipay.merchant-private-key=xxxxxxxxxxxxxxx

# 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥
alipay.alipay-public-key=xxxxxxxxxxxxxxx

# 接口内容加密秘钥,对称秘钥
alipay.content-key=xxxxxxxxxxxxxxx

# 页面跳转同步通知页面路径,告知支付宝交易成功后,前端需要跳转的页面
alipay.return-url=http://localhost:8080/#/success

# 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
# 注意:每次重新启动ngrok,都需要根据实际情况修改这个配置
alipay.notify-url=https://5676m99s35.goho.co/api/ali-pay/trade/notify

基本配置类

@Configuration
@PropertySource("classpath:alipay-sandbox.properties")//加载配置文件
public class AlipayClientConfig {
    @Resource
    private Environment config;

    @Bean
    public AlipayClient alipayClient() throws AlipayApiException {

        AlipayConfig alipayConfig = new AlipayConfig();

        //设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
        //设置应用Id
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
        //设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
        //设置请求格式,固定值json
        alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
        //设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
        //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
        //设置签名类型
        alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
        //构造client
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);

        return alipayClient;
    }
}

2. 使用

2.1 支付

2.1.1 创建支付

https://opendocs.alipay.com/apis/028r8t?scene=22

public String tradeCreate(Long productId) {
    try {
        //生成订单
        log.info("生成订单");
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);

        //调用支付宝接口
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        //配置需要的公共请求参数
        //支付完成后,支付宝向谷粒学院发起的异步通知地址
        request.setNotifyUrl(config.getProperty("alipay.notify-url"));
        //支付完成后,我们想让页面跳转回谷粒学院的页面,配置returnUrl
        request.setReturnUrl(config.getProperty("alipay.return-url"));

        //组装当前业务方法的请求参数
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderInfo.getOrderNo());
        BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal("100"));//元
        bizContent.put("total_amount", total);
        bizContent.put("subject", orderInfo.getTitle());
        bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
        //bizContent.put("time_expire", "2022-08-01 22:00:00");

        request.setBizContent(bizContent.toString());

        AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
            return response.getBody();
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            throw new RuntimeException("创建支付交易失败");
        }
    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("创建支付交易失败");
    }
}
2.1.2 支付通知

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

@ApiOperation("支付通知")
@PostMapping("/trade/notify")
public String tradeNotify(@RequestParam Map<String, String> params){

    log.info("支付通知正在执行");
    log.info("通知参数 ===> {}", params);

    String result = "failure";

    try {
        //异步通知验签
        boolean signVerified = AlipaySignature.rsaCheckV1(
            params,
            config.getProperty("alipay.alipay-public-key"),
            AlipayConstants.CHARSET_UTF8,
            AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名

        if(!signVerified){
            //验签失败则记录异常日志,并在response中返回failure.
            log.error("支付成功异步通知验签失败!");
            return result;
        }

        // 验签成功后
        log.info("支付成功异步通知验签成功!");

        //按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,
        //1 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号
        String outTradeNo = params.get("out_trade_no");
        OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);
        if(order == null){
            log.error("订单不存在");
            return result;
        }

        //2 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)
        String totalAmount = params.get("total_amount");
        int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal("100")).intValue();
        int totalFeeInt = order.getTotalFee().intValue();
        if(totalAmountInt != totalFeeInt){
            log.error("金额校验失败");
            return result;
        }

        //3 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方
        String sellerId = params.get("seller_id");
        String sellerIdProperty = config.getProperty("alipay.seller-id");
        if(!sellerId.equals(sellerIdProperty)){
            log.error("商家pid校验失败");
            return result;
        }

        //4 验证 app_id 是否为该商户本身
        String appId = params.get("app_id");
        String appIdProperty = config.getProperty("alipay.app-id");
        if(!appId.equals(appIdProperty)){
            log.error("appid校验失败");
            return result;
        }

        //在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS时,
        // 支付宝才会认定为买家付款成功。
        String tradeStatus = params.get("trade_status");
        if(!"TRADE_SUCCESS".equals(tradeStatus)){
            log.error("支付未成功");
            return result;
        }

        //处理业务 修改订单状态 记录支付日志
        aliPayService.processOrder(params);

        //校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
        result = "success";
    } catch (AlipayApiException e) {
        e.printStackTrace();
    }
    return result;
}
2.1.3 查询支付

https://opendocs.alipay.com/open/028woa

public String queryOrder(String orderNo) {
    try {
        log.info("查单接口调用 ===> {}", orderNo);

        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderNo);
        request.setBizContent(bizContent.toString());

        AlipayTradeQueryResponse response = alipayClient.execute(request);
        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
            return response.getBody();
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            //throw new RuntimeException("查单接口的调用失败");
            return null;//订单不存在
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("查单接口的调用失败");
    }
}
2.1.4 取消支付

https://opendocs.alipay.com/open/028wob

private void closeOrder(String orderNo) {
    try {
        log.info("关单接口的调用,订单号 ===> {}", orderNo);

        AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderNo);
        request.setBizContent(bizContent.toString());
        AlipayTradeCloseResponse response = alipayClient.execute(request);

        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            //throw new RuntimeException("关单接口的调用失败");
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("关单接口的调用失败");
    }
}

2.2 退款

流程图

img

2.2.1 创建退款

https://opendocs.alipay.com/open/028sm9

public void refund(String orderNo, String reason) {
    try {
        log.info("调用退款API");

        //创建退款单
        RefundInfo refundInfo = refundsInfoService.createRefundByOrderNoForAliPay(orderNo, reason);

        //调用统一收单交易退款接口
        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest ();

        //组装当前业务方法的请求参数
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderNo);//订单编号
        BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));
        //BigDecimal refund = new BigDecimal("2").divide(new BigDecimal("100"));
        bizContent.put("refund_amount", refund);//退款金额:不能大于支付金额
        bizContent.put("refund_reason", reason);//退款原因(可选)

        request.setBizContent(bizContent.toString());

        //执行请求,调用支付宝接口
        AlipayTradeRefundResponse response = alipayClient.execute(request);

        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

            //更新退款单
            refundsInfoService.updateRefundForAliPay(
                refundInfo.getRefundNo(),
                response.getBody(),
                AliPayTradeState.REFUND_SUCCESS.getType()); //退款成功
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());

            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);

            //更新退款单
            refundsInfoService.updateRefundForAliPay(
                refundInfo.getRefundNo(),
                response.getBody(),
                AliPayTradeState.REFUND_ERROR.getType()); //退款失败
        }


    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("创建退款申请失败");
    }
}
2.2.1 查询退款

https://opendocs.alipay.com/open/028sma

public String queryRefund(String orderNo) {
    try {
        log.info("查询退款接口调用 ===> {}", orderNo);

        AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", orderNo);
        bizContent.put("out_request_no", orderNo);
        request.setBizContent(bizContent.toString());

        AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);
        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());
            return response.getBody();
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            //throw new RuntimeException("查单接口的调用失败");
            return null;//订单不存在
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("查单接口的调用失败");
    }
}

2.3 账单

https://opendocs.alipay.com/open/028woc

2.3.1 查询账单下载URL
public String queryBill(String billDate, String type) {
    try {

        AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("bill_type", type);
        bizContent.put("bill_date", billDate);
        request.setBizContent(bizContent.toString());
        AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);

        if(response.isSuccess()){
            log.info("调用成功,返回结果 ===> " + response.getBody());

            //获取账单下载地址
            Gson gson = new Gson();
            HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);
            LinkedTreeMap billDownloadurlResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");
            String billDownloadUrl = (String)billDownloadurlResponse.get("bill_download_url");

            return billDownloadUrl;
        } else {
            log.info("调用失败,返回码 ===> " + response.getCode() + ", 返回描述 ===> " + response.getMsg());
            throw new RuntimeException("申请账单失败");
        }

    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new RuntimeException("申请账单失败");
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值