preface

This week, I was responsible for integrating APP payment function at the server side. The payment channels were wechat APP Payment and Alipay APP payment, and spring Cloud technology stack was adopted at the back end. The startup container uses the Netty container built in Spring Boot WebFlux. Write this article is mainly to record their step pit, later encountered again can save a lot of time.

The signature of the

The signing logic is to hash the information in the request body using a hash algorithm to get a string of hash values before the request is sent to alipay or wechat Pay servers. This string of hash values is irreversible, but the hash value of the same request body is the same. In this way, the information in the request body can be protected from malicious tampering, because once tampered, the hash value will definitely be inconsistent.

attestation

If the purpose of a signature is to prevent a request from being maliciously tampered with, then a signature check is to confirm that the request has been maliciously tampered with. The logic of checking is basically the same as signing. As long as the signature obtained by checking is the same as the signature obtained by signing, the request can be trusted.

Good news and bad news

The good news is that Alipay provides the server SDK, and most of the details have been packaged into the SDK.

The bad news is that wechat Pay currently does not provide the server SDK, and all the details such as signing verification, sending requests and receiving return values of requests need to be undertaken by the server developer.

Alipay

Alipay has a very perfect development documents, as well as integration examples, integrated experience is the best, and Alipay has real online technical advice, real! Real person! Real person!

Due to the limited space, users are required to read the official documents for the functions of creating, launching and signing applications. Here are some things that have a lot to do with development.

The key configuration

Alipay open platform supports developers to use common public key and public key certificate two signature methods, just choose one of the two. Please refer to the official documents for specific operations.

The next step is how public key certificates are signed,

Important configuration

Complete the key configuration in the public key certificate mode, you can obtain the important configuration for development

  • APPID (created app)
  • Apply a private key
  • Applying a Public Key Cert
  • Alipay Public Key
  • Alipay Public Key Cert
  • Alipay Root Cert

APP Payment process

There are detailed documents on the official website of APP Payment. Readers are advised to read the primary information on the official website. Here, the author will only mention important integration and development information.

Before integration, it is necessary to understand the way of accessing alipay payment and the architecture suggestions

It can be seen that “order” is required before payment, and the merchant background calls the alipay background ordering interface to generate payment order information; For the payment action, the merchant APP first invokes the Alipay APP, and then the Alipay APP is used for payment. After the payment is completed, the alipay payment background will notify the payment result (synchronous notification) to the APP, and also send asynchronous notification to the merchant background.

This is a flow chart of cross-functional system interaction

Place the order

Placing an order is the premise of APP payment. In addition to the business meaning of placing an order, the main problem it solves is to ensure the security of the payment process, by means of signing, so as to avoid payment information being secretly tampered with during the payment process.

Task list:

  1. configuration
    • Four public key certificates (required for Alipay interface)
    • Application of private key (required for verification)
    • Check-in algorithm (RSA1 and RSA2, suggested RSA2)
    • Interface for asynchronously notifying the public network of payment results (notify_url)
    • For other configurations, see the official documentation
  2. Build AlipayClient
    • Build alipay certificate requestCertAlipayRequest.java
    • Build the default Alipay clientnew DefaultAlipayClient(certAlipayRequest)
    • Build the request parameters for the single interfaceAlipayTradeAppPayRequest.javaAlipayTradeAppPayModel.java, the latter corresponds to the biz_content parameter of the order interface, which has mandatory parameters (amount and order number are important) and optional parameters (passbackParams needs to be paid attention to), please refer to the official document.
    • Initiate an order requestalipayClient.sdkExecute(request)
    • useAlipayTradeAppPayResponse.javaAs the return value of an order request
  3. Merchant business logic code.
  4. Write a test to simulate APP ordering

Part of the code is as follows:

@Test
void should_return_alipay_place_order_info(a) throws Exception {
    // given
    PlaceOrderCommand command = PlaceOrderCommand.builder()
            .userId(1L)
            .userName("Zhang")
            .orderNo("123456789")
            .goodId(1L)
            .subject("Coat")
            .totalAmount(100)
            .payChannel(Payment.PayChannel.ALIPAY)
            .build();
    // when
    WebTestClient.ResponseSpec responseSpec = this.post("/orders", command);
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(new ParameterizedTypeReference<PlaceOrderResult<Object>>() {})
            .value(actualPlaceOrderInfo -> {
                assertThat(actualPlaceOrderInfo.getId()).isNotNull();
                assertThat(actualPlaceOrderInfo.getOrderInfo()).isNotBlank();
                assertThat(actualPlaceOrderInfo.getPayChannel()).isEqualTo(Payment.PayChannel.ALIPAY);
                // other assert ...
            })
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
}

/** * Place an order (generate a payment order) **@return* /
@PostMapping("/orders")
public Mono<PlaceOrderResult> placeOrder(@RequestBody PlaceOrderCommand command) {
    command.verify();
    if (command.isAlipay()) {
        return Mono.just(this.alipayService.placeOrder(command));
    }
    throw new PayException("Not open for the time being.");
}

@Slf4j
@Component
public class AlipayClient {

    /** * Alipay signature algorithm */
    private static final String SIGN_TYPE = "RSA2";
    /** * Alipay client */
    private final com.alipay.api.AlipayClient alipayClient;
    private final AlipayProperties alipayProperties;

    public AlipayClient(AlipayProperties alipayProperties) throws AlipayApiException {
        this.alipayProperties = alipayProperties;
        this.alipayClient = new DefaultAlipayClient(this.createCertAlipayRequest());
    }

    /** ** ** *@param command
     * @paramPaymentId Payment information ID *@return
     * @throws AlipayApiException
     */
    public AlipayTradeAppPayResponse placeOrder(PlaceOrderCommand command, Long paymentId) throws AlipayApiException {
        PassBackParam passBackParam = new PassBackParam();
        passBackParam.setPaymentId(paymentId);
        // Instantiate the request class corresponding to the specific API. The class name corresponds to the interface name. The current call interface name is alipay
        AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
        // The SDK has wrapped the public parameters, so you just need to pass in the business parameters. The following method is the MODEL input method of SDK (biz_content if model and biz_content exist at the same time).
        AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
        model.setSubject(command.getSubject());
        model.setOutTradeNo(command.getOrderNo());
        // Set the expiration time
        model.setTimeoutExpress(this.alipayProperties.getTimeoutExpress());
        // The unit of the input amount of the interface is minute, and the unit of the unified single order interface of Alipay is yuan
        model.setTotalAmount(command.getTotalAmount().toString());
        model.setProductCode("QUICK_MSECURITY_PAY");
        model.setPassbackParams(JSON.toJSONString(passBackParam));
        request.setBizModel(model);
        request.setNotifyUrl(this.alipayProperties.getNotifyUrl());
        // Instead of a normal interface call, sdkExecute is used
        return this.alipayClient.sdkExecute(request);
    }

    private CertAlipayRequest createCertAlipayRequest(a) {
        //构造client
        CertAlipayRequest certAlipayRequest = new CertAlipayRequest();
        // Set the gateway address
        certAlipayRequest.setServerUrl(this.alipayProperties.getServerUrl());
        // Set the application Id
        certAlipayRequest.setAppId(this.alipayProperties.getAppId());
        // Set application private key -> Configure key
        certAlipayRequest.setPrivateKey(this.alipayProperties.getPrivateKey());
        // Set the request format, fixed value json
        certAlipayRequest.setFormat("json");
        // Set the character set
        certAlipayRequest.setCharset(StandardCharsets.UTF_8.name());
        // Set the signature type
        certAlipayRequest.setSignType(SIGN_TYPE);
        // Set the path of the applied public key certificate
        certAlipayRequest.setCertPath(this.alipayProperties.getAppCertPublicKey());
        // Set the alipay public key certificate path
        certAlipayRequest.setAlipayPublicCertPath(this.alipayProperties.getAlipayCertPublicKey_RSA2());
        // Set the alipay root certificate path
        certAlipayRequest.setRootCertPath(this.alipayProperties.getAlipayRootCert());
        returncertAlipayRequest; }}@Slf4j
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Component
@ConfigurationProperties("pay.alipay")
public class AlipayProperties {

