Procházet zdrojové kódy

feat: "Airwallex支付和退款"

ritchie před 2 roky
rodič
revize
19c836d41e
27 změnil soubory, kde provedl 1123 přidání a 107 odebrání
  1. 1 1
      README.md
  2. 10 0
      pom.xml
  3. 2 0
      src/main/java/com/szwl/Application.java
  4. 22 3
      src/main/java/com/szwl/model/utils/AirwallexConstant.java
  5. 7 0
      src/main/java/com/szwl/constant/WebhooksEvents.java
  6. 133 17
      src/main/java/com/szwl/controller/AirwallexPayController.java
  7. 45 0
      src/main/java/com/szwl/controller/HppController.java
  8. 21 0
      src/main/java/com/szwl/controller/TOrderAbroadController.java
  9. 163 0
      src/main/java/com/szwl/controller/WebhookController.java
  10. 16 0
      src/main/java/com/szwl/mapper/TOrderAbroadMapper.java
  11. 52 0
      src/main/java/com/szwl/mapper/xml/TOrderAbroadMapper.xml
  12. 14 0
      src/main/java/com/szwl/model/bean/PaymentIntent.java
  13. 25 0
      src/main/java/com/szwl/model/bean/PaymentIntentRequestBody.java
  14. 15 0
      src/main/java/com/szwl/model/bean/RefundRequestBody.java
  15. 4 0
      src/main/java/com/szwl/model/bo/R.java
  16. 146 0
      src/main/java/com/szwl/model/entity/TOrderAbroad.java
  17. 56 0
      src/main/java/com/szwl/service/AirwallexService.java
  18. 0 22
      src/main/java/com/szwl/service/TAirwallexService.java
  19. 20 0
      src/main/java/com/szwl/service/TOrderAbroadService.java
  20. 238 0
      src/main/java/com/szwl/service/impl/AirwallexServiceImpl.java
  21. 0 63
      src/main/java/com/szwl/service/impl/TAirwallexServiceImpl.java
  22. 31 0
      src/main/java/com/szwl/service/impl/TOrderAbroadServiceImpl.java
  23. 35 0
      src/main/resources/templates/hpp.html
  24. 59 0
      src/main/resources/templates/test.html
  25. 6 0
      src/main/resources/templates/tohpp.html
  26. 1 0
      src/test/java/com/szwl/ApplicationTests.java
  27. 1 1
      src/test/java/com/szwl/AutoGeneratorTests.java

+ 1 - 1
README.md

@@ -11,7 +11,7 @@ http://112.74.63.148:49001/
 增删查改
 使用参考 https://baomidou.com/pages/24112f/
 
-## swwager2 生成接口文档
+## swagger2 生成接口文档
 在controller 加上@Api(value = "/test", tags = {"controller描述"})
 接口方法上加上 @ApiOperation(value = "接口描述") 即可。
 文档地址:http://127.0.0.1:端口/doc.html

+ 10 - 0
pom.xml

@@ -62,6 +62,16 @@
 	</dependencyManagement>
 
 	<dependencies>
+		<!--访问静态资源-->
+<!--		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+			<version>3.0.1</version>
+		</dependency>-->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+		</dependency>
 		<!-- springcloud组件 start -->
 		<dependency>
 			<groupId>org.springframework.boot</groupId>

+ 2 - 0
src/main/java/com/szwl/Application.java

@@ -5,9 +5,11 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @EnableEurekaClient
 @EnableFeignClients
