RcbOnlineHttpClient.java 17.3 KB
 package com.diligrp.cashier.pipeline.client;

 import com.diligrp.cashier.pipeline.domain.*;
 import com.diligrp.cashier.pipeline.exception.PaymentPipelineException;
 import com.diligrp.cashier.pipeline.type.OutPaymentType;
 import com.diligrp.cashier.pipeline.type.PaymentState;
 import com.diligrp.cashier.pipeline.util.RcbSignatureUtils;
 import com.diligrp.cashier.pipeline.util.RcbStateUtils;
 import com.diligrp.cashier.shared.ErrorCode;
 import com.diligrp.cashier.shared.exception.ServiceAccessException;
 import com.diligrp.cashier.shared.service.ServiceEndpointSupport;
 import com.diligrp.cashier.shared.util.DateUtils;
 import com.diligrp.cashier.shared.util.JsonUtils;
 import com.diligrp.cashier.shared.util.ObjectUtils;
 import com.diligrp.cashier.shared.util.RandomUtils;
 import com.fasterxml.jackson.core.type.TypeReference;
 import jakarta.annotation.Resource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.redis.core.RedisOperations;
 import org.springframework.data.redis.core.SessionCallback;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.lang.NonNull;

 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
 import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.TimeUnit;

 /**
 * 安徽农商银行聚合支付客户端
 */
public class RcbOnlineHttpClient extends ServiceEndpointSupport {

    private static final Logger LOG = LoggerFactory.getLogger(RcbOnlineHttpClient.class);

    // 微信API BASE URL
    private static final String WECHAT_BASE_URL = "https://api.weixin.qq.com";
    // code2session接口: 根据登录凭证code获取登录信息
    private static final String CODE_TO_SESSION = "/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";

    private static final int STATUS_OK = 200;

    private final String uri;

    private final String merchantNo;

    private final String terminalNo;

    private final String key;

    private final String appId;

     @Resource
    private StringRedisTemplate stringRedisTemplate;

    public RcbOnlineHttpClient(String uri, String merchantNo, String terminalNo, String key, String appId) {
        this.uri = uri;
        this.merchantNo = merchantNo;
        this.terminalNo = terminalNo;
        this.key = key;
        this.appId = appId;
    }