    /**
     * 支付宝地址
     */
    private String serverUrl;
    /** * merchant id */
    private String appId;
    /** * apply the private key */
    private String privateKey;
    /** * Physical absolute path to apply the public key certificate */
    private String appCertPublicKey;
    /** * Alipay public key certificate physical absolute path */
    private String alipayCertPublicKey_RSA2;
    /** * Alipay root certificate physical absolute path */
    private String alipayRootCert;
    /** * pay callback notification URL */
    private String notifyUrl;
    /** * Payment order expiration time */
    private String timeoutExpress;

    public String getAlipayCertPublicKey_RSA2(a) {
        return FileUtils.getAbsolutePath(this.alipayCertPublicKey_RSA2);
    }

    public String getAlipayRootCert(a) {
        return FileUtils.getAbsolutePath(this.alipayRootCert);
    }

    public String getAppCertPublicKey(a) {
        return FileUtils.getAbsolutePath(this.appCertPublicKey); }}@Slf4j
public class FileUtils {

    /** * If the file prefix contains /, the file is located in the operating system file system. ** If the file prefix does not contain /, the file is located in the resources directory
    private static final String PREFIX = "/";

    public static String getAbsolutePath(String filePath) {
        if (filePath.startsWith(PREFIX)) {
            return filePath;
        }
        try {
            return ResourceUtils.getFile("classpath:" + filePath).getAbsolutePath();
        } catch (Throwable e) {
            log.error("File {} not found", filePath);
            log.error("Can't find file", e);
            return null; }} pay: alipay: # alipay address server-url://openapi.alipay.com/gateway.do# app ID app-id: # app private keyprivate-key: # App-cert - specifies the physical absolute path to apply the public key certificatepublic-key: # Alipay public key certificate physical absolute path Alipay -cert-public-key_rsa2: # alipay root certificate physical absolute path alipay root certificate physical absolute path alipay-root-cert: # payment result asynchronous notification url notify-url: # payment order invalid timeout-express:Copy the code

The logic of ordering is very simple, and as long as you carefully check the configuration of the four public key certificates and the application private key, you are basically fine.

If you use a public key certificate, pay special attention to one detail during key configuration. The CSR file generated by the key generation tool downloaded from the official website is not an application public key. All four public key certificates are downloaded through the platform, including the application private key.

Payment result callback notification

If the APP requests the ordering interface or can arouse Alipay and pay successfully, it indicates that the public key certificate and signature method are correct. At this time, the merchant APP can obtain the payment result synchronously, such as telling the user the payment is successful.

Generally, for the sake of security, we do not use the payment result of the client, but the information of the payment result notification initiated by the background of Alipay as the proof of payment. That’s number 12, number 13.

According to the official documentation, this interface is an HTTP POST request. For details about parameters, see the official documentation.

Task list:

  1. Develop an interface to receive the callback of Alipay background. The interface is notify_URL filled in when placing an order.
  2. attestation
  3. Develop merchant business logic
  4. The interface returns either SUCCESS (callback and validation succeeded) or failure (code validation failed).
  5. Write a test to simulate the Alipay callback
@Test
void should_received_payment_success_from_alipay(a) throws Exception {
    // given
    Payment payment = Payment.builder()
            .buyerId(1L)
            .buyerName("Tester")
            .state(Payment.State.WAIT_BUYER_PAY)
            .orderNo("6823789339978248")
            .payChannel(Payment.PayChannel.ALIPAY)
            .payMoney(100L)
            .build();
    this.paymentRepository.save(payment);

    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    body.add("app_id"."123123123123");
    body.add("subject"."Coat");
    body.add("notify_type"."trade_status_sync");
    body.add("notify_id"."ac05099524730693a8b330c5ecf72da9786");
    body.add("charset"."utf-8");
    body.add("version"."1.0");
    body.add("sign_type"."RSA2");
    body.add("sign", UUID.randomUUID().toString().replaceAll("-".""));
    body.add("trade_no"."2013112011001004330000121536");
    body.add("out_trade_no"."6823789339978248");
    body.add("trade_status", TradeStatus.TRADE_SUCCESS.toString());
    body.add("gmt_close"."The 2019-10-10 10:10:10");
    body.add("notify_time"."The 2019-10-10 10:10:10");
    body.add("gmt_payment"."The 2019-10-10 10:10:10");
    body.add("gmt_create"."The 2019-10-10 10:10:10");
    body.add("buyer_pay_amount"."0.01");
    body.add("amount"."0.01");
    body.add("receipt_amount"."0.01");
    body.add("total_amount"."0.01");
    body.add("fund_bill_list"."[{\" amount \ ": \" 0.01 \ ", \ "fundChannel \" : \ "ALIPAYACCOUNT \}"]");
    body.add("passback_params"."{\"paymentId\":\"" + payment.getId() + "\"}");

    given(this.alipayNotifyHandler.verifySignature(anyMap())).willReturn(true);
    // when
    WebTestClient.ResponseSpec responseSpec = this.client.post()
            .uri("/alipay/notifications/payment")
            .body(BodyInserters.fromFormData(body))
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
            .exchange();
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody();
    Payment actualPayment = this.paymentRepository.findById(payment.getId()).get();
    assertThat(actualPayment.getState()).isEqualTo(Payment.State.SUCCESS);
    AlipayTransaction alipayTransaction = this.alipayTransactionRepository.findByPaymentId(payment.getId()).get();
    assertThat(alipayTransaction.getTradeStatus()).isEqualTo(TradeStatus.TRADE_SUCCESS);
    verify(this.orderClient, times(1)).notifyPaid(any());
    // other assert
}
Copy the code

It should be noted that the Content-type of Alipay background callback interface is Application/X-www-form-urlencoded.

/** * Alipay payment result change callback interface **@param requestParams
 */
@PostMapping(value = "/alipay/notifications/payment")
public String receiveAlipayNotify(@RequestBody MultiValueMap<String, String> requestParams) {
    Map<String, String> params = this.asMap(requestParams);
    if (!this.alipayNotifyHandler.verifySignature(params)) {
        log.warn("Alipay payment result signature verification failed {}", JSON.toJSONString(params));
        return "failure";
    }
    ReceiveAlipayNotifyCommand command = this.parseToReceiveAlipayNotifyCommand(params);
    if (command.isWaitBuyerPay()) {
        return "failure";
    }
    this.alipayNotifyHandler.receiveNotify(command);
    return "success";
}

/** * verify signature **@param params
 * @return* /
public boolean verifySignature(Map<String, String> params) {
    try {
        String alipayPublicCertPath = this.alipayProperties.getAlipayCertPublicKey_RSA2();
        return AlipaySignature.rsaCertCheckV1(params, alipayPublicCertPath, StandardCharsets.UTF_8.name(), "RSA2");
    } catch (AlipayApiException e) {
        log.error("Alipay payment result callback check abnormal signature", e);
        return false; }}private ReceiveAlipayNotifyCommand parseToReceiveAlipayNotifyCommand(Map<String, String> params) {
    String passbackParams = params.remove("passback_params");
    String fundBillList = params.remove("fund_bill_list");

    ReceiveAlipayNotifyCommand command = JSON.parseObject(JSON.toJSONString(params), ReceiveAlipayNotifyCommand.class);
    command.setFundBillList(JSON.parseArray(fundBillList, FundBill.class));
    command.setPassBackParam(JSON.parseObject(passbackParams, PassBackParam.class));
    return command;
}

@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReceiveAlipayNotifyCommand {

    /** * Notification time Date is the notification sending time. The format is YYYY-MM-DD HH: MM: SS 2015-14-27 15:45:58 */
    @JSONField(name = "notify_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime notifyTime;
    /** * Notification type String(64) is the notification type trade_status_sync */
    @JSONField(name = "notify_type")
    private String notifyType;
    / * * * notice check ID String (128) is a notification check ID ac05099524730693a8b330c5ecf72da9786 * /
    @JSONField(name = "notify_id")
    private String notifyId;
    /** * developer app_id String(32) is the app Id 2014072300007148 */
    @JSONField(name = "app_id")
    private String appId;
    String(10) is a encoding format, such as UTF-8, GBK, gb2312, and UTF-8 */
    @JSONField(name = "charset")
    private String charset;
    /** * Interface version String(3) is the interface version of the call, which is fixed at 1.0 1.0 */
    @JSONField(name = "version")
    private String version;
    /** * Signature Type String(10) Indicates the signature algorithm used by merchants to generate signature strings. Currently, RSA2 and RSA are supported. RSA2 RSA2 */ is recommended
    @JSONField(name = "sign_type")
    private String signType;
    / * * * signature String (256) refer to the asynchronous attestation 601510 b7970e52cc63db0f44997cf70e * / return the result
    @JSONField(name = "sign")
    private String sign;
    / * * * pay treasure transaction number String (64) is 2013112011001004330000121536 * / alipay transaction voucher number
    @JSONField(name = "trade_no")
    private String tradeNo;
    String(64) is the merchant order number of the original payment request 6823789339978248 */
    @JSONField(name = "out_trade_no")
    private String outTradeNo;
    /** * Merchant business ID String(64) No Merchant business ID, which is the serial number of the refund application returned in the refund notice HZRF001 */
    @JSONField(name = "out_biz_no")
    private String outBizNo;
    /** * Buyer's Alipay User NUMBER String(16) No Unique Alipay user number corresponding to the buyer's Alipay account. A pure 16-bit number starting with 2088:2088102122524333 */
    @JSONField(name = "buyer_id")
    private String buyerId;
    /** * Buyer alipay account String(100) No Buyer Alipay account 159******20 */
    @JSONField(name = "buyer_logon_id")
    private String buyerLogonId;
    /** * Seller alipay user id String(30) No Seller alipay user ID 2088101106499364 */
    @JSONField(name = "seller_id")
    private String sellerId;
    /** * String(100) No Zhu **@alitest.com
     */
    @JSONField(name = "seller_email")
    private String sellerEmail;
    /** * Trade Status String(32) No Trade status, see TRADE_CLOSED */
    @JSONField(name = "trade_status")
    private TradeStatus tradeStatus;
    /** * Order Amount No Order amount to be paid in this transaction, unit: RMB 20 */
    @JSONField(name = "total_amount")
    private BigDecimal totalAmount;
    /** * paid-in amount Number(9,2) no the amount actually received by the merchant in the transaction, the unit is RMB 15 */
    @JSONField(name = "receipt_amount")
    private BigDecimal receiptAmount;
    /** * invoicing amount Number(9,2) no invoicing amount paid by the user in the transaction 10.00 */
    @JSONField(name = "invoice_amount")
    private BigDecimal invoiceAmount;
    /** * payment amount Number(9,2) no amount paid by the user in the transaction 13.88 */
    @JSONField(name = "buyer_pay_amount")
    private BigDecimal buyerPayAmount;
    /** * Number(9,2) no 12.00 */
    @JSONField(name = "point_amount")
    private BigDecimal pointAmount;
    /** * total refund amount Number(9,2) no in the refund notice, the total refund amount is returned in yuan, supporting two decimal digits 2.58 */
    @JSONField(name = "refund_fee")
    private BigDecimal refundFee;
    /** * Order title String(256) No Commodity title/transaction title/order title/order keyword, etc., is the corresponding parameter when the request, original notification back to pay transaction */
    @JSONField(name = "subject")
    private String subject;
    /** * Description String(400) No Remarks, description, and details about the order. Corresponding to the body parameter at the time of the request, the original notification comes back to pay the transaction content */
    @JSONField(name = "body")
    private String body;
    /** * Transaction creation time Date No Transaction creation time. The format is YYYY-MM-DD HH: MM: SS 2015-04-27 15:45:57 */
    @JSONField(name = "gmt_create")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime gmtCreate;
    /** * Transaction payment Date No Buyer's payment Date for this transaction. The format is YYYY-MM-DD HH: MM: SS 2015-04-27 15:45:57 */
    @JSONField(name = "gmt_payment")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime gmtPayment;
    /** * Transaction refund Date No Transaction refund Date. The format is YYYY-MM-DD HH: MM :ss.S 2015-04-28 15:45:57.320 */
    @JSONField(name = "gmt_refund")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime gmtRefund;
    /** * Transaction end Date No Transaction end time. The format is YYYY-MM-DD HH: MM: SS 2015-04-29 15:45:57 */
    @JSONField(name = "gmt_close")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime gmtClose;
    /** * Payment Amount Information String(512) No Payment amount information of each channel. For details, see fund details [{"amount":"15.00","fundChannel":"ALIPAYACCOUNT"}] */
    @JSONField(name = "fund_bill_list")
    private List<FundBill> fundBillList;
    /** * Return parameter String(512) No Public return parameter. If this parameter is passed when a request is made, it will be returned to the merchant as it is in the asynchronous notification. This parameter must be UrlEncode before it can be sent to Alipay merchantBizType%3d3C%26merchantBizNo% 3D2016010101111 */
    @JSONField(name = "passback_params")
    private PassBackParam passBackParam;
    /** * Coupon information String No coupon information used in the payment of this transaction, See the coupon information for details [{"amount":"0.20","merchantContribute":"0.00","name":" one-click create coupon template coupon name","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUC < p style = "max-width: 100%; clear: both; min-width: 100%; * /
    @JSONField(name = "voucher_detail_list")
    private String voucherDetailList;

    @JSONField(serialize = false)
    public boolean isTradeClosed(a) {
        return this.getTradeStatus() == TradeStatus.TRADE_CLOSED;
    }

    @JSONField(serialize = false)
    public boolean isTradeFinished(a) {
        return this.getTradeStatus() == TradeStatus.TRADE_FINISHED;
    }

    @JSONField(serialize = false)
    public boolean isTradeSuccess(a) {
        return this.getTradeStatus() == TradeStatus.TRADE_SUCCESS;
    }

    @JSONField(serialize = false)
    public boolean isWaitBuyerPay(a) {
        return this.getTradeStatus() == TradeStatus.WAIT_BUYER_PAY; }}Copy the code

The most important thing for payment result callback is to know what the protocol of alipay background callback interface is, so as to know how to test the interface and define the interface and avoid wasting time.

Here I suggest writing API tests to simulate the callback of Alipay background, such development efficiency is very high, rather than deploying to the line to test.

A refund

The unified receipt transaction refund interface provides the information required for refund. The only pity is that there is no example of the certificate signing method, which misled me for most of the day. As a result, the interface has been disconnected and wasted a lot of time.

Task list:

  1. Get the merchant order number or Alipay transaction number, choose one of them.
  2. Get the refund amount
  3. Build the refund interface requestAlipayTradeRefundRequestAlipayTradeRefundModel, the latter is the interfacebiz_contentParameters.
  4. Initiating a refund request
  5. Develop business logic
  6. test
@Test
void should_refund_from_alipay(a) throws Exception {
    // given
    Payment payment = Payment.builder()
            .state(Payment.State.SUCCESS)
            .orderNo("77718857416704")
            .payMoney(1L)
            .payChannel(Payment.PayChannel.ALIPAY)
            .build();
    payment.setId(77718947676160L);
    AlipayTransaction alipayTransaction = AlipayTransaction.builder()
            .payment(payment)
            .outTradeNo(payment.getOrderNo())
            .totalAmount(BigDecimal.valueOf(payment.getPayMoney()).divide(BigDecimal.valueOf(100)))
            .build();
    given(this.paymentRepository.findByIdAndOrderNo(anyLong(), anyString()))
            .willReturn(Optional.of(payment));
    given(this.alipayTransactionRepository.findByPaymentId(eq(payment.getId())))
            .willReturn(Optional.of(alipayTransaction));
    // when
    RefundCommand command = RefundCommand.builder()
            .refundAmount(payment.getPayMoney())
            .refundChannel(Payment.PayChannel.ALIPAY)
            .paymentId(payment.getId())
            .orderNo(payment.getOrderNo())
            .operatorId(1L)
            .build();
    WebTestClient.ResponseSpec responseSpec = this.post("/refunds", command);
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(RefundResult.class)
            .value(value -> {
                assertThat(value.getOrderNo()).isEqualTo(command.getOrderNo());
                assertThat(value.getPayAmount()).isEqualTo(1);
                assertThat(value.getPaymentId()).isEqualTo(command.getPaymentId());
                assertThat(value.getRefundAmount()).isEqualTo(command.getRefundAmount());
                assertThat(value.getRefundChannel()).isEqualTo(command.getRefundChannel());
            })
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
    verify(this.orderClient, times(1)).notifyRefund(any());
}

/** ** Refund **@return* /
@PostMapping("/refunds")
public Mono<RefundResult> refund(@RequestBody RefundCommand command) {
    command.verify();
    if (command.isAlipay()) {
        return Mono.just(this.alipayRefundService.refund(command));
    }
    throw new PayException("This method of refund is not supported at present");
}

@Slf4j
@Component
public class AlipayClient {
    / * * * * refund details please accepts the text document https://docs.open.alipay.com/api_1/alipay.trade.refund/ * *@paramOrderNo orderNo *@paramRefundAmount refundAmount *@return
     * @throws AlipayApiException
     */
    public AlipayTradeRefundResponse refund(String orderNo, BigDecimal refundAmount) throws AlipayApiException {
        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
        model.setOutTradeNo(orderNo);
        model.setRefundAmount(refundAmount.toString());
        request.setBizModel(model);
        return this.alipayClient.certificateExecute(request); }}Copy the code

There are two things to note here:

  1. Using public KeyalipayClient.certificateExecute(request)Initiate a refund request instead ofalipayClient.execute(request)巴拿马事件
  2. Refund is a synchronous request and is not recommendednotify_urlOf course, it depends on the actual situation.

WeChat pay

Wechat Pay integration is not difficult and does not have so many configurations, but the documentation is not very careful, and there is no server SDK, so all kinds of integration problems should be borne by the developers themselves.

Wechat: love with not proud jiao face.gif. Developer: need not also use grievance face. GIF.Copy the code

Important configuration

  • APPID (APPID, the APPID of the app approved by wechat open platform (please log in to open.weixin.qq.com to check, note that it is different from the APPID of the official account)
  • McH-id (Merchant ID, merchant number assigned by wechat Pay)
  • Api-secret-key (API key)
  • Notify-url (url for asynchronous notification of payment results)
  • Cert – file – path (WeChat pay interface, involving money rollback interface will be used to the API certificate, including a refund, cancel the interface, see pay.weixin.qq.com/wiki/doc/ap…

APP Payment process

The wechat APP payment process is described in the wechat Payment development document, and the process is not much different from Alipay. For details, please refer to the first-hand information on the official website.

Agreement rules

It should be noted that both the request and return data of wechat Pay are in XML format, and the root node is named XML

Attestation tool

Wechat provides a graphical interface for visa verification, and the rules are very simple. Please read the official document by yourself.

Unified order

Details please unified order.

Task list:

  1. Important configuration
  2. Build the checkered utility class
  3. Build the wechat payment tool class
  4. Build the install utility class for XML <=> custom objects
  5. Define single interface
  6. Write the test
@Test
void should_return_wechat_place_order_info(a) throws Exception {
    // given
    PlaceOrderCommand command = PlaceOrderCommand.builder()
            .userId(2L)
            .userName("Tester")
            .goodsId(1L)
            .orderNo(RandomStringUtils.randomNumeric(20))
            .subject("Coat")
            .totalAmount(BigDecimal.ONE)
            .payChannel(PayChannel.WECHAT)
            .build();
    // when
    WebTestClient.ResponseSpec responseSpec = this.post("/orders", command);
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(new ParameterizedTypeReference<PlaceOrderResult<WechatOrderInfo>>() {})
            .value(actualPaymentOrder -> {
                assertThat(actualPaymentOrder.getId()).isNotNull();
                assertThat(actualPaymentOrder.getPayChannel()).isEqualTo(Payment.PayChannel.WECHAT);
                assertThat(actualPaymentOrder.getOrderInfo()).isNotNull();
                assertThat(actualPaymentOrder.getOrderInfo().getAppid()).isNotBlank();
                assertThat(actualPaymentOrder.getOrderInfo().getNoncestr()).isNotBlank();
                assertThat(actualPaymentOrder.getOrderInfo().getSign()).isNotBlank();
                assertThat(actualPaymentOrder.getOrderInfo().getPartnerid()).isNotBlank();
                assertThat(actualPaymentOrder.getOrderInfo().getPrepayid()).isNotBlank();
                assertThat(actualPaymentOrder.getOrderInfo().getPackageValue()).isEqualTo("Sign=WXPay");
                assertThat(actualPaymentOrder.getOrderInfo().getTimestamp()).isNotNull();
                // other assert
            })
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
}

/** * Place an order (generate a payment order) **@return* /
@PostMapping("/orders")
public Mono<PlaceOrderResult> placeOrder(@RequestBody PlaceOrderCommand command) {
    command.verify();
    if (command.isAlipay()) {
        return Mono.just(this.alipayService.placeOrder(command));
    }
    if (command.isWechat()) {
        return this.wechatPayService.placeOrder(command);
    }
    throw new PayException("Not open for the time being.");
}


public class WechatSigner {

    public static String sign(Map<String, Object> map, String apiSecretKey) {
        SortedSet<String> sortedSet = new TreeSet<>(map.keySet());
        StringBuilder urlParams = new StringBuilder();
        for (String key : sortedSet) {
            urlParams.append(key)
                    .append("=")
                    .append(map.get(key).toString())
                    .append("&");
        }
        urlParams.append("key=").append(apiSecretKey);
        return DigestUtils.md5Hex(urlParams.toString())
                .toUpperCase();
    }

    public static <T> String sign(T jsonObject, String apiSecretKey) {
        Map<String, Object> map = toMap(jsonObject);
        return sign(map, apiSecretKey);
    }

    private static <T> Map<String, Object> toMap(T jsonObject) {
        String jsonString = JSON.toJSONString(jsonObject);
        return JSON.parseObject(jsonString, newTypeReference<HashMap<String, Object>>() {}.getType()); }}@Slf4j
public class XmlUtils {

    public static <T> T parse2Object(String xml, Class<T> claz) {
        try {
            return JAXB.unmarshal(CharSource.wrap(xml).openStream(), claz);
        } catch (IOException e) {
            log.error("Parsing XML exception occurred", e);
            throw new ParseXmlException("Parsing XML exception occurred"); }}public static <T> String toXmlString(T jsonObject) {
        StringWriter xml = new StringWriter();
        JAXB.marshal(jsonObject, xml);
        returnxml.toString(); }}@Slf4j
@Component
public class WechatClient {

    private final HttpClient wechatPayHttpClient;
    private final WechatPayProperties wechatPayProperties;

    public WechatClient(WechatPayProperties wechatPayProperties) {
        this.wechatPayProperties = wechatPayProperties;
        this.wechatPayHttpClient = HttpClient.create(ConnectionProvider.fixed("wechat-unifiedorder".10))
                .baseUrl("https://api.mch.weixin.qq.com");
    }

    public Mono<WechatUnifiedOrderResponse> postUnifiedOrder(PlaceOrderCommand command, Long paymentId) {
        String requestBody = this.generateUnifiedOrderBody(command, paymentId);
        return this.wechatPayHttpClient
                .post()
                .uri("/pay/unifiedorder")
                .send((req, out) -> out.sendString(Mono.just(requestBody)))
                .responseContent()
                .aggregate()
                .asString()
                .onErrorResume(ex -> Mono.just(ex.getMessage()))
                .flatMap(responseXml -> Mono.just(XmlUtils.parse2Object(responseXml, WechatUnifiedOrderResponse.class)));
    }

    private SslContext createSslContext(a) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        File apiCertFile = ResourceUtils.getFile(this.wechatPayProperties.getCertFilePath());
        keyStore.load(new FileInputStream(apiCertFile), this.wechatPayProperties.getMchId().toCharArray());
        // Set up key manager factory to use our key store
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, this.wechatPayProperties.getMchId().toCharArray());
        return SslContextBuilder.forClient()
                .keyManager(keyManagerFactory)
                .build();
    }

    /** * Generate call wechat unified single interface request body **@param command
     * @param paymentId
     * @return* /
    private String generateUnifiedOrderBody(PlaceOrderCommand command, Long paymentId) {
        var wechatPlaceOrderRequest = WechatPlaceOrderRequest.builder()
                .appid(this.wechatPayProperties.getAppid())
                .mchId(this.wechatPayProperties.getMchId())
                .nonceStr(UUID.randomUUID().toString().replaceAll("-".""))
                .body(command.getSubject())
                .outTradeNo(command.getOrderNo())
                .totalFee(command.getTotalAmount().longValue())
                .spbillCreateIp(this.getIpV4())
                .notifyUrl(this.wechatPayProperties.getNotifyUrl())
                .tradeType(this.wechatPayProperties.getTradeType()) .attach(JSON.toJSONString(Attach.builder().paymentId(paymentId).build())) .build();  wechatPlaceOrderRequest.setSign(WechatSigner.sign(wechatPlaceOrderRequest,this.wechatPayProperties.getApiSecretKey()));

        return XmlUtils.toXmlString(wechatPlaceOrderRequest);
    }

    private String getIpV4(a) {
        String ip = null;
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface iface = interfaces.nextElement();
                // filters out 127.0.0.1 and inactive interfaces
                if(iface.isLoopback() || ! iface.isUp()) {continue;
                }
                Enumeration<InetAddress> addresses = iface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress addr = addresses.nextElement();
                    if (addr instanceof Inet6Address) {
                        continue; } ip = addr.getHostAddress(); }}}catch (SocketException e) {
            throw new RuntimeException(e);
        }
        returnip; }}@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class WechatPlaceOrderRequest {

    /** * APPID * APPID approved by wechat open platform (please log in to open.weixin.qq.com to check, note that it is different from the APPID of the official account) */
    @NotBlank
    @XmlElement(name = "appid")
    private String appid;

    /** * Merchant number * Merchant number assigned by wechat Pay */
    @NotBlank
    @XmlElement(name = "mch_id")
    @JSONField(name = "mch_id")
    private String mchId;

    / * * * random string, not longer than 32 bits (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3) * /
    @NotBlank
    @XmlElement(name = "nonce_str")
    @JSONField(name = "nonce_str")
    private String nonceStr;

    / * * * signature (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3) * /
    @NotBlank
    @XmlElement(name = "sign")
    @JSONField(name = "sign")
    private String sign;

    /** * Commodity description * Commodity description Transaction field format according to different application scenarios according to the following formats: * APP - the name of the APP to be imported into the application market - the name of the actual commodity, daily love elimination - game recharge. * /
    @NotBlank
    @XmlElement(name = "body")
    @JSONField(name = "body")
    private String body;

    / * * * merchants merchants system internal order number, order number * request within 32 characters, only Numbers, upper and lower case letters _ - | * and only under the same merchant number. See merchant order Number */ for details
    @NotBlank
    @XmlElement(name = "out_trade_no")
    @JSONField(name = "out_trade_no")
    private String outTradeNo;

    / * * * * order total amount, total amount of units: points (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2) * / * see the payment amount
    @NotBlank
    @XmlElement(name = "total_fee")
    @JSONField(name = "total_fee")
    private Long totalFee;

    /** * Terminal IP address * Supports both IPV4 and IPV6 addresses. IP address of machine calling wechat Pay API * Example: 123.12.12.123 */
    @NotBlank
    @XmlElement(name = "spbill_create_ip")
    @JSONField(name = "spbill_create_ip")
    private String spbillCreateIp;

    /** * Notification address * Address for receiving wechat Pay asynchronous notification callback. The notification URL must be a directly accessible URL without parameters. * /
    @NotBlank
    @XmlElement(name = "notify_url")
    @JSONField(name = "notify_url")
    private String notifyUrl;

    /** * Payment type * Example: APP */
    @NotBlank
    @XmlElement(name = "trade_type")
    @JSONField(name = "trade_type")
    private String tradeType;

    /** * Additional data */
    @XmlElement(name = "attach")
    @JSONField(name = "attach")
    private String attach;

    public Attach getAttach(a) {
        return JSON.parseObject(this.attach, Attach.class); }}@Setter
@Getter
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class WechatUnifiedOrderResponse {

    /** * SUCCESS/FAIL * 

* This field is the communication identifier, not the transaction identifier, the transaction is successful need to view the result_code to determine */

@NotBlank @XmlElement(name = "return_code") private String returnCode; /** * Error cause *

* signature failed *

* Parameter format verification error */

@Nullable @XmlElement(name = "return_msg") private String returnMsg; /** * Application APPID is String(32) wx88888888888888 Application ID submitted by the calling interface */ @NotBlank private String appid; /** * Merchant number is String(32) 1900000109 Merchant number submitted by the call interface */ @NotBlank @XmlElement(name = "mch_id") private String mchId; /** * Device ID No String(32) 013467007045764 Terminal device ID submitted by the calling interface, */ @NotBlank @XmlElement(name = "device_info") private String deviceInfo; / * * * random String is a String (32) 5 k8264iltkch16cq2502si8znmtm67vs WeChat returned by the random String * / @NotBlank @XmlElement(name = "nonce_str") private String nonceStr; / * * * signature is a String (32) C380BEC2BFD727A4B6845133519F3AD6 WeChat return the signature, as shown in the signature algorithm * / @NotBlank private String sign; /** * The service result is String(16) SUCCESS SUCCESS/FAIL */ @NotBlank @XmlElement(name = "result_code") private String resultCode; /** * Error code No String(32) SYSTEMERROR See Section 6 Error list */ for details @NotBlank @XmlElement(name = "err_code") private String errCode; /** * Error code Description No String(128) System error Description of an error */ @NotBlank @XmlElement(name = "err_code_des") private String errCodeDes; / * * * prepaid trading session ID String (32) is WX1217752501201407033233368018 WeChat return payment transaction session ID * / @NotBlank @XmlElement(name = "prepay_id") private String prepayId; /** * Payment type */ @NotBlank @XmlElement(name = "trade_type") private String tradeType; } @Setter @Getter @Builder @NoArgsConstructor @AllArgsConstructor @Component @ConfigurationProperties("pay.wechat") public class WechatPayProperties { /** * APPID * APPID approved by wechat open platform (please log in to open.weixin.qq.com to check, note that it is different from the APPID of the official account) */ private String appid; /** * Merchant number * Merchant number assigned by wechat Pay */ private String mchId; /** * API key */ private String apiSecretKey; /** * Notification address * Address for receiving wechat Pay asynchronous notification callback. The notification URL must be a directly accessible URL without parameters. * / private String notifyUrl; /** * Payment type */ private String tradeType; /** * Certificate file path */ private String certFilePath; public String getCertFilePath(a) { return FileUtils.getAbsolutePath(this.certFilePath); }} pay: wechat: APPID, wechat: McH-id, wechat: McH-id, wechat: McH-id, wechat: McH-id, wechat: McH-id, wechat: McH-id, wechat: McH-id, wechat: McH-id # API key api-secret-key: # notify-url: # payment type trade-type: In APP # wechat Payment interface, API certificates will be used for interfaces involving fund rollback, including refund and revocation interfaces. Please see HTTPS for details://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3 cert-file-path: Copy the code

In this way, the order is completed. It is not difficult, but tedious. You need to encapsulate XML processing, signing and wechat client by yourself. If you use the REACTOR NetTY HTTP Client, you also need to understand the REACTOR Netty API.

Notice of payment result

Most of the information is described in the payment result notification documentation, so please feel free to read it.

Task list:

  • Define the callback interface for receiving wechat payment
  • Define the callback parameter object that receives wechat payment
  • Reads the XML from the request and converts it into an input parameter object
  • attestation
  • Develop merchant business logic
  • The successful XML format is returned to the wechat Pay background
  • If the verification fails, the failed XML format will be returned to the wechat Pay background
  • Write API tests to simulate payment result notification callbacks

@Test
void should_response_failure_when_the_sign_is_incorrect(a) throws Exception {
    // given
    String xml = "<xml>" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "<total_fee>1</total_fee>\n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "</xml>";
    // when
    WebTestClient.ResponseSpec responseSpec = this.client.post()
            .uri("/wechat/notifications/payment")
            .body(BodyInserters.fromObject(xml))
            .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML_VALUE)
            .exchange();
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(String.class)
            .isEqualTo("<xml>\n" +
                    "  <return_code><![CDATA[FAILURE]]></return_code>\n" +
                    "  <return_msg><![CDATA[FAILURE]]></return_msg>\n" +
                    "</xml>")
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
    // other assert
}


@Test
void should_response_success_when_the_sign_is_correct(a) throws Exception {
    // given
    Payment payment = Payment.builder()
            .buyerId(1L)
            .buyerName("Tester")
            .state(State.WAIT_BUYER_PAY)
            .orderNo("75321441763328")
            .payChannel(PayChannel.WECHAT)
            .payMoney(100)
            .build();
    payment.setId(75321692864512L);
    given(this.paymentRepository.findById(eq(payment.getId()))).willReturn(Optional.of(payment));
    doReturn(WechatTransaction.builder().payment(payment).build())
            .when(this.wechatTransactionRepository)
            .save(any());

    String xml = "<xml>" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "<total_fee>1</total_fee>\n" +
            "
      
       
      \n" +
            "
      
       
      \n" +
            "</xml>";
    // when
    WebTestClient.ResponseSpec responseSpec = this.client.post()
            .uri("/wechat/notifications/payment")
            .body(BodyInserters.fromObject(xml))
            .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_XML_VALUE)
            .exchange();
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(String.class)
            .isEqualTo("<xml>\n" +
                    "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
                    "  <return_msg><![CDATA[OK]]></return_msg>\n" +
                    "</xml>")
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
    Payment dbPayment = this.paymentRepository.findById(payment.getId()).get();
    assertThat(dbPayment.getState()).isEqualTo(Payment.State.SUCCESS);
    verify(this.orderClient, times(1)).notifyPaid(any());
    // other assert
}