+@EnableScheduling
 @MapperScan("com.szwl.mapper")
 @SpringBootApplication(scanBasePackages = {"com.szwl", "cn.com.crbank.ommo.exception","cn.com.crbank.ommo.esclient"})
 public class Application{

+ 22 - 3
src/main/java/com/szwl/model/utils/AirwallexConstant.java

@@ -1,13 +1,32 @@
-package com.szwl.model.utils;
+package com.szwl.constant;
 
 import java.util.HashMap;
 import java.util.Map;
 
 /**
- * 汇聚支付 常量
+ * Airwallex、汇聚支付 常量
  */
 public class AirwallexConstant {
 
+
+//    /**
+//     * Airwallex支付结果回调
+//     */
+//    public final static String WEBHOOKS = "/webhook/paymentIntent";
+
+    public final static String QR_URL = "http://localhost:8080/shenze/#/hpp";
+
+    /**
+     * Webhooks密钥
+     */
+    public final static String WEBHOOKS_KEY = "whsec_KyZE-K9wIT2JBjpmE3Lf0JVm6q2rkyxX";
+    public final static String MODE = "payment";
+    /**
+     * staging | demo | prod
+     */
+    public final static String ENV = "demo";
+
+    public final static String BEARER = "Bearer ";
     /**
      * 商户密钥
      */
@@ -26,7 +45,7 @@ public class AirwallexConstant {
      */
     public final static String key = "8cb1d25eba7d4f3caecf1f0569150e04";
 
-
+    // 上面的是Airwallex
     /**
      * 汇聚公钥
      */

+ 7 - 0
src/main/java/com/szwl/constant/WebhooksEvents.java

@@ -0,0 +1,7 @@
+package com.szwl.constant;
+
+public class WebhooksEvents {
+    public final static String PAYMENT_INTENT_CREATED = "payment_intent.created";
+    public final static String PAYMENT_INTENT_SUCCEEDED = "payment_intent.succeeded";
+    public final static String PAYMENT_INTENT_CANCELLED = "payment_intent.cancelled";
+}

+ 133 - 17
src/main/java/com/szwl/controller/AirwallexPayController.java

@@ -1,46 +1,162 @@
 package com.szwl.controller;
 
-
-import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.szwl.constant.ResponseCodesEnum;
+import com.szwl.model.bean.PaymentIntentRequestBody;
+import com.szwl.model.bean.RefundRequestBody;
 import com.szwl.model.bo.R;
 import com.szwl.model.bo.ResponseModel;
-import com.szwl.model.entity.TAdmin;
-import com.szwl.model.query.TAdminParam;
+import com.szwl.model.entity.TOrderAbroad;
+import com.szwl.service.AirwallexService;
 import com.szwl.service.TAdminService;
-import com.szwl.service.TAirwallexService;
+import com.szwl.service.TOrderAbroadService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.Map;
+import java.util.Objects;
 
+import static com.szwl.constant.ResponseCodesEnum.L0006;
 
-@Api(value = "/api/airwallexPay", tags = {"空中云汇"})
+//@CrossOrigin
+@Api(value = "/api/airwallexPay", tags = {"空中云汇" })
 @RestController
 @RequestMapping("/api/airwallexPay")
+@Slf4j
 public class AirwallexPayController {
     @Value("${test.value:bbb}")
     private String testValue;
     @Autowired
     TAdminService tAdminService;
     @Autowired
-    TAirwallexService airwallexService;
-    @ApiOperation(value = "请求支付")
+    AirwallexService airwallexService;
+    @Resource
+    TOrderAbroadService tOrderAbroadService;
+
+
+    @ApiOperation(value = "获取token")
     @GetMapping("/getAccessToken")
-    public ResponseModel<?> getAccessToken(String id) {
+    @Scheduled(cron = "0 0/20 * * * ?") // 每20分钟执行一次,我删了入参String id
+    public ResponseModel<?> getAccessToken() {
         String accessToken = airwallexService.getAccessToken();
         return R.ok(accessToken);
     }
 
+    @ApiOperation(value = "Create a PaymentIntent")
+    @PostMapping("/createAPaymentIntent")
+    public R createAPaymentIntent(@RequestBody PaymentIntentRequestBody paymentIntentRequestBody) {
+        log.info("发起支付请求");
+        // 先获取token
+        String accessToken = airwallexService.getAccessToken();
+        // TODO: 创建订单
+        if(accessToken == null || "".equals(accessToken)) {
+            return R.fail(L0006);
+        }
+
+        BigDecimal amount = paymentIntentRequestBody.getAmount();
+        String currency = paymentIntentRequestBody.getCurrency();
+        String merchantOrderId = paymentIntentRequestBody.getMerchantOrderId();
+        String requestId = paymentIntentRequestBody.getRequestId();
+        Long productId = paymentIntentRequestBody.getProductId();
+        String productName = paymentIntentRequestBody.getProductName();
+        String clientId = paymentIntentRequestBody.getClientId();
+        Long equipmentId = paymentIntentRequestBody.getEquipmentId();
+
+        TOrderAbroad tOrderAbroad = new TOrderAbroad();
+        tOrderAbroad.setAmount(amount);
+        tOrderAbroad.setCurrency(currency);
+        tOrderAbroad.setMerchantOrderId(merchantOrderId);
+        tOrderAbroad.setRequestId(requestId);
+        tOrderAbroad.setProductId(productId);
+        tOrderAbroad.setProductName(productName);
+        tOrderAbroad.setClientId(clientId);
+        tOrderAbroad.setEquipmentId(equipmentId);
+        // TODO: 设置订单支付状态,0未支付,1已支付。
+        tOrderAbroad.setPayStatus(0);
+        // 将订单存入数据库
+        boolean save = tOrderAbroadService.save(tOrderAbroad);
+        System.out.println("将订单存入数据库:" + save);
+
+        // 返回payment intent id和client secret,拼接二维码链接给前端
+        String QR = airwallexService.caPaymentIntent(amount, currency, requestId, merchantOrderId);
+
+        // TODO: 设置分销
+
+        return R.ok().setData(QR);
+    }
+
+    @ApiOperation(value = "获取支付订单的信息,订单状态")
+    @GetMapping("/retrieveAPaymentIntent/{intId}")
+    public String retrieveAPaymentIntent(@PathVariable Long intId) {
+        log.info("查询支付订单信息");
+
+        String retrieve = airwallexService.retrieveAPaymentIntent(intId);
+        JSONObject jsonObject = JSON.parseObject(retrieve);
+        // 取消订单的原因
+//        String cancellationReason = jsonObject.getString("cancellation_reason");
+        // 订单状态
+        String status = jsonObject.getString("status");
+
+        return status;
+    }
+
+
+    @ApiOperation(value = "发起退款")
+    @PostMapping("/createARefund")
+    ResponseModel<String> createARefund(@RequestBody RefundRequestBody refundRequestBody) {
+        log.info("发起退款");
+        String requestId = refundRequestBody.getRequestId();
+//        QueryWrapper<TOrderAbroad> tOrderAbroadQueryWrapper = new QueryWrapper<>();
+//        tOrderAbroadQueryWrapper.eq("request_id", requestId);
+//        TOrderAbroad orderAbroad = tOrderAbroadService.getOne(tOrderAbroadQueryWrapper);
+
+        LambdaQueryWrapper<TOrderAbroad> lambdaQueryWrapper = Wrappers.lambdaQuery();
+        lambdaQueryWrapper.eq(TOrderAbroad::getRequestId, requestId);
+        TOrderAbroad orderAbroad = tOrderAbroadService.getOne(lambdaQueryWrapper);
+
+        if(orderAbroad == null) {
+            return R.fail("订单为空/error");
+        }
+        // 修改订单表
+//        SimpleDateFormat sdf = new SimpleDateFormat();
+//        sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
+//        orderAbroad.setRefundDate(sdf.format(date));
+
+//        LocalDateTime localDateTime = LocalDateTime.now();
+//        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+//        String format = localDateTime.format(dtf);
+        // 退款发起时间
+        orderAbroad.setRefundDate(new Date());
+
+
+        // 发起退款
+        String refund = airwallexService.createARefund(requestId);
+
+        JSONObject jsonObject = JSON.parseObject(refund);
+        String status = jsonObject.getString("status");
+
+        if (Objects.equals(status, "SUCCEEDED")) {
+            return R.ok("退款申请中,请稍候查询");
+        } else {
+            return R.ok("退款失败");
+        }
+    }
+
 }
 

+ 45 - 0
src/main/java/com/szwl/controller/HppController.java

@@ -0,0 +1,45 @@
+package com.szwl.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Controller
+public class HppController {
+
+    /**
+     * 重定向到 hpp
+     * @param env
+     * @param mode
+     * @param intendId
+     * @param clientSecret
+     * @param currency
+     * @param httpServletResponse
+     * @throws IOException
+     */
+    @RequestMapping("/toHpp")
+    public String toHpp(HttpServletRequest request,String env, String mode, String intendId, String clientSecret, String currency, HttpServletResponse httpServletResponse) throws IOException {
+//        String url = "http://localhost:63342/payServer/templates/test.html?" + env + "&mode=" + mode + "&intend_id=" + intendId + "&client_secret=" + clientSecret + "&currency=" + currency;
+//        httpServletResponse.sendRedirect(url);
+        request.setAttribute("intendId", intendId);
+        request.setAttribute("clientSecret", clientSecret);
+        request.setAttribute("currency", currency);
+        return "test";
+    }
+
+//
+    @GetMapping("/hpp")
+    public String hpp(String env, String mode, String intendId, String clientSecret, String currency, Model model) {
+        model.addAttribute("env", env);
+        model.addAttribute("mode", mode);
+        model.addAttribute("intend_id", intendId);
+        model.addAttribute("client_secret", clientSecret);
+        model.addAttribute("currency", currency);
+        return "redirect: http://112.74.63.148:49011/#/hpp?env=" + env + "&mode=" + mode + "&intend_id=" + intendId + "&client_secret=" + clientSecret + "&currency=" + currency;
+    }
+}

+ 21 - 0
src/main/java/com/szwl/controller/TOrderAbroadController.java

@@ -0,0 +1,21 @@
+package com.szwl.controller;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ *  前端控制器
+ * </p>
+ *
+ * @author wuhs
+ * @since 2023-04-17
+ */
+@RestController
+@RequestMapping("/tOrderAbroad")
+public class TOrderAbroadController {
+
+}
+

+ 163 - 0
src/main/java/com/szwl/controller/WebhookController.java

@@ -0,0 +1,163 @@
+package com.szwl.controller;
+
+import cn.com.sand.third.org.apache.commons.codec.digest.HmacUtils;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.szwl.model.bo.R;
+import com.szwl.model.bo.ResponseModel;
+import com.szwl.model.entity.TOrderAbroad;
+import com.szwl.model.utils.PushUtils;
+import com.szwl.service.AirwallexService;
+import com.szwl.service.TEquipmentService;
+import com.szwl.service.TOrderAbroadService;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.sql.Wrapper;
+import java.util.ArrayList;
+
+import static com.szwl.constant.AirwallexConstant.WEBHOOKS_KEY;
+import static com.szwl.constant.AirwallexConstant.ENV;
+
+
+@Controller
+@Slf4j
+public class WebhookController {
+
+    @Resource
+    TOrderAbroadService tOrderAbroadService;
+
+    @Resource
+    TEquipmentService tEquipmentService;
+//    @Resource
+//    AirwallexService airwallexService;
+
+//    public ResponseModel<?> createAWebhook(String[] envents, String requiredId, String url, String version) {
+//        log.info("创建webhook");
+//        String accessToken = airwallexService.getAccessToken();
+//
+//        airwallexService.caWebhook(envents, requiredId, url, version);
+//
+//        return
+//    }
+
+    /**
+     * 获取退款的回调
+     * @param request
+     * @param response
+     * @return
+     */
+    @ResponseBody
+    @PostMapping("/webhook/refund")
+    public String receiveRefund(HttpServletRequest request, @RequestBody String payload, HttpServletResponse response) {
+
+        String responseBody = "";
+
+        StringBuilder valueToDigest = new StringBuilder();
+// Get the timestamp from header
+        String timestamp = request.getHeader("x-timestamp");
+        valueToDigest.append(timestamp);
+        valueToDigest.append(payload);
+
+// Get the signature from header
+        String signature = request.getHeader("x-signature");
+
+// Get your secret
+        String secret = getSecret();
+
+        if (HmacUtils.hmacSha256Hex(secret, valueToDigest.toString()).equals(signature)) {
+
+// Do something with event
+
+            response.setStatus(HttpServletResponse.SC_OK);
+        } else {
+// Invalid signature
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            responseBody = "failed to verify the signature";
+        }
+
+        return responseBody;
+    }
+
+
+
+
+    /**
+     * 获取支付意向的回调
+     * @param request
+     * @param payload
+     * @param response
+     * @return
+     */
+    @ResponseBody
+    @PostMapping("/webhook/paymentIntent")
+    public String receivePaymentIntent(HttpServletRequest request, @RequestBody String payload, HttpServletResponse response) {
+        String responseBody = "";
+
+        StringBuilder valueToDigest = new StringBuilder();
+// Get the timestamp from header
+        String timestamp = request.getHeader("x-timestamp");
+        valueToDigest.append(timestamp);
+        valueToDigest.append(payload);
+
+// Get the signature from header
+        String signature = request.getHeader("x-signature");
+
+// Get your secret
+        String secret = getSecret();
+
+        if (HmacUtils.hmacSha256Hex(secret, valueToDigest.toString()).equals(signature)) {
+
+// Do something with event
+
+            response.setStatus(HttpServletResponse.SC_OK);
+            responseBody = "payment intent ok";
+
+            // 如果用户支付成功,将订单支付状态改成 1已支付。
+            JSONObject jsonObject = JSON.parseObject(payload);
+            String data = jsonObject.getString("data");
+            JSONObject jsonObject1 = JSON.parseObject(data);
+            String dataObject = jsonObject1.getString("object");
+            JSONObject jsonObject2 = JSON.parseObject(dataObject);
+            String requestId = jsonObject2.getString("request_id");
+            QueryWrapper<TOrderAbroad> tOrderAbroadQueryWrapper = new QueryWrapper<>();
+            tOrderAbroadQueryWrapper.eq("request_id",requestId);
+            TOrderAbroad orderAbroad = tOrderAbroadService.getOne(tOrderAbroadQueryWrapper);
+            System.out.println(orderAbroad);
+
+            orderAbroad.setPayStatus(1);
+            tOrderAbroadService.updateById(orderAbroad);
+
+            // 通知做糖
+            JSONObject kindData = new JSONObject();
+            kindData.put("sn", orderAbroad.getMerchantOrderId());
+            kindData.put("productName", orderAbroad.getProductName());
+            tEquipmentService.sentMessage(orderAbroad.getClientId(), PushUtils.buildJson("pay_success", kindData.toString()).toString());
+
+        } else {
+// Invalid signature
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            responseBody = "failed to verify the signature";
+
+            // TODO: 验签失败,先retrieve手动获取订单信息,如果成功,走上面那一套,如果失败不做处理
+
+
+
+        }
+        System.out.println(response);
+        System.out.println(responseBody);
+        return responseBody;
+    }
+
+    private String getSecret() {
+        return WEBHOOKS_KEY;
+    }
+}

+ 16 - 0
src/main/java/com/szwl/mapper/TOrderAbroadMapper.java

@@ -0,0 +1,16 @@
+package com.szwl.mapper;
+
+import com.szwl.model.entity.TOrderAbroad;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author wuhs
+ * @since 2023-04-17
+ */
+public interface TOrderAbroadMapper extends BaseMapper<TOrderAbroad> {
+
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 52 - 0
src/main/java/com/szwl/mapper/xml/TOrderAbroadMapper.xml


+ 14 - 0
src/main/java/com/szwl/model/bean/PaymentIntent.java

@@ -0,0 +1,14 @@
+package com.szwl.model.bean;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class PaymentIntent {
+
+    private String client_secret;
+
+    private String paymentIntentId;
+
+}

+ 25 - 0
src/main/java/com/szwl/model/bean/PaymentIntentRequestBody.java

@@ -0,0 +1,25 @@
+package com.szwl.model.bean;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class PaymentIntentRequestBody {
+
+    private BigDecimal amount;
+
+    private String currency;
+
+    private String merchantOrderId;
+
+    private String requestId;
+
+    private Long productId;
+
+    private String productName;
+
+    private String clientId;
+
+    private Long equipmentId;
+}

+ 15 - 0
src/main/java/com/szwl/model/bean/RefundRequestBody.java

@@ -0,0 +1,15 @@
+package com.szwl.model.bean;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class RefundRequestBody {
+
+    private BigDecimal amount;
+
+    private String requestId;
+
+//    private String paymentIntentId;
+}

+ 4 - 0
src/main/java/com/szwl/model/bo/R.java

@@ -3,7 +3,11 @@ package com.szwl.model.bo;
 
 import com.szwl.constant.ResponseCodesEnum;
 import com.szwl.exception.MyException;
+import lombok.Data;
+import lombok.experimental.Accessors;
 
+//@Data
+//@Accessors(chain = true)
 public class R<T> {
 
     public static <T> ResponseModel<T> ok(T data) {

+ 146 - 0
src/main/java/com/szwl/model/entity/TOrderAbroad.java

@@ -0,0 +1,146 @@
+package com.szwl.model.entity;
+
+import java.math.BigDecimal;
+import com.baomidou.mybatisplus.annotation.IdType;
+import java.util.Date;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.io.Serializable;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.Column;
+
+/**
+ * <p>
+ * 
+ * </p>
+ *
+ * @author wuhs
+ * @since 2023-04-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@ApiModel(value="TOrderAbroad对象", description="")
+public class TOrderAbroad implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+//    @CreationTimestamp
+    @Column(nullable = false, updatable = false, columnDefinition="TIMESTAMP default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP")
+    @ApiModelProperty(value = "订单创建时间")
+    private Date createDate;
+
+    @ApiModelProperty(value = "订单修改时间")
+    private Date modifyDate;
+
+    @ApiModelProperty(value = "商户id,用户判断权限")
+    private Long adminId;
+
+    @ApiModelProperty(value = "平台分账")
+    private BigDecimal adminProportion;
+
+    @ApiModelProperty(value = "一级分销商户id")
+    private Long agencyId;
+
+    @ApiModelProperty(value = "分账比例,中介分账")
+    private BigDecimal agencyProportion;
+
+    @ApiModelProperty(value = "分销逻辑")
+    private String altInfo;
+
+    @ApiModelProperty(value = "设备编号")
+    private String clientId;
+
+    @ApiModelProperty(value = "设备id")
+    private Long equipmentId;
+
+    @ApiModelProperty(value = "支付方式")
+    private String frpCode;
+
+    @ApiModelProperty(value = "二级分销商户id")
+    private Long merchantId;
+
+    @ApiModelProperty(value = "二级分销商分账比例")
+    private BigDecimal merchantProportion;
+
+    @ApiModelProperty(value = "支付时间")
+    private Date payDate;
+
+    @ApiModelProperty(value = "三级分销商户id")
+    private Long personageId;
+
+    @ApiModelProperty(value = "三级分销商分账比例")
+    private BigDecimal personageProportion;
+
+    @ApiModelProperty(value = "金额")
+    private BigDecimal amount;
+
+    @ApiModelProperty(value = "商品id")
+    private Long productId;
+
+    @ApiModelProperty(value = "商品数量,每笔订单中有几个商品")
+    private Integer productNum;
+
+    @ApiModelProperty(value = "商品名称,烈焰红唇")
+    private String productName;
+
+    @ApiModelProperty(value = "商品编号,A06")
+    private String productNo;
+
+    @ApiModelProperty(value = "退款时间")
+    private Date refundDate;
+
+    @ApiModelProperty(value = "商品描述,{xxx,1个;xxx,2个}")
+    private String productDesc;
+
+    @ApiModelProperty(value = "支付状态")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "分账方类型,0:分账方2个,1:分账方3个,3:分账方4个,3:分账方超4个;")
+    private Integer proportionType;
+
+    @ApiModelProperty(value = "支付流水号")
+    private String trxNo;
+
+    @ApiModelProperty(value = "退款流水号")
+    private String refundTrxNo;
+
+    @ApiModelProperty(value = "是否发送es")
+    private String es;
+
+    @ApiModelProperty(value = "退款金额")
+    private BigDecimal refundAmount;
+
+    @ApiModelProperty(value = "标记")
+    private String note;
+
+    @ApiModelProperty(value = "分账详情,当分账超过4方时,这个字段才启用")
+    private String proportionDesc;
+
+    @ApiModelProperty(value = "营销金额")
+    private BigDecimal marketingAmount;
+
+    @ApiModelProperty(value = "退款营销金额")
+    private BigDecimal refundMarketingAmount;
+
+    @ApiModelProperty(value = "订单状态;0:排队中,1:正在制作,2:已制作")
+    private String orderStatus;
+
+    @ApiModelProperty(value = "商品数量")
+    private Integer productNumber;
+
+    @ApiModelProperty(value = "货币")
+    private String currency;
+
+    @ApiModelProperty(value = "商户订单id,唯一订单编号,类似于order的sn")
+    private String merchantOrderId;
+
+    @ApiModelProperty(value = "商户唯一请求id")
+    private String requestId;
+
+}

+ 56 - 0
src/main/java/com/szwl/service/AirwallexService.java

@@ -0,0 +1,56 @@
+package com.szwl.service;
+
+import com.szwl.model.bo.R;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.util.Map;
+
+
+public interface AirwallexService  {
+
+
+    /**
+     * 获取 access token
+     * 30分钟过期
+     */
+    String getAccessToken();
+
+    /**
+     * 获取某一笔订单的信息 Retrieve a PaymentIntent
+     * @param intId
+     * @return
+     */
+    String retrieveAPaymentIntent(Long intId);
+
+
+    R getFormSchema();
+
+    R getAPISchema();
+
+    String createBeneficiary(Map<String, Object> requestBody);
+
+    String createPayout();
+
+    R getPaymentById(String paymentId);
+
+    String caPaymentIntent(BigDecimal amount, String currency, String requestId, String merchantOrderId);
+
+    /**
+     * 创建一笔退款 Create a Refund
+     * @param requestId
+     * @return
+     */
+    String createARefund(String requestId);
+
+//    /**
+//     * 退款结果回调
+//     * @param request
+//     * @param response
+//     * @return
+//     */
+//    String refundWebhooks(HttpServletRequest request, HttpServletResponse response);
+
+//    void caWebhook(String[] envents, String requiredId, String url, String version);
+}

+ 0 - 22
src/main/java/com/szwl/service/TAirwallexService.java

@@ -1,22 +0,0 @@
-package com.szwl.service;
-
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.szwl.model.entity.TOrder;
-
-/**
- * <p>
- *  服务类
- * </p>
- *
- * @author
- * @since 2022-06-17
- */
-public interface TAirwallexService  {
-
-    /**
-     * 获取 access token
-     * 30分钟过期
-     */
-    String getAccessToken();
-
-}

+ 20 - 0
src/main/java/com/szwl/service/TOrderAbroadService.java

@@ -0,0 +1,20 @@
+package com.szwl.service;
+
+import com.szwl.model.entity.TOrderAbroad;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author wuhs
+ * @since 2023-04-17
+ */
+public interface TOrderAbroadService extends IService<TOrderAbroad> {
+
+//    boolean updatePayStatusById(TOrderAbroad tOrderAbroad);
+}

+ 238 - 0
src/main/java/com/szwl/service/impl/AirwallexServiceImpl.java

@@ -0,0 +1,238 @@
+package com.szwl.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.szwl.constant.AirwallexConstant;
+import com.szwl.model.bean.PaymentIntent;
+import com.szwl.model.bean.PaymentIntentRequestBody;
+import com.szwl.model.bo.R;
+import com.szwl.model.entity.TOrderAbroad;
+import com.szwl.model.utils.HttpClientSslUtils;
+import com.szwl.service.AirwallexService;
+import com.szwl.service.TOrderAbroadService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.entity.ContentType;
+import org.apache.http.message.BasicHeader;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.math.BigDecimal;
+import java.sql.Array;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author wuhs
+ * @since 2022-07-12
+ */
+@Service
+@Slf4j
+public class AirwallexServiceImpl implements AirwallexService {
+
+    @Resource
+    TOrderAbroadService tOrderAbroadService;
+
+
+    @Override
+    public String getAccessToken() {
+//        HttpResponse<String> response = Unirest.post("https://api-demo.airwallex.com/api/v1/authentication/login")
+//                .header("Content-Type", "application/json")
+//                .header("x-client-id", "W_ORsgAFTiuA9k2KuqZt8A")
+//                .header("x-api-key", "8ac97c856c6d6cae7eb8fd05511f7a165be798d032381cb8026de7b4aa9aaee2e6312a8888a3474d783a40913ab6b55d")
+//                .body("{
+//    }").asString();
+        String url = AirwallexConstant.url+"/api/v1/authentication/login";
+        List<BasicHeader> headers = new ArrayList<>();
+        BasicHeader header1 = new BasicHeader("x-client-id",AirwallexConstant.clientid);
+        BasicHeader header2 = new BasicHeader("x-api-key",AirwallexConstant.apiKey);
+        headers.add(header1);
+        headers.add(header2);
+        Map<String,String> map = new HashMap<>();
+        String data = JSON.toJSONString(map);
+        String response=null;
+        try {
+            response = HttpClientSslUtils.doPost(url, data,ContentType.APPLICATION_JSON,headers);
+            JSONObject resultJson = JSONObject.parseObject(response);
+            String token = resultJson.getString("token");
+            if(StringUtils.isNotEmpty(token)){
+                System.out.println("token="+token);
+                return token;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return response;
+    }
+
+
+    @Override
+    public String caPaymentIntent(BigDecimal amount, String currency, String requestId, String merchantOrderId) {
+        // 调用付款意向 api,返回payment intent id和client secret
+        // 请求头
+        String url = AirwallexConstant.url+"/api/v1/pa/payment_intents/create";
+        List<BasicHeader> headers = new ArrayList<>();
+        BasicHeader header1 = new BasicHeader("Authorization",AirwallexConstant.BEARER + this.getAccessToken());
+        headers.add(header1);
+        // body参数
+        Map<String, Object> bodyMap = new HashMap<>();
+        bodyMap.put("amount", amount);
+        bodyMap.put("currency", currency);
+        bodyMap.put("request_id", requestId);
+        bodyMap.put("merchant_order_id", merchantOrderId);
+
+        String data = JSON.toJSONString(bodyMap);
+        log.info("请求body参数:" +data);
+
+        Map<String, Object> responseMap = new HashMap<>();
+        String resp = null;
+        try {
+            // 发送请求,获取响应结果
+            resp = HttpClientSslUtils.doPost(url, data, ContentType.APPLICATION_JSON, headers);
+            System.out.println(resp);
+
+            responseMap = JSON.parseObject(resp);
+//            return resp;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        String paymentIntentId = (String) responseMap.get("id");
+        String clientSecret = (String) responseMap.get("client_secret");
+
+        String env = AirwallexConstant.ENV;
+        String mode = AirwallexConstant.MODE;
+
+//        String locale = "it";
+        String qrUrl = AirwallexConstant.QR_URL;
+        return qrUrl + "?intent_id=" + paymentIntentId + "&client_secret=" + clientSecret + "&currency=" + currency;
+    }
+
+    @Override
+    public String createARefund(String requestId) {
+
+        // 请求头
+        String url = AirwallexConstant.url + "/api/v1/pa/refunds/create";
+        List<BasicHeader> headers = new ArrayList<>();
+        BasicHeader header1 = new BasicHeader("Authorization", AirwallexConstant.BEARER + this.getAccessToken());
+        headers.add(header1);
+
+        // body参数
+        Map<String, Object> bodyMap = new HashMap<>();
+        bodyMap.put("request_id", requestId);
+//        bodyMap.put("amount", amount); // 退款金额
+//        bodyMap.put("reason", reason); // 退款原因
+        String data = JSON.toJSONString(bodyMap);
+
+        String resp = null;
+        try {
+            HttpClientSslUtils.doPost(url, data, ContentType.APPLICATION_JSON, headers);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return resp;
+    }
+
+//    @Override
+//    public String refundWebhooks(HttpServletRequest request, HttpServletResponse response) {
+//        return null;
+//    }
+
+
+    /**
+     * 获取某笔支付订单的信息
+     * @param intId
+     * @return
+     */
+    @Override
+    public String retrieveAPaymentIntent(Long intId) {
+
+        String url = AirwallexConstant.url + "/api/v1/pa/payment_intents/{id}";
+        // 请求头
+        List<BasicHeader> headers = new ArrayList<>();
+        new BasicHeader("Authorization", AirwallexConstant.BEARER + this.getAccessToken());
+        // 请求体
+//        Map<String, Object> bodyMap = new HashMap<>();
+//        String data = JSON.toJSONString(bodyMap);
+
+        String resp = null;
+        try {
+            resp = HttpClientSslUtils.doGet(url, ContentType.APPLICATION_JSON, headers);
+
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return resp;
+    }
+
+    @Override
+    public R getFormSchema() {
+        // POST /api/v1/beneficiary_form_schemas/generate 根据国家/付款方式查询表单字段,返回前端所需表单信息(对接付款国较多)
+//        https://www.airwallex.com/docs/api#/Payouts/Beneficiaries/_api_v1_beneficiary_form_schemas_generate/post
+//        HttpResponse<String> response = Unirest.post("https://api-demo.airwallex.com/api/v1/beneficiary_form_schemas/generate")
+//                .header("Content-Type", "application/json")
+//                .header("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6WyJ1c2VyIl0sImlhdCI6MTQ4ODQxNTI1NywiZXhwIjoxNDg4NDE1MjY3fQ.UHqau03y5kEk5lFbTp7J4a-U6LXsfxIVNEsux85hj-Q")
+//                .header("headers", "[object Object]")
+//                .body("{
+//                        \"account_currency\": \"USD\",
+//                        \"bank_country_code\": \"US\",
+//                        \"entity_type\": \"PERSONAL\",
+//                        \"local_clearing_system\": \"ACH\",
+//                        \"payment_method\": \"LOCAL\"
+//    }")
+//            .asString();
+        return null;
+
+    }
+
+    @Override
+    public R getAPISchema() {
+        // POST /api/v1/beneficiary_api_schemas/generate 动态获取指定国家、支付方式,返回创建收款人请求所需字段
+//        https://www.airwallex.com/docs/api#/Payouts/Beneficiaries/_api_v1_beneficiary_api_schemas_generate/post
+        return null;
+    }
+
+    @Override
+    public String createBeneficiary (Map<String, Object> requestBody) {
+        // POST /api/v1/beneficiaries/create 创建收款人,返回收款人ID
+//        https://www.airwallex.com/docs/api#/Payouts/Beneficiaries/_api_v1_beneficiaries_create/post
+        Map beneficiary = new HashMap();
+        Object address = null;
+
+        ArrayList paymentMethods = new ArrayList<>();
+
+        String beneficiaryId = "fsdfsd";
+        return beneficiaryId;
+    }
+
+    @Override
+    public String createPayout() {
+        // POST /api/v1/payments/create 创建付款交易,返回创建结果paymentID
+//        https://www.airwallex.com/docs/api#/Payouts/Payments/_api_v1_payments_create/post
+        String paymentId = "111";
+        return paymentId;
+    }
+
+
+    @Override
+    public R getPaymentById(String paymentId) {
+        // GET /api/v1/payments/{payment_id} 查询支付结果,返回支付结果状态
+//        https://www.airwallex.com/docs/api#/Payouts/Payments/_api_v1_payments__payment_id_/get
+        return null;
+    }
+
+
+    // Cancel、Retry
+
+}

+ 0 - 63
src/main/java/com/szwl/service/impl/TAirwallexServiceImpl.java

@@ -1,63 +0,0 @@
-package com.szwl.service.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.szwl.mapper.TAreaMapper;
-import com.szwl.model.entity.TArea;
-import com.szwl.model.utils.AirwallexConstant;
-import com.szwl.model.utils.HttpClientSslUtils;
-import com.szwl.service.TAirwallexService;
-import com.szwl.service.TAreaService;
-import org.apache.commons.lang.StringUtils;
-import org.apache.http.entity.ContentType;
-import org.apache.http.message.BasicHeader;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * <p>
- *  服务实现类
- * </p>
- *
- * @author wuhs
- * @since 2022-07-12
- */
-@Service
-public class TAirwallexServiceImpl implements TAirwallexService {
-
-    @Override
-    public String getAccessToken() {
-//        HttpResponse<String> response = Unirest.post("https://api-demo.airwallex.com/api/v1/authentication/login")
-//                .header("Content-Type", "application/json")
-//                .header("x-client-id", "W_ORsgAFTiuA9k2KuqZt8A")
-//                .header("x-api-key", "8ac97c856c6d6cae7eb8fd05511f7a165be798d032381cb8026de7b4aa9aaee2e6312a8888a3474d783a40913ab6b55d")
-//                .body("{
-//    }").asString();
-        String url = AirwallexConstant.url+"/api/v1/authentication/login";
-        List<BasicHeader> headers = new ArrayList<>();
-        BasicHeader header1 = new BasicHeader("x-client-id",AirwallexConstant.clientid);
-        BasicHeader header2 = new BasicHeader("x-api-key",AirwallexConstant.apiKey);
-        headers.add(header1);
-        headers.add(header2);
-        Map<String,String> map = new HashMap<>();
-        String data = JSON.toJSONString(map);
-        String response=null;
-        try {
-            response = HttpClientSslUtils.doPost(url, data,ContentType.APPLICATION_JSON,headers);
-            JSONObject resultJson = JSONObject.parseObject(response);
-            String token = resultJson.getString("token");
-            if(StringUtils.isNotEmpty(token)){
-                System.out.println("token="+token);
-                return token;
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        return response;
-    }
-}

+ 31 - 0
src/main/java/com/szwl/service/impl/TOrderAbroadServiceImpl.java

@@ -0,0 +1,31 @@
+package com.szwl.service.impl;
+
+import com.szwl.model.entity.TOrderAbroad;
+import com.szwl.mapper.TOrderAbroadMapper;
+import com.szwl.service.TOrderAbroadService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author wuhs
+ * @since 2023-04-17
+ */
+@Service
+public class TOrderAbroadServiceImpl extends ServiceImpl<TOrderAbroadMapper, TOrderAbroad> implements TOrderAbroadService {
+
+    /**
+     * 根据 erquestId 将订单的支付状态 payStatus 改成1
+     *
+     * @return
+     */
+//    @Override
+//    public boolean updatePayStatusById(TOrderAbroad tOrderAbroad) {
+//
+//
+//        return false;
+//    }
+}

+ 35 - 0
src/main/resources/templates/hpp.html

@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head lang="en">
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>Airwallex Checkout Playground</title>
+    <!-- STEP #1: Import airwallex-payment-elements bundle -->
+    <script src="https://checkout.airwallex.com/assets/elements.bundle.min.js"></script>
+</head>
+<body>
+<h1>Hosted payment page (HPP) integration</h1>
+<!-- STEP #3: Add a checkout button -->
+<button id="hpp">Redirect to HPP for checkout</button>
+
+<script th:inline="javascript">
+    // STEP #2: Initialize the Airwallex package with the appropriate environment
+    Airwallex.init({
+        env: 'demo', // Setup which Airwallex env('staging' | 'demo' | 'prod') to integrate with
+        origin: window.location.origin, // Setup your event target to receive the browser events message
+    });
+    console.log('${paymentIntentId}');
+    document.getElementById('hpp').addEventListener('click', () => {
+        // STEP #4: Add a button handler to trigger the redirect to HPP
+        Airwallex.redirectToCheckout({
+            env: 'demo', // Which env('staging' | 'demo' | 'prod') you would like to integrate with
+            mode: 'payment',
+            intent_id: [[${param.intentId}]],
+            client_secret: 'replace-with-your-client-secret',
+            currency: 'replace-with-your-currency',
+            country_code: 'replace-with-your-country-code'
+        });
+    });
+</script>
+</body>
+</html>

+ 59 - 0
src/main/resources/templates/test.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head lang="en">
+  <meta charset="utf-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1" />
+  <title>Airwallex Checkout Playground</title>
+
+  <!-- STEP #1: Import airwallex-payment-elements bundle -->
+  <script src="https://checkout.airwallex.com/assets/elements.bundle.min.js"></script>
+</head>
+<body>
+<div >
+
+  <h1>Hosted payment page (HPP) integration</h1>
+  <!-- STEP #3: Add a checkout button -->
+  <button id="hpp">Redirect to HPP for checkout</button>
+</div>
+<script>
+
+  function getQueryString(name) {
+    var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
+    var r = window.location.search.substr(1).match(reg);
+    if (r != null) {
+      return unescape(r[2]);
+    }
+    return null;
+  }
+  var intendId=""
+  var clientSecret=""
+  var currency=""
+  window.onLoad=function(){}
+  window.onload=function()
+  {
+
+     intendId = getQueryString("intendId")
+     clientSecret = getQueryString("clientSecret")
+     currency = getQueryString("currency")
+  }
+
+
+  // STEP #2: Initialize the Airwallex package with the appropriate environment
+  Airwallex.init({
+    env: 'demo', // Setup which Airwallex env('staging' | 'demo' | 'prod') to integrate with
+    origin: window.location.origin, // Setup your event target to receive the browser events message
+  });
+  document.getElementById('hpp').addEventListener('click', () => {
+    // STEP #4: Add a button handler to trigger the redirect to HPP
+    Airwallex.redirectToCheckout({
+      env: 'demo', // Which env('staging' | 'demo' | 'prod') you would like to integrate with
+      mode: 'payment',
+      intent_id: intendId,
+      client_secret: clientSecret,
+      currency: currency,
+      country_code: 'replace-with-your-country-code'
+    });
+  });
+</script>
+</body>
+</html>

+ 6 - 0
src/main/resources/templates/tohpp.html

@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<meta http-equiv="refresh" content="0;url=http://localhost:8080/?aaa=[[${aaa}]]&p=u3219">
+</body>
+</html>

+ 1 - 0
src/test/java/com/szwl/ApplicationTests.java

@@ -8,4 +8,5 @@ import org.springframework.boot.test.context.SpringBootTest;
 class ApplicationTests {
 
 
+
 }

+ 1 - 1
src/test/java/com/szwl/AutoGeneratorTests.java

@@ -47,7 +47,7 @@ class AutoGeneratorTests {
 		strategyConfig
 //				.setCapitalMode(true)//设置全局大写命名
 				.setInclude(new String[]{
-						"t_weixin_payment_mch"
+						""
 				})//只会生成该表
 				.setEntityLombokModel(true)//实体类生成之后自动添加lombok注解
 				.setNaming(NamingStrategy.underline_to_camel)//数据库表映射到实体的命名策略