diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/controller/CallbackController.java b/mt-pay/src/main/java/com/mtkj/mtpay/controller/CallbackController.java new file mode 100644 index 0000000..1512d3f --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/controller/CallbackController.java @@ -0,0 +1,136 @@ +package com.mtkj.mtpay.controller; + +import com.mtkj.mtpay.service.CallbackService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 回调控制器 + * 接收PingPong支付回调通知 + */ +@Slf4j +@RestController +@RequestMapping("/api/callback") +@RequiredArgsConstructor +public class CallbackController { + + private final CallbackService callbackService; + + /** + * PingPong支付回调接口 + */ + @PostMapping("/pingpong") + public ResponseEntity> pingpongCallback(@RequestBody Map callbackData) { + log.info("收到PingPong回调通知"); + + try { + boolean success = callbackService.handleCallback(callbackData); + + if (success) { + return ResponseEntity.ok(Map.of("code", "0000", "message", "success")); + } else { + return ResponseEntity.badRequest().body(Map.of("code", "9999", "message", "处理失败")); + } + + } catch (Exception e) { + log.error("处理回调失败", e); + return ResponseEntity.badRequest().body(Map.of("code", "9999", "message", "处理异常: " + e.getMessage())); + } + } + + /** + * 支付结果重定向页面(用户支付完成后跳转) + */ + @GetMapping("/result") + public ResponseEntity paymentResult( + @RequestParam(required = false) String merchantTransactionId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String message + ) { + log.info("支付结果页面,商户订单号: {}, 状态: {}", merchantTransactionId, status); + + String html = """ + + + + + + 支付结果 + + + +
+

支付%s

+

订单号: %s

+

状态: %s

+ %s +
+ + + """; + + String statusClass = "success"; + String statusText = "成功"; + String messageHtml = ""; + + if (status != null) { + switch (status.toUpperCase()) { + case "SUCCESS", "SUCCESSFUL" -> { + statusClass = "success"; + statusText = "成功"; + } + case "FAILED", "FAILURE" -> { + statusClass = "failed"; + statusText = "失败"; + } + case "REVIEW" -> { + statusClass = "pending"; + statusText = "审核中"; + } + default -> { + statusClass = "pending"; + statusText = "处理中"; + } + } + } + + if (message != null && !message.isEmpty()) { + messageHtml = "

说明: " + message + "

"; + } + + String finalHtml = String.format(html, statusClass, statusText, + merchantTransactionId != null ? merchantTransactionId : "未知", + status != null ? status : "未知", + messageHtml); + + return ResponseEntity.ok() + .header("Content-Type", "text/html;charset=UTF-8") + .body(finalHtml); + } +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/request/CheckoutRequestDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/request/CheckoutRequestDTO.java new file mode 100644 index 0000000..a30612f --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/request/CheckoutRequestDTO.java @@ -0,0 +1,80 @@ +package com.mtkj.mtpay.dto.request; + +import com.mtkj.mtpay.dto.RiskInfoDTO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import lombok.Data; + +import java.io.Serializable; + +/** + * Checkout接口请求DTO + */ +@Data +public class CheckoutRequestDTO implements Serializable { + + @NotBlank(message = "商户店铺编号不能为空") + @Size(max = 64, message = "商户店铺编号长度不能超过64") + private String accId; + + @NotBlank(message = "交易金额不能为空") + @Pattern(regexp = "^\\d+(\\.\\d{2})?$", message = "交易金额格式不正确,需保留两位小数") + @Size(max = 12, message = "交易金额长度不能超过12") + private String amount; + + @NotBlank(message = "交易币种不能为空") + @Size(min = 3, max = 3, message = "交易币种必须为3位ISO 4217币种代码") + private String currency; + + @NotBlank(message = "商户订单号不能为空") + @Size(max = 64, message = "商户订单号长度不能超过64") + private String merchantTransactionId; + + @NotBlank(message = "交易类型不能为空") + @Pattern(regexp = "^(SALE|AUTH)$", message = "交易类型必须为SALE或AUTH") + private String paymentType; + + @Size(max = 64, message = "支付方式长度不能超过64") + private String paymentBrand; + + @NotBlank(message = "结果重定向URL不能为空") + @Size(max = 255, message = "结果重定向URL长度不能超过255") + private String shopperResultUrl; + + @NotBlank(message = "取消重定向URL不能为空") + @Size(max = 255, message = "取消重定向URL长度不能超过255") + private String shopperCancelUrl; + + @NotBlank(message = "签名类型不能为空") + @Pattern(regexp = "^(MD5|SHA256)$", message = "签名类型必须为MD5或SHA256") + private String signType; + + @NotBlank(message = "签名不能为空") + @Size(max = 255, message = "签名长度不能超过255") + private String sign; + + @Size(max = 64, message = "商户用户ID长度不能超过64") + private String merchantUserId; + + @Size(min = 2, max = 4, message = "语言代码长度必须在2-4之间") + private String language = "en"; + + @Pattern(regexp = "^(Y|N)$", message = "3DS参数必须为Y或N") + private String threeDSecure = "N"; + + @Size(max = 64, message = "原始商户交易订单号长度不能超过64") + private String primaryMerchantTransactionId; + + private Integer periodsNum; + + @NotNull(message = "风控信息不能为空") + @Valid + private RiskInfoDTO riskInfo; + + @Size(max = 255, message = "异步通知地址长度不能超过255") + private String notificationUrl; + + @Size(max = 500, message = "备注长度不能超过500") + private String remark; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/response/CheckoutResponseDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/response/CheckoutResponseDTO.java new file mode 100644 index 0000000..ac242d4 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/response/CheckoutResponseDTO.java @@ -0,0 +1,37 @@ +package com.mtkj.mtpay.dto.response; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Checkout接口响应DTO + */ +@Data +public class CheckoutResponseDTO implements Serializable { + + private String clientId; + + private String accId; + + private String merchantTransactionId; + + private String code; + + private String description; + + private String token; + + private String paymentUrl; + + private String innerJsUrl; + + private String signType; + + private String sign; + + private String remark; + + private String paymentHtml; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/AirlineDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/AirlineDTO.java new file mode 100644 index 0000000..e7dbd25 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/AirlineDTO.java @@ -0,0 +1,15 @@ +package com.mtkj.mtpay.dto.risk; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 航空信息DTO + */ +@Data +public class AirlineDTO implements Serializable { + + // 根据实际需求添加字段 +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/BillingDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/BillingDTO.java new file mode 100644 index 0000000..4ffd4e4 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/BillingDTO.java @@ -0,0 +1,31 @@ +package com.mtkj.mtpay.dto.risk; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 账单地址DTO + */ +@Data +public class BillingDTO implements Serializable { + + private String firstName; + + private String lastName; + + private String phone; + + private String email; + + private String street; + + private String postcode; + + private String city; + + private String state; + + private String country; +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/CarRentalDTO.java b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/CarRentalDTO.java new file mode 100644 index 0000000..0c44084 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/dto/risk/CarRentalDTO.java @@ -0,0 +1,15 @@ +package com.mtkj.mtpay.dto.risk; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 租车信息DTO + */ +@Data +public class CarRentalDTO implements Serializable { + + // 根据实际需求添加字段 +} + diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/CallbackService.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/CallbackService.java new file mode 100644 index 0000000..0de0eaf --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/CallbackService.java @@ -0,0 +1,18 @@ +package com.mtkj.mtpay.service; + +import java.util.Map; + +/** + * 回调处理服务接口 + * 处理PingPong支付回调通知 + */ +public interface CallbackService { + + /** + * 处理支付回调 + * + * @param callbackData 回调数据 + * @return 处理结果 + */ + boolean handleCallback(Map callbackData); +} diff --git a/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/CallbackServiceImpl.java b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/CallbackServiceImpl.java new file mode 100644 index 0000000..390b804 --- /dev/null +++ b/mt-pay/src/main/java/com/mtkj/mtpay/service/impl/CallbackServiceImpl.java @@ -0,0 +1,135 @@ +package com.mtkj.mtpay.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mtkj.mtpay.entity.PaymentOrder; +import com.mtkj.mtpay.entity.PaymentRecord; +import com.mtkj.mtpay.mapper.PaymentOrderMapper; +import com.mtkj.mtpay.mapper.PaymentRecordMapper; +import com.mtkj.mtpay.service.CallbackService; +import com.mtkj.mtpay.service.SignatureService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.Optional; + +/** + * 回调处理服务实现类 + * 处理PingPong支付回调通知 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CallbackServiceImpl implements CallbackService { + + private final PaymentOrderMapper paymentOrderMapper; + private final PaymentRecordMapper paymentRecordMapper; + private final SignatureService signatureService; + private final ObjectMapper objectMapper; + + @Override + @Transactional + public boolean handleCallback(Map callbackData) { + log.info("收到PingPong回调通知: {}", callbackData); + + // 验证签名 + if (!signatureService.verifySign(callbackData)) { + log.error("回调签名验证失败"); + return false; + } + + // 获取订单信息 + String merchantTransactionId = (String) callbackData.get("merchantTransactionId"); + String transactionId = (String) callbackData.get("transactionId"); + String code = (String) callbackData.get("code"); + String description = (String) callbackData.get("description"); + String status = (String) callbackData.get("status"); + + if (merchantTransactionId == null || merchantTransactionId.isEmpty()) { + log.error("回调数据中缺少商户订单号"); + return false; + } + + // 查询订单 + Optional orderOpt = paymentOrderMapper.findByMerchantTransactionId(merchantTransactionId); + if (orderOpt.isEmpty()) { + log.error("订单不存在: {}", merchantTransactionId); + return false; + } + + PaymentOrder order = orderOpt.get(); + + // 更新订单状态 + if (status != null && !status.isEmpty()) { + // 根据status更新订单状态 + String orderStatus = mapCallbackStatusToOrderStatus(status); + order.setStatus(orderStatus); + + if (transactionId != null && !transactionId.isEmpty()) { + order.setTransactionId(transactionId); + } + + paymentOrderMapper.updateById(order); + log.info("订单状态已更新,商户订单号: {}, 状态: {}", merchantTransactionId, orderStatus); + } + + // 保存回调记录 + PaymentRecord record = new PaymentRecord(); + record.setMerchantTransactionId(merchantTransactionId); + record.setTransactionId(transactionId); + record.setRecordType("CALLBACK"); + record.setCode(code); + record.setDescription(description); + record.setStatus(status); + + try { + record.setRequestData(objectMapper.writeValueAsString(callbackData)); + } catch (Exception e) { + log.warn("序列化回调数据失败", e); + } + + paymentRecordMapper.insert(record); + + // 处理业务逻辑(例如:通知业务系统、更新库存等) + handleBusinessLogic(order, status); + + return true; + } + + /** + * 将回调状态映射到订单状态 + */ + private String mapCallbackStatusToOrderStatus(String callbackStatus) { + return switch (callbackStatus.toUpperCase()) { + case "SUCCESS", "SUCCESSFUL" -> "SUCCESS"; + case "FAILED", "FAILURE" -> "FAILED"; + case "REVIEW" -> "REVIEW"; + case "CANCELLED", "CANCEL" -> "CANCELLED"; + case "PENDING" -> "PENDING"; + default -> { + log.warn("未知的回调状态: {}", callbackStatus); + yield "PENDING"; + } + }; + } + + /** + * 处理业务逻辑 + * 这里可以根据实际业务需求实现,例如: + * - 通知业务系统支付结果 + * - 更新库存 + * - 发送通知等 + */ + private void handleBusinessLogic(PaymentOrder order, String status) { + log.info("处理业务逻辑,订单号: {}, 状态: {}", order.getMerchantTransactionId(), status); + + // TODO: 实现具体的业务逻辑 + // 例如: + // - 如果支付成功,通知业务系统 + // - 如果支付失败,记录失败原因 + // - 如果状态为REVIEW,触发审核流程 + } +} +