/** * wechat Pay payment result change callback interface **@param request
 */
@PostMapping("/wechat/notifications/payment")
public Mono<String> receiveWechatNotify(ServerHttpRequest request) {
    return this.readXmlFrom(request)
            .map(xml -> {
                ReceiveWechatNotifyCommand command = XmlUtils.parse2Object(xml, ReceiveWechatNotifyCommand.class);
                if(! command.isSuccess()) { log.error("Wechat pay callback notification error {} {}", command.getErrCodeDes(), JSON.toJSONString(command));
                    return this.sendWechatPayNotifyFailResponse();
                }
                if (!this.wechatNotifyHandler.verifySignature(command)) {
                    log.error("Wechat callback interface signature verification failed {}", JSON.toJSONString(command));
                    return this.sendWechatPayNotifyFailResponse();
                }
                this.wechatNotifyHandler.receiveNotify(command);
                return this.sendWechatPayNotifySuccessResponse();
            });
}

public boolean verifySignature(ReceiveWechatNotifyCommand command) {
    Map<String, Object> map = JSON.parseObject(JSON.toJSONString(command), new TypeReference<HashMap<String, Object>>() {}.getType());
    // sign does not participate in signing
    map.remove("sign");
    String sign = WechatSigner.sign(map, this.wechatPayProperties.getApiSecretKey());
    return StringUtils.equals(command.getSign(), sign);
}

