zhuyifan

支付回调对接

......@@ -12,8 +12,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
......@@ -32,7 +32,7 @@ public class PayController {
private final PayServiceClient payServiceClient;
@SaIgnore
@ApiOperation(value = "支付")
@ApiOperation(value = "支付信息")
@PostMapping("/pay/inform")
@ResponseStatus(HttpStatus.OK)
public PayResponseDto pay(@Valid @RequestBody PayDTO.PayInformRequestDto payInformRequestDto) {
......@@ -40,11 +40,19 @@ public class PayController {
}
@SaIgnore
@ApiOperation(value = "支付中")
@PostMapping("/order/{orderId}/pay/waiting")
@ResponseStatus(HttpStatus.OK)
public boolean waiting(@PathVariable("orderId")Integer orderId) {
return payServiceClient.waiting(orderId);
}
@SaIgnore
@ApiOperation(value = "支付通知")
@PostMapping(value = "/pay/callback", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/pay/callback")
@ResponseStatus(HttpStatus.OK)
public void callback(@RequestBody PaymentCallbackDTO payInformRequestDto) {
payServiceClient.callback(payInformRequestDto);
public boolean callback(@Valid @RequestBody PaymentCallbackDTO paymentCallbackDTO) {
return payServiceClient.callback(paymentCallbackDTO);
}
}
\ No newline at end of file
......
package com.infoloop.tianting.model.dto;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName = "Request")
public class PaymentCallbackDTO {
@JacksonXmlProperty(localName = "TradeId")
private String tradeId;
@JacksonXmlProperty(localName = "TransSN")
private String transSN;
@JacksonXmlProperty(localName = "TransType")
private String transType;
@JacksonXmlProperty(localName = "TransAmount")
private String transAmount;
@JacksonXmlProperty(localName = "RealAmount")
private String realAmount;
@JacksonXmlProperty(localName = "PayMode")
private String payMode;
@JacksonXmlProperty(localName = "PayModeRemark")
private String payModeRemark;
@JacksonXmlProperty(localName = "TransDate")
private String transDate;
@JacksonXmlProperty(localName = "OrginalTransSN")
private String orginalTransSN;
@JacksonXmlProperty(localName = "Remark")
private String remark;
@JacksonXmlProperty(localName = "PayModeNote")
private String payModeNote;
@NotEmpty(message = "AppId不能为空")
@JsonProperty("AppId")
private String appId;
@JacksonXmlProperty(localName = "PayIdentify")
private String payIdentify;
@NotEmpty(message = "PayResult不能为空")
@JsonProperty("PayResult")
private String payResult;
@JacksonXmlProperty(localName = "PayIdentifyNote")
private String payIdentifyNote;
@NotEmpty(message = "SignInfo不能为空")
@JsonProperty("SignInfo")
private String signInfo;
}
......
package com.infoloop.tianting.model.dto;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName = "Request")
public class PaymentCallbackResult {
@JacksonXmlProperty(localName = "TradeId")
private String tradeId;
@JacksonXmlProperty(localName = "TransSN")
private String transSN;
@JacksonXmlProperty(localName = "TransType")
private String transType;
@JacksonXmlProperty(localName = "TransAmount")
private String transAmount;
@JacksonXmlProperty(localName = "RealAmount")
private String realAmount;
@JacksonXmlProperty(localName = "PayMode")
private String payMode;
@JacksonXmlProperty(localName = "PayModeRemark")
private String payModeRemark;
@JacksonXmlProperty(localName = "TransDate")
private String transDate;
@JacksonXmlProperty(localName = "OrginalTransSN")
private String orginalTransSN;
@JacksonXmlProperty(localName = "Remark")
private String remark;
@JacksonXmlProperty(localName = "PayModeNote")
private String payModeNote;
@JacksonXmlProperty(localName = "PayIdentify")
private String payIdentify;
@JacksonXmlProperty(localName = "PayIdentifyNote")
private String payIdentifyNote;
}
......@@ -15,6 +15,7 @@ import com.infoloop.tianting.clientcustomerorderservice.CloseTimeType;
import com.infoloop.tianting.clientcustomerorderservice.CreateClientCustomerOrderRpcRequest;
import com.infoloop.tianting.clientcustomerorderservice.CreateClientCustomerOrderRpcResponse;
import com.infoloop.tianting.clientcustomerorderservice.CustomerNotice;
import com.infoloop.tianting.clientcustomerorderservice.GetClientCustomerOrderByIdRpcRequest;
import com.infoloop.tianting.clientcustomerorderservice.GetClientCustomerOrderDetailsByIdsRpcRequest;
import com.infoloop.tianting.clientcustomerorderservice.GetClientCustomerOrderDetailsByOrderIdsRpcRequest;
import com.infoloop.tianting.clientcustomerorderservice.GetClientCustomerOrdersByCustomerIdRpcRequest;
......@@ -54,6 +55,7 @@ import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
......@@ -71,6 +73,15 @@ public class OrderServiceRpcClient {
private final OperatorServiceRpcClient operatorServiceRpcClient;
private final RedissonClient redissonClient;
@Nullable
public SingleClientCustomerOrderRpcResponse getOrderById(Integer orderId) {
final var clientCustomerOrderById = orderServiceRpcBlockingStub.getClientCustomerOrderById(GetClientCustomerOrderByIdRpcRequest.newBuilder().setId(orderId).build());
if (!clientCustomerOrderById.hasResponse()) {
return null;
}
return clientCustomerOrderById.getResponse();
}
public List<SingleClientCustomerOrderRpcResponse> getOrdersByCustomerId(Integer customerId) {
return orderServiceRpcBlockingStub.getClientCustomerOrdersByCustomerId(
GetClientCustomerOrdersByCustomerIdRpcRequest.newBuilder().setCustomerId(customerId).build()).getResponsesList();
......@@ -138,6 +149,16 @@ public class OrderServiceRpcClient {
return orderServiceRpcBlockingStub.updateClientCustomerOrder(request);
}
public UpdateClientCustomerOrderRpcResponse updateClientCustomerOrderPaymentResult(SingleClientCustomerOrderRpcResponse order, ClientCustomerOrderModification modification) {
final var request = UpdateClientCustomerOrderRpcRequest.newBuilder()
.setEnterpriseId(order.getEnterpriseId())
.setUpdatedBy(order.getCreatedBy())
.setUpdateSource(order.getCreationSource())
.setModification(modification)
.build();
return orderServiceRpcBlockingStub.updateClientCustomerOrder(request);
}
public BatchUpdateClientCustomerOrderDetailsRpcResponse batchUpdateClientCustomerOrderDetails(
OrderDetailBatchUpdateDto orderDetailBatchUpdateDto) {
......
package com.infoloop.tianting.service.client;
import cn.hutool.core.convert.Convert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.infoloop.tianting.clientcustomerorderservice.ClientCustomerOrderModification;
import com.infoloop.tianting.clientcustomerorderservice.PayStatusEnum;
import com.infoloop.tianting.config.BusinessConfig;
import com.infoloop.tianting.model.dto.PayDTO.PayInformRequestDto;
import com.infoloop.tianting.model.dto.PayDTO.PayResponseDto;
import com.infoloop.tianting.model.dto.PayDTO.ThirdPartyPayInformRequestDto;
import com.infoloop.tianting.model.dto.PaymentCallbackDTO;
import com.infoloop.tianting.model.dto.PaymentCallbackResult;
import com.infoloop.tianting.utils.DateUtil;
import com.infoloop.tianting.utils.EncryptionUtil;
import lombok.RequiredArgsConstructor;
......@@ -25,6 +29,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.text.ParseException;
import java.time.LocalDateTime;
......@@ -37,11 +43,11 @@ public class PayServiceClient {
private final BusinessConfig businessConfig;
private final OrderServiceRpcClient orderServiceRpcClient;
private final XmlMapper xmlMapper = new XmlMapper();
@Value("${payment.client-id:Order0001}")
private String clientId;
private final XmlMapper xmlMapper = new XmlMapper();
public PayResponseDto payInform(PayInformRequestDto payRequestDto) {
String url = businessConfig.getPayUrl();
String requestDate = DateUtil.formatDate(LocalDateTime.now(), DateUtil.YMDHMS);
......@@ -55,7 +61,7 @@ public class PayServiceClient {
jsonDataString = objectMapper.writeValueAsString(jsonData);
} catch (JsonProcessingException e) {
log.error("Failed to serialize request data", e);
throw new IllegalArgumentException("支付通知失败: JSON 解析错误");
throw new IllegalArgumentException("支付信息获取失败: JSON 解析错误");
}
HttpEntity<String> requestEntity = new HttpEntity<>(jsonDataString, createHeaders());
log.info("payInform request: {}", jsonDataString);
......@@ -88,19 +94,84 @@ public class PayServiceClient {
private PayResponseDto processResponse(ResponseEntity<String> responseEntity) {
if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) {
log.error("payInform failed with response: {}", responseEntity.getBody());
throw new IllegalArgumentException("支付通知失败");
throw new IllegalArgumentException("支付信息获取失败");
}
try {
log.info("processResponse response: {}", responseEntity.getBody());
return xmlMapper.readValue(responseEntity.getBody(), PayResponseDto.class);
} catch (Exception e) {
log.error("Failed to parse XML response", e);
throw new RuntimeException("支付通知结果解析失败", e);
throw new RuntimeException("支付信息结果解析失败", e);
}
}
public void callback(PaymentCallbackDTO payInformRequestDto) {
log.info("callback: {}", payInformRequestDto);
public boolean callback(PaymentCallbackDTO paymentCallbackDTO) {
log.info("callback request received: {}", paymentCallbackDTO);
try {
final var paymentCallbackResult = xmlMapper.readValue(paymentCallbackDTO.getPayResult(), PaymentCallbackResult.class);
log.info("Parsed paymentCallbackResult: {}", paymentCallbackResult);
final var orderId = Integer.parseInt(paymentCallbackResult.getTradeId());
final var orderById = orderServiceRpcClient.getOrderById(orderId);
if (orderById == null) {
log.error("Order not found, orderId: {}", orderId);
return false;
}
if (orderById.getPayStatus() != PayStatusEnum.IN_PROGRESS) {
log.info("Order {} already processed, skipping.", orderId);
return true;
}
long payTime;
try {
payTime = DateUtil.parseDate(paymentCallbackResult.getTransDate(), "yyyyMMddHHmmss").getTime();
} catch (ParseException e) {
log.error("Failed to parse TransDate: {}", paymentCallbackResult.getTransDate(), e);
throw new IllegalArgumentException("支付时间格式错误: " + paymentCallbackResult.getTransDate(), e);
}
BigDecimal realAmount;
try {
realAmount = Convert.toBigDecimal(paymentCallbackResult.getRealAmount());
if (realAmount == null) {
throw new NumberFormatException("RealAmount is null or empty");
}
} catch (NumberFormatException e) {
log.error("Invalid RealAmount value: {}", paymentCallbackResult.getRealAmount(), e);
throw new IllegalArgumentException("支付金额错误: " + paymentCallbackResult.getRealAmount(), e);
}
final var build = ClientCustomerOrderModification.newBuilder()
.setId(orderId)
.setShouldUpdatePayTime(true).setPayTime(payTime)
.setShouldUpdatePayPrice(true).setPayPrice(realAmount.multiply(new BigDecimal("100")).intValue())
.setShouldUpdatePayStatus(true).setPayStatus(PayStatusEnum.SUCCEED)
.setShouldUpdateTransactionId(true).setTransactionId(paymentCallbackResult.getTransSN())
.build();
final var updateResponse = orderServiceRpcClient.updateClientCustomerOrderPaymentResult(orderById, build);
log.info("callback updateClientCustomerOrder result: {}", updateResponse.getIsUpdated());
return updateResponse.getIsUpdated();
} catch (JsonProcessingException e) {
log.error("Failed to parse XML response, raw XML: {}", paymentCallbackDTO.getPayResult(), e);
throw new RuntimeException("支付通知结果解析失败", e);
} catch (Exception e) {
log.error("Unexpected error in callback, request: {}", paymentCallbackDTO, e);
throw new RuntimeException("支付通知处理失败", e);
}
}
public boolean waiting(Integer orderId) {
final var orderById = orderServiceRpcClient.getOrderById(orderId);
if (orderById == null) {
log.error("waiting Order not found, orderId: {}", orderId);
return false;
}
if (orderById.getPayStatus() != PayStatusEnum.TO_PAY) {
log.info("waiting Order {} already processed, skipping.", orderId);
return false;
}
final var build = ClientCustomerOrderModification.newBuilder()
.setId(orderId)
.setShouldUpdatePayStatus(true).setPayStatus(PayStatusEnum.IN_PROGRESS)
.build();
final var updateResponse = orderServiceRpcClient.updateClientCustomerOrderPaymentResult(orderById, build);
log.info("waiting updateClientCustomerOrder result: {}", updateResponse.getIsUpdated());
return updateResponse.getIsUpdated();
}
}
......