     public String sendMiniProPrepayRequest(MiniProPrepayRequest request, String notifyUrl) {
         Map<String, String> params = new HashMap<>();
         params.put("merchantNo", merchantNo);
         params.put("terminalNo", terminalNo);
         params.put("batchNo", getBatchNo());
         params.put("traceNo", getTraceNo());
         params.put("outTradeNo", request.getPaymentId());
         params.put("transAmount", String.valueOf(request.getAmount()));
         params.put("appid", appId);
         params.put("openId", request.getOpenId());
         params.put("timeExpire", "5"); // 5分钟
         params.put("tradeChannel", "01"); // 微信小程序
         params.put("notifyUrl", notifyUrl);
         params.put("nonceStr", RandomUtils.randomString(32));
         params.put("sign", RcbSignatureUtils.sign(params, key));

         String payload = JsonUtils.toJsonString(params);
         LOG.debug("Sending rcb minipro prepay request: {}", payload);
         HttpResult result = send(uri + "/cposp/pay/unifiedorder", payload);
         if (result.statusCode != STATUS_OK) {
             LOG.error("Failed to send rcb minipro prepay, statusCode: {}", result.statusCode);
             throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "调用小程序预支付接口失败: " + result.statusCode);
         }
         LOG.debug("Received rcb mini pro prepay response: {}", result.responseText);

         Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
         String resultCode = data.get("resultCode");
         String resultMessage =  data.get("resultMessage");
         if ("00".equals(resultCode)) {
             String signature = data.remove("sign");
             if (!RcbSignatureUtils.verify(data, key, signature)) {
                 throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "小程序预支付请求数据验签失败");
             }
             return data.get("payInfo");
         } else {
             LOG.error("Failed to send minipro prepay request, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
             throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用小程序预支付接口失败: " + resultMessage);
         }
     }

    public OnlinePaymentResponse queryPrepayResponse(OnlinePrepayOrder request) {
        Map<String, String> params = new LinkedHashMap<>();
        params.put("merchantNo", merchantNo);
        params.put("terminalNo", terminalNo);
        params.put("batchNo", getBatchNo());
        params.put("traceNo", getTraceNo());
        params.put("outTradeNo", request.getPaymentId());
        params.put("nonceStr", RandomUtils.randomString(32));

        params.put("sign", RcbSignatureUtils.sign(params, key));
        String payload = JsonUtils.toJsonString(params);
        LOG.debug("Sending rcb order query request: {}", payload);
        HttpResult result = send(uri + "/cposp/pay/orderQuery", payload);
        if (result.statusCode != STATUS_OK) {
            LOG.error("Failed to query rcb order, statusCode: {}", result.statusCode);
            throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "查询支付状态失败: " + result.statusCode);
        }
        LOG.debug("Received rcb order query response: {}", result.responseText);

        Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
        String signature = data.remove("sign");
        if (!RcbSignatureUtils.verify(data, key, signature)) {
            throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "查询支付状态数据验签失败");
        }

        String resultCode = data.get("resultCode");
        String resultMessage =  data.get("resultMessage");
        if ("00".equals(resultCode)) {
            String orderStatus = data.get("orderStatus");
            String errorDesc = data.get("errorDesc");
            String outTradeNo = data.get("cposOrderId");
            OutPaymentType paymentType = RcbStateUtils.outPayType(data.get("tradeChannel"));
            LocalDateTime when = LocalDateTime.now().withNano(0);
            // 不能使用paidTime或transTime, paidTime只有年月日,transTime是订单发起时间
            // 当前未设计存储支付时间字段(采用记录修改时间作为支付时间),因此采用当前时间作为记录修改时间
            String timeEnd = data.get("timeEnd");
            if (ObjectUtils.isNotEmpty(timeEnd)) {
                long timestamp = Long.parseLong(timeEnd); // ⽀付完成时间戳
                Instant instant = Instant.ofEpochMilli(timestamp);
                when = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
            }
            String payUserInfo = data.get("payUserInfo");
            // 第三方支付通道的订单编号,比如:微信订单
            // String outOrderId = data.get("chnOrderId");
            return new OnlinePaymentResponse(request.getPaymentId(), outTradeNo, paymentType,
                payUserInfo, when, RcbStateUtils.paymentState(orderStatus), errorDesc);
        } else {
            LOG.error("Failed to query rcb order, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
            throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "调用支付结果查询接口失败");
        }
    }

     public void closePrepayOrder(OnlinePrepayOrder request) {
         Map<String, String> params = new HashMap<>();
         params.put("merchantNo", merchantNo);
         params.put("terminalNo", terminalNo);
         params.put("batchNo", getBatchNo());
         params.put("traceNo", getTraceNo());
         params.put("outTradeNo", request.getPaymentId());
         params.put("nonceStr", RandomUtils.randomString(32));
         params.put("sign", RcbSignatureUtils.sign(params, key));

         String payload = JsonUtils.toJsonString(params);
         LOG.info("Sending close rcb order request: {}", payload);
         HttpResult result = send(uri + "/cposp/pay/closeOrder", payload);
         if (result.statusCode != STATUS_OK) {
             LOG.error("Failed to close rcb order, statusCode: {}", result.statusCode);
             throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "关闭支付订单失败: " + result.statusCode);
         }
         LOG.info("Received close rcb order response: {}", result.responseText);

         Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
         String resultCode = data.get("resultCode");
         String resultMessage =  data.get("resultMessage");
         if (!"00".equals(resultCode)) {
             LOG.error("Failed to close rcb order, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
             throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "关闭支付订单失败");
         }

         String signature = data.remove("sign");
         if (!RcbSignatureUtils.verify(data, key, signature)) {
             throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "关闭订单数据验签失败");
         }
     }

    public OnlineRefundResponse sendRefundRequest(OnlineRefundRequest request) {
        Map<String, String> params = new HashMap<>();
        params.put("merchantNo", merchantNo);
        params.put("terminalNo", terminalNo);
        params.put("batchNo", getBatchNo());
        params.put("traceNo", getTraceNo());
        params.put("mchtRefundNo", request.getRefundId());
        params.put("outTradeNo", request.getPaymentId());
        params.put("refundAmount", String.valueOf(request.getAmount()));
        params.put("nonceStr", RandomUtils.randomString(32));
        params.put("sign", RcbSignatureUtils.sign(params, key));

        LocalDateTime now = LocalDateTime.now();
        String payload = JsonUtils.toJsonString(params);
        LOG.debug("Sending refund request: {}", payload);
        HttpResult result = send(uri + "/cposp/pay/refund", payload);
        if (result.statusCode != STATUS_OK) {
            LOG.error("Failed to refund rcb order, statusCode: {}", result.statusCode);
            throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "发送退款请求失败: " + result.statusCode);
        }
        LOG.debug("Received rcb order refund response: {}", result.responseText);

        Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
        String resultCode = data.get("resultCode");
        String resultMessage =  data.get("resultMessage");
        if (!"00".equals(resultCode)) {
            LOG.error("Failed to refund, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
            throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "发送退款请求失败");
        }

        String signature = data.remove("sign");
        if (!RcbSignatureUtils.verify(data, key, signature)) {
            throw new PaymentPipelineException(ErrorCode.OPERATION_NOT_ALLOWED, "退款接口数据验签失败");
        }
        String refundStatus = data.get("refundStatus");
        String cposRefundOrderId = data.get("cposRefundOrderId");
        PaymentState refundState = RcbStateUtils.refundState(refundStatus);
        return new OnlineRefundResponse(request.getRefundId(), cposRefundOrderId, now, refundState, RcbStateUtils.refundInfo(refundState));
    }

    /**
     * 各交易终端,每⽇第⼀笔交易时,需要通过签到,获取批次号等信息。
     *
     * @return 批次号
     */
    protected String getBatchNo() {
        try {
            String key = "rcb:online:batchNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
            String batchNo = stringRedisTemplate.opsForValue().get(key);
            if (batchNo != null) {
                return batchNo;
            }

            String payload = String.format("{\"merchantNo\": \"%s\", \"terminalNo\": \"%s\"}", merchantNo, terminalNo);
            LOG.debug("Sending signIn request: {}", payload);
            HttpResult result = send(uri + "/cposp/pay/signIn", payload);
            if (result.statusCode != STATUS_OK) {
                LOG.error("Failed to get rcb batch no, statusCode: {}", result.statusCode);
                throw new PaymentPipelineException(ErrorCode.SYSTEM_UNKNOWN_ERROR, "获取签到批次号: " + result.statusCode);
            }
            LOG.debug("Received rcb signIn response: {}", result.responseText);

            Map<String, String> data = JsonUtils.fromJsonString(result.responseText, new TypeReference<>() {});
            String resultCode = data.get("resultCode");
            String resultMessage =  data.get("resultMessage");
            if ("00".equals(resultCode)) {
                batchNo = data.get("batchNo");
                stringRedisTemplate.opsForValue().set(key, batchNo, 36 * 60 * 60, TimeUnit.SECONDS);
                return batchNo;
            } else {
                LOG.error("Failed to rcb sign in, errorCode: {}, resultMessage: {}", resultCode, resultMessage);
                throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "签到接口调用失败");
            }
        } catch (ServiceAccessException | PaymentPipelineException rex) {
            throw rex;
        } catch (Exception ex) {
            LOG.error("Failed to get rcb sign in batchNo", ex);
            throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取签到批次号失败");
        }
    }

    protected String getTraceNo() {
        try {
            String key = "rcb:online:traceNo" + DateUtils.formatDate(LocalDate.now(), DateUtils.YYYYMMDD);
            StringBuilder traceNo = new StringBuilder();

            List<Object> results = stringRedisTemplate.executePipelined(new SessionCallback<Object>() {
                @Override
                public Object execute(@NonNull RedisOperations operations) throws DataAccessException {
                    operations.opsForValue().increment(key);
                    operations.expire(key, 36 * 60 * 60, TimeUnit.SECONDS);
                    return null;
                }
            });

            traceNo.append(results.getFirst());
            int length = traceNo.length();
            if (length < 6) {
                for (int i = length; i < 6; i++) {
                    traceNo.insert(0, "0");
                }
            }
            return traceNo.toString();
        } catch (Exception ex) {
            LOG.error("Failed to get traceNo", ex);
            throw new PaymentPipelineException(ErrorCode.SERVICE_ACCESS_ERROR, "获取系统跟踪号失败");
        }
    }

     protected Optional<javax.net.ssl.SSLContext> buildSSLContext() {
         SSLContext sslContext = null;
         try {
             TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[] {
                 new X509TrustManager() {
                     @Override
                     public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
                     }

                     @Override
                     public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
                     }

                     @Override
                     public X509Certificate[] getAcceptedIssuers() {
                         return new X509Certificate[0];
                     }
                 }
             };
             sslContext = SSLContext.getInstance("SSL");
             sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
         } catch (Exception ex) {
             LOG.error("Build SSLContext failed", ex);
         }
         return Optional.ofNullable(sslContext);
     }

    private static class AuthorizationSession {
        // 用户唯一标识
        private String openid;
        // 会话密钥
        private String session_key;
        // 用户在开放平台的唯一标识
        private String unionid;
        // 错误码
        private Integer errcode;
        // 错误信息
        private String errmsg;

        public String getOpenid() {
            return openid;
        }

        public void setOpenid(String openid) {
            this.openid = openid;
        }

        public String getSession_key() {
            return session_key;
        }

        public void setSession_key(String session_key) {
            this.session_key = session_key;
        }

        public String getUnionid() {
            return unionid;
        }

        public void setUnionid(String unionid) {
            this.unionid = unionid;
        }

        public Integer getErrcode() {
            return errcode;
        }

        public void setErrcode(Integer errcode) {
            this.errcode = errcode;
        }

        public String getErrmsg() {
            return errmsg;
        }

        public void setErrmsg(String errmsg) {
            this.errmsg = errmsg;
        }
    }
}