private Mono<String> readXmlFrom(ServerHttpRequest request) {
    return request.getBody()
            .flatMap(dataBuffer -> Mono.just(String.valueOf(StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()))))
            .reduce(String::concat);
}

private String sendWechatPayNotifyFailResponse(a) {
    return "<xml>\n" +
            "  <return_code><![CDATA[FAILURE]]></return_code>\n" +
            "  <return_msg><![CDATA[FAILURE]]></return_msg>\n" +
            "</xml>";
}

private String sendWechatPayNotifySuccessResponse(a) {
    return "<xml>\n" +
            "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
            "  <return_msg><![CDATA[OK]]></return_msg>\n" +
            "</xml>";
}

@Setter
@Getter
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class ReceiveWechatNotifyCommand {

    /** * The return status code is String(16) SUCCESS */
    @NotBlank
    @XmlElement(name = "return_code")
    @JSONField(name = "return_code")
    private String returnCode;

    /** * The return message is String(128) OK */
    @NotBlank
    @XmlElement(name = "return_msg")
    @JSONField(name = "return_msg")
    private String returnMsg;

    /** * APPID String(32) wx88888888888888 approved app APPID */
    private String appid;
    /** * Merchant ID String(32) 1900000109 Merchant id allocated by wechat Pay */
    @XmlElement(name = "mch_id")
    @JSONField(name = "mch_id")
    private String mchId;
    / * * * random String String (32) 5 k8264iltkch16cq2502si8znmtm67vs random String, not longer than 32 * /
    @XmlElement(name = "nonce_str")
    @JSONField(name = "nonce_str")
    private String nonceStr;
    / * * * signature String (32) C380BEC2BFD727A4B6845133519F3AD6 signature, as shown in the signature algorithm * /
    private String sign;
    /** * Service result String(16) SUCCESS SUCCESS/FAIL */
    @XmlElement(name = "result_code")
    @JSONField(name = "result_code")
    private String resultCode;
    /** * Error code String(32) SYSTEMERROR Description of the returned error */
    @XmlElement(name = "err_code")
    @JSONField(name = "err_code")
    private String errCode;
    /** * Error Code Description String(128) System error Description of an error */
    @XmlElement(name = "err_code_des")
    @JSONField(name = "err_code_des")
    private String errCodeDes;
    /** * User ID String(128) wXD930EA5D5A258F4f Unique identifier of a user under merchant APPID */
    private String openid;
    String(1) Indicates whether the user follows the public account, Y- Follows, N- does not follow */
    @XmlElement(name = "is_subscribe")
    @JSONField(name = "is_subscribe")
    private String isSubscribe;
    /** * Transaction type String(16) APP APP */
    @XmlElement(name = "trade_type")
    @JSONField(name = "trade_type")
    private String tradeType;
    /** * Bank String(16) Bank id of the CMC. For the bank type, see the bank list */
    @XmlElement(name = "bank_type")
    @JSONField(name = "bank_type")
    private String bankType;
    /** * total amount Int 100 Total amount of the order, in minutes */
    @XmlElement(name = "total_fee")
    @JSONField(name = "total_fee")
    private Long totalFee;
    /** * Currency Type String(8) CNY currency type, which conforms to the THREE-letter code of ISO4217. The default value is CNY. For lists of other values, see currency type */
    @XmlElement(name = "fee_type")
    @JSONField(name = "fee_type")
    private String feeType;
    /** ** Cash payment amount Int 100 Cash payment amount of the order, see Payment Amount */
    @XmlElement(name = "cash_fee")
    @JSONField(name = "cash_fee")
    private Long cashFee;
    /** * Cash payment currency type String(16) CNY currency type, which conforms to the THREE-letter code of ISO4217 standard. The default value is CNY. For lists of other values, see currency type */
    @XmlElement(name = "cash_fee_type")
    @JSONField(name = "cash_fee_type")
    private String cashFeeType;
    /** * Voucher amount Int 10 Voucher or immediate discount amount <= total order amount, total order amount - Voucher or immediate discount amount = cash payment amount, see payment Amount */
    @XmlElement(name = "coupon_fee")
    @JSONField(name = "coupon_fee")
    private Long couponFee;
    / * * * WeChat payment order number String (32) 1217752501201407033233368018 WeChat pay order number * /
    @XmlElement(name = "transaction_id")
    @JSONField(name = "transaction_id")
    private String transactionId;
    / * * * merchant order number String (32) 1212321211201407033568112322 merchants system internal order number, request within 32 characters, only Numbers, upper and lower case letters _ - | * @, and only under the same merchant number. * /
    @XmlElement(name = "out_trade_no")
    @JSONField(name = "out_trade_no")
    private String outTradeNo;
    /** * Merchant data packet String(128) 123456 Merchant data packet, returns */ as is
    private String attach;
    /** * Payment Completion Time String(14) 20141030133525 Payment completion time in the yyyyMMddHHmmss format. For example, 09:10:10 on December 25, 2009, it is 20091225091010. See other time rules */
    @XmlElement(name = "time_end")
    @JSONField(name = "time_end")
    private String timeEnd;

    public WechatTransaction.Attach getAttach(a) {
        return JSON.parseObject(this.attach, WechatTransaction.Attach.class);
    }

    @JSONField(serialize = false)
    public boolean isSuccess(a) {
        return "SUCCESS".equals(this.getReturnCode())
                && "SUCCESS".equals(this.getResultCode()); }}Copy the code

To note here is that the API layer into the need to use the refs org. Springframework.. The HTTP server. The reactive. ServerHttpRequest request to inject in the current request, This allows you to read the request message body and convert it to XML.

To apply for a refund

The documentation for applying for a refund already describes most of the information. Please read it yourself.

The task list

  • Configuring the API Certificate
  • Define wechat refund interface input parameters
  • Define the return value of wechat refund interface
  • Define wechat refund API
  • Define the merchant backend API
  • Write the test
@Test
void should_refund_from_wechat_pay(a) throws Exception {
    // given
    Payment payment = Payment.builder()
            .state(Payment.State.SUCCESS)
            .orderNo("77718857416704")
            .payMoney(1L)
            .payChannel(PayChannel.WECHAT)
            .build();
    payment.setId(77718947676160L);
    WechatTransaction wechatTransaction = WechatTransaction.builder()
            .payment(payment)
            .outTradeNo(payment.getOrderNo())
            .build();
    given(this.paymentRepository.findByIdAndOrderNo(anyLong(), anyString()))
            .willReturn(Optional.of(payment));
    given(this.wechatTransactionRepository.findByPaymentId(eq(payment.getId())))
            .willReturn(Optional.of(wechatTransaction));
    // when
    RefundCommand command = RefundCommand.builder()
            .operatorId(1L)
            .orderNo(payment.getOrderNo())
            .paymentId(payment.getId())
            .refundAmount(1L)
            .refundChannel(WECHAT)
            .build();
    doReturn(this.createWechatRefundSuccessResult(payment.getPayMoney(), command.getRefundAmount(), command.getOrderNo()))
            .when(this.wechatClient)
            .postRefund(any(), eq(payment.getPayMoney()));
    WebTestClient.ResponseSpec responseSpec = this.post("/refunds", command);
    // then
    responseSpec
            .expectStatus().isOk()
            .expectBody(RefundResult.class)
            .value(value -> {
                assertThat(value.getOrderNo()).isEqualTo(payment.getOrderNo());
                assertThat(value.getPayAmount()).isEqualTo(1);
                assertThat(value.getPaymentId()).isEqualTo(payment.getId());
                assertThat(value.getRefundAmount()).isEqualTo(payment.getRefundMoney());
                assertThat(value.getRefundChannel()).isEqualTo(payment.getPayChannel());
            })
            // Automatically generate API documentation
            .consumeWith(this.commonDocumentation());
    verify(this.orderClient, times(1)).notifyRefund(any());
    // other assert
}

/** ** Refund **@return* /
@PostMapping("/refunds")
public Mono<RefundResult> refund(@RequestBody RefundCommand command) {
    command.verify();
    if (command.isWechat()) {
        return this.wechayRefundService.refund(command);
    }
    throw new PayException("This method of refund is not supported at present");
}

@Slf4j
@Component
public class WechatClient {

    private final HttpClient secureHttpClient;
    private final WechatPayProperties wechatPayProperties;

    public WechatClient(WechatPayProperties wechatPayProperties) {
        this.wechatPayProperties = wechatPayProperties;
        this.secureHttpClient = HttpClient.create(ConnectionProvider.fixed("secure-http-client".5))
                .secure(spec -> {
                    try {
                        spec.sslContext(this.createSslContext());
                    } catch (Exception e) {
                        log.warn("Unable to set SSL Context", e);
                    }
                })
                .baseUrl("https://api.mch.weixin.qq.com");
    }

    public Mono<WechatRefundResponse> postRefund(RefundCommand command, Long orderTotalFee) {
        String requestBody = this.generateRefundRequestBody(command, orderTotalFee);
        return this.secureHttpClient
                .post()
                .uri("/secapi/pay/refund")
                .send((req, out) -> out.sendString(Mono.just(requestBody)))
                .responseContent()
                .aggregate()
                .asString()
                .onErrorResume(ex -> Mono.just(ex.getMessage()))
                .flatMap(responseXml -> Mono.just(XmlUtils.parse2Object(responseXml, WechatRefundResponse.class)));
    }

    /** * Generate call wechat refund interface request body **@param command
     * @return* /
    private String generateRefundRequestBody(RefundCommand command, Long orderTotalFee) {
        var wechatRefundRequest = WechatRefundRequest.builder()
                .appid(this.wechatPayProperties.getAppid())
                .mchId(this.wechatPayProperties.getMchId())
                .nonceStr(UUID.randomUUID().toString().replaceAll("-".""))
                .outTradeNo(command.getOrderNo())
                .outRefundNo(command.getOrderNo())
                .refundFee(command.getRefundAmount())
                .totalFee(orderTotalFee)
                .build();
        wechatRefundRequest.setSign(WechatSigner.sign(wechatRefundRequest, this.wechatPayProperties.getApiSecretKey()));

        returnXmlUtils.toXmlString(wechatRefundRequest); }}@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class WechatRefundRequest {
    /** * APPID * APPID approved by wechat open platform (please log in to open.weixin.qq.com to check, note that it is different from the APPID of the official account) */
    @NotBlank
    @XmlElement(name = "appid")
    private String appid;

    /** * Merchant number * Merchant number assigned by wechat Pay */
    @NotBlank
    @XmlElement(name = "mch_id")
    @JSONField(name = "mch_id")
    private String mchId;

    / * * * random string, not longer than 32 bits (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3) * /
    @NotBlank
    @XmlElement(name = "nonce_str")
    @JSONField(name = "nonce_str")
    private String nonceStr;

    / * * * signature (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3) * /
    @NotBlank
    @XmlElement(name = "sign")
    @JSONField(name = "sign")
    private String sign;

    / * * * merchants merchants system internal order number, order number * request within 32 characters, only Numbers, upper and lower case letters _ - | * @, and only under the same merchant number * /
    @NotBlank
    @XmlElement(name = "out_trade_no")
    @JSONField(name = "out_trade_no")
    private String outTradeNo;

    / merchant refund number * * * * merchants system within the refund number, merchants within the system only, only Numbers, upper and lower case letters _ - | * @, same refund order request only a back many times. * /
    @NotBlank
    @XmlElement(name = "out_refund_no")
    @JSONField(name = "out_refund_no")
    private String outRefundNo;

    / * * * * order total amount, total amount of units: points (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2) * / * see the payment amount
    @NotBlank
    @XmlElement(name = "total_fee")
    @JSONField(name = "total_fee")
    private Long totalFee;

    / * * * * refund amount refund total amount, total amount of the order, the unit for points, only as an integer (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2) * / * see the payment amount
    @NotBlank
    @XmlElement(name = "refund_fee")
    @JSONField(name = "refund_fee")
    private Long refundFee;
}

@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "xml")
public class WechatRefundResponse {

    /**
     * 返回状态码
     * 是	String(16)	SUCCESS	SUCCESS/FAIL
     */
    @NotBlank
    @XmlElement(name = "return_code")
    private String returnCode;
    /** * Returned information * no String(128) Signature failure Message. If not empty, the signature failed. Parameter format verification error */
    @NotBlank
    @XmlElement(name = "return_msg")
    private String returnMsg;
    /** * Service result * Yes String(16) SUCCESS SUCCESS/FAIL SUCCESS The refund application is received successfully. As a result, the refund query interface fails to query the FAIL service submission */
    @NotBlank
    @XmlElement(name = "result_code")
    private String resultCode;

    /** * Error code * No String(32) SYSTEMERROR list See error code list */
    @NotBlank
    @XmlElement(name = "err_code")
    private String errCode;

    /** * Error code Description * No String(128) System timeout result Description */
    @XmlElement(name = "err_code_des")
    private String errCodeDes;

    /** * Public ID * Yes String(32) wx88888888888888 public ID assigned by wechat */
    @NotBlank
    @XmlElement(name = "appid")
    private String appid;

    /** * Merchant number * is String(32) 1900000109 Merchant number assigned by wechat Pay */
    @NotBlank
    @XmlElement(name = "mch_id")
    private String mchId;
    / * * * * is a random String String (32) 5 k8264iltkch16cq2502si8znmtm67vs random String, not longer than 32 * /
    @NotBlank
    @XmlElement(name = "nonce_str")
    private String nonceStr;
    Sign / * * * * is the String (32) 5 k8264iltkch16cq2502si8znmtm67vs signature, as shown in the signature algorithm * /
    @NotBlank
    @XmlElement(name = "sign")
    private String sign;
    / * * * WeChat order number * is the String (32) 4007752501201407033233368018 WeChat order number * /
    @NotBlank
    @XmlElement(name = "transaction_id")
    private String transactionId;
    / merchant order number * * * * is the String (32) 33368018 merchants system internal order number, request within 32 characters, only Numbers, upper and lower case letters _ - | * @, and only under the same merchant number. * /
    @NotBlank
    @XmlElement(name = "out_trade_no")
    private String outTradeNo;
    / merchant refund number * * * * is the String (64) 121775250 merchants system within the refund number, merchants within the system only, only Numbers, upper and lower case letters _ - | * @, same refund order request only a back many times. * /
    @NotBlank
    @XmlElement(name = "out_refund_no")
    private String outRefundNo;
    / WeChat refund number * * * * is the String (32) 2007752501201407033233368018 WeChat refund number * /
    @NotBlank
    @XmlElement(name = "refund_id")
    private String refundId;
    /** ** refund amount * is Int 100 total refund amount, in minutes, can do a partial refund */
    @NotBlank
    @XmlElement(name = "refund_fee")
    private Long refundFee;
    /** * Refund amount payable * no Int 100 Refund amount after excluding refund amount of non-recharge voucher, refund amount = refund amount applied - Refund amount of non-recharge voucher, refund amount <= Refund amount applied */
    @XmlElement(name = "settlement_refund_fee")
    private String settlementRefundFee;
    /** * list price * is Int 100 total amount of order, the unit is minute, can only be an integer, see payment amount */
    @NotBlank
    @XmlElement(name = "total_fee")
    private Long totalFee;
    /** * order amount payable * no Int 100 Total order amount after excluding non-top-up voucher amount, order amount payable = Order amount - Non-top-up voucher amount payable <= Order amount. * /
    @XmlElement(name = "settlement_total_fee")
    private Long settlementTotalFee;
    /** ** cash payment amount * is Int 100 cash payment amount, the unit is minute, can only be an integer, see payment amount */
    @NotBlank
    @XmlElement(name = "cash_fee")
    private Long cashFee;
    /** * Currency of cash payment * No String(16) CNY currency type, which conforms to ISO 4217 standard three-letter code. The default value is CNY. For lists of other values, see currency type */
    @XmlElement(name = "cash_fee_type")
    private String cashFeeType;
    /** * Cash refund amount * no Int 100 Cash refund amount, expressed in minutes, can only be an integer, see amount paid */
    @XmlElement(name = "cash_refund_fee")
    private Long cashRefundFee;

    public boolean isPostSuccess(a) {
        return "SUCCESS".equals(this.getReturnCode());
    }

    public boolean isRefundSuccess(a) {
        return "SUCCESS".equals(this.getResultCode()); }}Copy the code

Refunds are worth noting that an API certificate is required.

conclusion

This paper summarizes various problems encountered when the server access alipay payment SDK and wechat payment SDK, and also includes the principle, configuration and code of the two cases of order refund, hoping to be helpful to some readers.